/* =============================================================================================
   2Forge CRM - design system
   ---------------------------------------------------------------------------------------------
   Hand-rolled, no framework. Enterprise-sober: high information density, Fluent-like restraint,
   no decoration that does not carry meaning. A salesperson looks at this for seven hours a day.

   HOW TO USE IT
     - Never write a raw colour, spacing or radius in a page. Use a custom property from :root.
     - Never put a max-width on a data component. Tables, boards and lists take the full content
       column; only forms and prose are narrowed, by .form-column and .prose.
     - The component classes below are the shared vocabulary. Add a modifier, do not fork a class.
     - Never write a raw font-size. There are SEVEN steps, named by role, in the token block:
       --text-micro / --text-meta / --text-body / --text-title / --text-head / --text-page /
       --text-metric. There is no eighth. If none fits, the element has the wrong role.
     - There are FOUR weights: 300 / 400 / 500 / 600. 700 and "bold" do not exist here. A
       heading is light and large; 600 is for uppercase micro-labels and nothing else.

   THEMING
     Tokens are declared once on :root for light, then redefined twice for dark: once behind the
     OS preference (guarded so an explicit light choice still wins) and once behind an explicit
     data-theme="dark". The two dark blocks must stay in sync.
   ============================================================================================= */

/* ---------------------------------------------------------------------------------------------
   0. TYPEFACE - Geist, self-hosted. ONE FAMILY, FOUR WEIGHTS, NOTHING ELSE.
   ---------------------------------------------------------------------------------------------
   SELF-HOSTED, NOT GOOGLE FONTS. Embedding the Google CDN sends every visitor's IP address to
   a third country on every page load; a German court has already held that to be a GDPR
   breach, and this product ships to an EU client. 84 KB for eight files, served from our own
   origin, cached for a year.

   ONE FAMILY. IBM Plex Sans and IBM Plex Mono are both gone. The monospace existed for exactly
   one job - making figures line up in a column - and Geist does that job itself the moment
   tabular figures are switched on, which they are, globally, on body. A second family that buys
   nothing still costs a download, a fallback stack, and a decision every time someone writes a
   rule. There is no --font-mono token either: it went with them.

   Two subsets per weight. "latin" carries ASCII, "latin-ext" carries the Croatian letters -
   c with caron, c with acute, z, s, d with stroke. Both are required: latin-ext alone has no
   ASCII, so the unicode-range on each face is what makes a Croatian word render in one font
   instead of half in Geist and half in the fallback. The ranges below are the ones the IBM Plex
   faces used and are already proven against this app's Croatian copy.
   ============================================================================================= */

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 300;
    font-display: swap;
    src: url("fonts/geist-300-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 300;
    font-display: swap;
    src: url("fonts/geist-300-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("fonts/geist-400-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("fonts/geist-400-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("fonts/geist-500-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("fonts/geist-500-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("fonts/geist-600-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Geist";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("fonts/geist-600-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------------------------------------------------------------------------------------------
   1. TOKENS - light
   --------------------------------------------------------------------------------------------- */

:root {
    color-scheme: light dark;

    /* Neutral ramp: warm graphite on paper. Warm, because a cool grey next to a pure-black
       accent reads as cheap; paper reads as considered. */
    --c-neutral-0: #ffffff;
    --c-neutral-25: #fdfdfc;
    --c-neutral-50: #fafaf9;
    --c-neutral-100: #f4f4f2;
    --c-neutral-200: #e4e4e1;
    --c-neutral-300: #d3d3cf;
    --c-neutral-400: #a8a8a2;
    --c-neutral-500: #85857e;
    --c-neutral-600: #6b6b66;
    --c-neutral-700: #4a4a46;
    --c-neutral-800: #2c2c29;
    --c-neutral-900: #1a1a18;
    --c-neutral-950: #111111;

    /* THERE IS NO BRAND HUE. Ink is the accent: a primary button is black, an active row is
       black. Colour in this product means something - a pipeline stage, an ageing threshold, a
       status - and chrome that spends colour decoratively spends the signal too. */
    --c-ink-900: #111111;
    --c-ink-800: #1f1f1d;
    --c-ink-700: #2c2c29;
    --c-ink-600: #4a4a46;

    /* Semantic surfaces */
    /* THREE TIERS OF SURFACE, and the order matters. The navigation rail is RECESSED, the page
       sits above it, a card is brightest. A rail lighter than the page it borders reads as flat
       and slightly wrong without the eye being able to say why. The payoff is the active nav item:
       on a recessed rail it can be plain white and it will look lifted, which buys a premium
       active state for no colour and no shadow. */
    --surface-page: #fbfbfa;
    --surface: #ffffff;
    --surface-raised: #ffffff;
    --surface-sunken: #f4f4f2;
    --surface-hover: #f7f7f5;
    --surface-active: #f1f1ee;
    --surface-inverse: #111111;
    --surface-nav: #f6f6f4;
    --surface-nav-hover: #eeeeeb;
    --surface-nav-active: #ffffff;

    --border: #e4e4e1;
    --border-strong: #d3d3cf;
    --border-nav: #e4e4e1;

    --text: #111111;
    --text-muted: #6b6b66;
    --text-subtle: #6e6e68;
    --text-inverse: #ffffff;
    --text-nav: #4a4a46;
    --text-nav-strong: #111111;
    --text-on-accent: #ffffff;

    --accent: #111111;
    --accent-hover: #2c2c29;
    --accent-active: #000000;
    --accent-soft: #f4f4f2;
    --accent-soft-border: #d3d3cf;
    --accent-text: #111111;

    --focus-ring-color: #111111;
    --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--focus-ring-color);

    /* THE ONE COLOUR IN THIS PRODUCT THAT MEANS A COMPANY. #C0DF14 is 2Forge's green, off the
       logotype on 2forge.io, and it is declared once here so that nothing else reaches for it by
       eye. It is NOT redefined for dark: a brand colour that changes with the theme is a different
       brand colour, and this one is legible on both the paper command bar and the near-black rail.

       It does not contradict "there is no brand hue" at the top of this block. That rule is about
       CHROME spending colour decoratively - a coloured button, a tinted panel, an accented row -
       because colour here marks a pipeline stage, an ageing threshold or a status. A logo is the
       one thing entitled to be its own colour, and this token is used by exactly one element. */
    --brand-green: #c0df14;

    /* Status - used by .alert, .badge--*, validation messages */
    --info: #1a56db;
    --info-bg: #eef3ff;
    --info-border: #c4d5fb;
    --info-text: #14398c;

    --success: #147a3d;
    --success-bg: #e6f5ec;
    --success-border: #b7e0c7;
    --success-text: #0f5c30;

    --warning: #9a6200;
    --warning-bg: #fdf3e2;
    --warning-border: #f2dcb0;
    --warning-text: #7a4d00;

    --danger: #c22a22;
    --danger-bg: #fdecec;
    --danger-border: #f5c9c6;
    --danger-text: #8f201a;

    /* Pipeline stages - a cool-neutral to green progression, closed-lost a muted red, dropped
       grey. Each pair is a soft fill with a text colour that clears WCAG AA against that fill. */
    --stage-new-inquiry-bg: #eef1f5;
    --stage-new-inquiry-fg: #44506b;
    --stage-awaiting-client-bg: #eaeefb;
    --stage-awaiting-client-fg: #3f4b85;
    --stage-questionnaire-sent-bg: #e9ecfd;
    --stage-questionnaire-sent-fg: #3d45a0;
    --stage-questionnaire-returned-bg: #e7e9ff;
    --stage-questionnaire-returned-fg: #3730a3;
    --stage-quote-sent-bg: #e3eefe;
    --stage-quote-sent-fg: #1c4da6;
    --stage-meeting-scheduled-bg: #ddf0fb;
    --stage-meeting-scheduled-fg: #12537b;
    --stage-meeting-held-bg: #d9f1f2;
    --stage-meeting-held-fg: #0d5d63;
    --stage-negotiation-bg: #d8f2ea;
    --stage-negotiation-fg: #0a614e;
    --stage-contract-signed-bg: #dcf3de;
    --stage-contract-signed-fg: #18682a;
    --stage-won-bg: #cdeed2;
    --stage-won-fg: #115a24;
    --stage-lost-bg: #fae8e7;
    --stage-lost-fg: #94231e;
    --stage-disqualified-bg: #ededef;
    --stage-disqualified-fg: #575c63;

    /* Ageing - amber past 7 days, red past 14. Both clear AA against their own fill. */
    --ageing-fresh-fg: #5a6679;
    --ageing-fresh-bg: transparent;
    --ageing-warn-fg: #8a5503;
    --ageing-warn-bg: #fdf1dc;
    --ageing-late-fg: #9d1c16;
    --ageing-late-bg: #fbe4e3;

    /* ---- LIST WORKBENCH -------------------------------------------------------------------
       The colours the four list screens share. Every one of them is a value a page would
       otherwise invent inline, which is exactly how a design system rots. */

    /* A monogram is an ANCHOR, not a label: one flat neutral tint for every row, never a hue
       derived from the name. Twenty-five differently coloured circles is a fruit salad, and it
       spends the colour budget that the pipeline stages need. */
    --monogram-bg: #efefec;
    --monogram-fg: #2c2c29;

    /* A selected row has to beat the hover tint without becoming a second accent. */
    --row-selected-bg: #eeeeea;
    --row-selected-marker: #111111;

    /* The bulk bar is the one inverted surface in the product. It appears only while a selection
       exists, which is what makes an inversion legible rather than loud. */
    --bulk-bar-bg: #111111;
    --bulk-bar-fg: #ffffff;
    --bulk-bar-border: rgba(255, 255, 255, 0.22);
    --bulk-bar-hover: rgba(255, 255, 255, 0.14);

    --skeleton-bg: #eeeeeb;
    --skeleton-sheen: rgba(255, 255, 255, 0.9);

    /* The stand-in for a value nobody entered. Recessive on purpose, and rendered aria-hidden
       wherever it is used: an empty cell must read as empty to a screen reader, not as a dash. */
    --text-empty: #6e6e69;

    /* Dot marks. A 5px dot plus a plain label, in place of a filled pill: the dot is the only
       colour on the row, so these are the least saturated hues in the product that still
       separate from one another. The label carries the meaning; the dot only speeds the scan. */
    --dot-neutral: #82827c;
    --dot-positive: #2f7d4f;
    --dot-info: #4a5b7a;
    --dot-attention: #b07400;
    --dot-critical: #c22a22;
    --dot-inactive: #878782;

    /* Spacing - 4px base. Nothing in this product is spaced off the scale. */
    --space-0: 0;
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.25rem;
    --space-6: 1.5rem;
    --space-7: 2rem;
    --space-8: 2.5rem;
    --space-9: 3rem;
    --space-10: 4rem;

    --radius-xs: 3px;
    --radius-sm: 5px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-full: 999px;

    --shadow-1: 0 1px 2px rgba(17, 22, 29, 0.06), 0 1px 1px rgba(17, 22, 29, 0.04);
    --shadow-2: 0 2px 6px rgba(17, 22, 29, 0.08), 0 1px 2px rgba(17, 22, 29, 0.05);
    --shadow-3: 0 12px 32px rgba(17, 22, 29, 0.16), 0 2px 8px rgba(17, 22, 29, 0.08);

    /* TYPOGRAPHY. One family: Geist. The system stack stays as the fallback so a failed font
       load degrades rather than breaks.

       MEASURED, NOT ASSUMED - read out of the real font files:
         Geist          x-height 0.530 em, cap 0.710 em, ascent+descent 1.300 em
         IBM Plex Sans  x-height 0.516 em, cap 0.698 em, ascent+descent 1.300 em
       Geist is 2.7% larger on the x-height at the same pixel size, but the line box is
       IDENTICAL, so nothing that depends on line height had to move. The base also drops
       14px -> 13px, which more than absorbs the difference: perceived x-height goes
       7.22px -> 6.89px. That is why no fixed control height (.btn 32px, .input 34px, the
       40px/32px table rows) needed to grow - at 13px they gained slack, not lost it. */
    --font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
        "Segoe UI", "Noto Sans", Roboto, "Helvetica Neue", Arial, sans-serif;

    /* THERE IS NO --font-mono. There was one, as an alias of --font-sans, only for as long as
       the sweep needed it; every use of it is gone and so is the token itself. A monospace
       existed here for exactly one job, lining figures up in a column, and Geist does that job
       itself now that tabular figures are on globally (see body). Reintroducing a second family
       means re-arguing the decision, not adding a line. */

    /* ------------------------------------------------------------------------------------
       THE SCALE. Seven steps, named by ROLE and not by size, because --text-sm invites
       someone to pick it for how it looks while --text-micro only fits one job. There is no
       eighth step. If something does not fit, it is the wrong role, not a missing size.
       ------------------------------------------------------------------------------------ */
    --text-micro: 0.6875rem;  /* 11px  600, uppercase, 0.1em - column headers, labels, chips */
    --text-meta: 0.75rem;     /* 12px  400 - secondary text, captions, footnotes             */
    --text-body: 0.8125rem;   /* 13px  400 - BASE: body, tables, inputs, buttons, nav        */
    --text-title: 0.9375rem;  /* 15px  500 - card and section titles                         */
    --text-head: 1.125rem;    /* 18px  400 - sub-page headings                               */
    --text-page: 1.625rem;    /* 26px  300 - page titles                                    */
    --text-metric: 1.75rem;   /* 28px  300, tabular - KPI figures                            */

    /* 11px IS THE FLOOR and it is not arbitrary: 10px uppercase at 0.1em tracking is where
       legibility actually fails on a 1080p laptop panel, which is what the client's sales desk
       runs. Every 9px and 10px in the old sheet came up to --text-micro. */

    /* --leading-normal was 1.45 under Plex. Nudged to 1.50 because Geist's larger x-height
       wants the air; at 13px that is a 19.5px line, still shorter than the 20.3px that
       14px/1.45 produced, so density improved rather than regressed. */
    --leading-tight: 1.25;
    --leading-normal: 1.5;
    --leading-relaxed: 1.6;

    /* THE WEIGHTS. Exactly four. 700 and "bold" are BANNED: a heading in this product is light
       and large, not bold. The weight contrast between a 300 page title and the 600 uppercase
       micro-labels under it IS the editorial idea; a bold heading collapses it. */
    --weight-light: 300;      /* display only: page titles, KPI metrics */
    --weight-normal: 400;     /* body                                   */
    --weight-medium: 500;     /* emphasis, active nav, name cells, buttons */
    --weight-semibold: 600;   /* micro-labels ONLY                      */

    /* THE NINE RETIRED SIZE NAMES ARE GONE, and so is --weight-bold. --text-2xs / -xs / -sm /
       -base / -md / -lg / -xl / -2xl / -3xl / -display pointed at the new scale for exactly one
       wave, so the component sweep could not break the app halfway through. The sweep finished,
       the aliases were deleted, and TypographyTests now fails the build if one comes back. Do
       not re-add a name because it reads better in a rule: pick the role. */


    /* Layout */
    --nav-width: 248px;

    /* THE RAIL. The collapsed sidebar is 60px, not zero: 34px of item plus 13px of breathing room
       either side puts a 16px glyph on the same optical centre it sits on when the rail is open, so
       collapsing does not make the icons jump sideways. */
    --nav-rail-width: 60px;

    /* 56. It was 48 before the bar carried anything, then 52 to make room for a logo, and it is 56
       because every control on the bar is now 36px and 56 is what gives that rank ten pixels of air
       above and below it. The controls were 32 to match the toolbars that sit over dense tables;
       the command bar is not dense, and at 32 seven controls float in a 56px band instead of
       forming a line across it.

       It is read by more than the bar: .table-surface parks every sticky table header under it, and
       MainLayout.razor.css measures the sidebar and the mobile drawer from it. */
    --topbar-height: 56px;

    /* Table row heights. A page never writes a height; it sets a density class and the table
       reads it from here, so every list in the product breathes identically. */
    --row-height: 40px;
    --row-height-compact: 32px;
    --content-max: 1800px;
    --form-column: 640px;
    --prose-max: 72ch;
    --gutter: 1.5rem;

    --transition-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1);
    --transition-base: 180ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* ---------------------------------------------------------------------------------------------
   2. TOKENS - dark. Declared twice on purpose: once for the OS preference (unless the user has
   explicitly asked for light) and once for an explicit choice of dark.
   --------------------------------------------------------------------------------------------- */

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --surface-page: #0e1319;
        --surface: #161c24;
        --surface-raised: #1c232c;
        --surface-sunken: #11171e;
        --surface-hover: #1f2731;
        --surface-active: #263039;
        --surface-inverse: #eef1f5;
        --surface-nav: #131312;
        --surface-nav-hover: #1e1e1c;
        --surface-nav-active: #262624;

        --border: #262f3a;
        --border-strong: #35414f;
        --border-nav: #2c2c29;

        --text: #e7ecf3;
        --text-muted: #9aa7b8;
        --text-subtle: #7b8798;
        --text-inverse: #11161d;
        --text-nav: #a8a8a2;
        --text-nav-strong: #ffffff;
        --text-on-accent: #0e1319;

        --accent: #4d87f5;
        --accent-hover: #6b9cf8;
        --accent-active: #85adfa;
        --accent-soft: #172540;
        --accent-soft-border: #26406b;
        --accent-text: #9dbdfb;

        --focus-ring-color: #5d8cf5;

        --info: #6b9cf8;
        --info-bg: #17223a;
        --info-border: #2a3d63;
        --info-text: #b3c9fb;

        --success: #56c281;
        --success-bg: #12291d;
        --success-border: #1f4a32;
        --success-text: #93dfb0;

        --warning: #e0a44a;
        --warning-bg: #2e2413;
        --warning-border: #4d3c1c;
        --warning-text: #f0c47c;

        --danger: #f07a72;
        --danger-bg: #331b1a;
        --danger-border: #55302d;
        --danger-text: #f5aba5;

        --stage-new-inquiry-bg: #272e39;
        --stage-new-inquiry-fg: #b2becf;
        --stage-awaiting-client-bg: #242a44;
        --stage-awaiting-client-fg: #b6c0ee;
        --stage-questionnaire-sent-bg: #24284a;
        --stage-questionnaire-sent-fg: #b3b9f2;
        --stage-questionnaire-returned-bg: #26254e;
        --stage-questionnaire-returned-fg: #b8b4f5;
        --stage-quote-sent-bg: #1c2d4b;
        --stage-quote-sent-fg: #a8c8f7;
        --stage-meeting-scheduled-bg: #133146;
        --stage-meeting-scheduled-fg: #92cef0;
        --stage-meeting-held-bg: #0f363a;
        --stage-meeting-held-fg: #86d8de;
        --stage-negotiation-bg: #0e372e;
        --stage-negotiation-fg: #80dcc1;
        --stage-contract-signed-bg: #143b1d;
        --stage-contract-signed-fg: #90dc9d;
        --stage-won-bg: #14411f;
        --stage-won-fg: #8ce49f;
        --stage-lost-bg: #3c1f1d;
        --stage-lost-fg: #f0a8a2;
        --stage-disqualified-bg: #2c3138;
        --stage-disqualified-fg: #aeb5bf;

        --ageing-fresh-fg: #9aa7b8;
        --ageing-warn-fg: #f0c268;
        --ageing-warn-bg: #362a13;
        --ageing-late-fg: #f4a49d;
        --ageing-late-bg: #3d1f1d;

        --monogram-bg: #253040;
        --monogram-fg: #cdd7e4;

        --row-selected-bg: #202b3a;
        --row-selected-marker: #4d87f5;

        --bulk-bar-bg: #eef1f5;
        --bulk-bar-fg: #11161d;
        --bulk-bar-border: rgba(17, 22, 29, 0.24);
        --bulk-bar-hover: rgba(17, 22, 29, 0.10);

        --skeleton-bg: #1f2731;
        --skeleton-sheen: rgba(255, 255, 255, 0.07);

        --text-empty: #8b97a8;

        --dot-neutral: #7b8798;
        --dot-positive: #56c281;
        --dot-info: #8fa8cd;
        --dot-attention: #e0a44a;
        --dot-critical: #f07a72;
        --dot-inactive: #6f7986;

        --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
        --shadow-2: 0 2px 6px rgba(0, 0, 0, 0.45);
        --shadow-3: 0 14px 34px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4);
    }
}

:root[data-theme="dark"] {
    --surface-page: #0e1319;
    --surface: #161c24;
    --surface-raised: #1c232c;
    --surface-sunken: #11171e;
    --surface-hover: #1f2731;
    --surface-active: #263039;
    --surface-inverse: #eef1f5;
    --surface-nav: #131312;
    --surface-nav-hover: #1e1e1c;
    --surface-nav-active: #262624;

    --border: #262f3a;
    --border-strong: #35414f;
    --border-nav: #2c2c29;

    --text: #e7ecf3;
    --text-muted: #9aa7b8;
    --text-subtle: #7b8798;
    --text-inverse: #11161d;
    --text-nav: #a8a8a2;
    --text-nav-strong: #ffffff;
    --text-on-accent: #0e1319;

    --accent: #4d87f5;
    --accent-hover: #6b9cf8;
    --accent-active: #85adfa;
    --accent-soft: #172540;
    --accent-soft-border: #26406b;
    --accent-text: #9dbdfb;

    --focus-ring-color: #5d8cf5;

    --info: #6b9cf8;
    --info-bg: #17223a;
    --info-border: #2a3d63;
    --info-text: #b3c9fb;

    --success: #56c281;
    --success-bg: #12291d;
    --success-border: #1f4a32;
    --success-text: #93dfb0;

    --warning: #e0a44a;
    --warning-bg: #2e2413;
    --warning-border: #4d3c1c;
    --warning-text: #f0c47c;

    --danger: #f07a72;
    --danger-bg: #331b1a;
    --danger-border: #55302d;
    --danger-text: #f5aba5;

    --stage-new-inquiry-bg: #272e39;
    --stage-new-inquiry-fg: #b2becf;
    --stage-awaiting-client-bg: #242a44;
    --stage-awaiting-client-fg: #b6c0ee;
    --stage-questionnaire-sent-bg: #24284a;
    --stage-questionnaire-sent-fg: #b3b9f2;
    --stage-questionnaire-returned-bg: #26254e;
    --stage-questionnaire-returned-fg: #b8b4f5;
    --stage-quote-sent-bg: #1c2d4b;
    --stage-quote-sent-fg: #a8c8f7;
    --stage-meeting-scheduled-bg: #133146;
    --stage-meeting-scheduled-fg: #92cef0;
    --stage-meeting-held-bg: #0f363a;
    --stage-meeting-held-fg: #86d8de;
    --stage-negotiation-bg: #0e372e;
    --stage-negotiation-fg: #80dcc1;
    --stage-contract-signed-bg: #143b1d;
    --stage-contract-signed-fg: #90dc9d;
    --stage-won-bg: #14411f;
    --stage-won-fg: #8ce49f;
    --stage-lost-bg: #3c1f1d;
    --stage-lost-fg: #f0a8a2;
    --stage-disqualified-bg: #2c3138;
    --stage-disqualified-fg: #aeb5bf;

    --ageing-fresh-fg: #9aa7b8;
    --ageing-warn-fg: #f0c268;
    --ageing-warn-bg: #362a13;
    --ageing-late-fg: #f4a49d;
    --ageing-late-bg: #3d1f1d;

    --monogram-bg: #253040;
    --monogram-fg: #cdd7e4;

    --row-selected-bg: #202b3a;
    --row-selected-marker: #4d87f5;

    --bulk-bar-bg: #eef1f5;
    --bulk-bar-fg: #11161d;
    --bulk-bar-border: rgba(17, 22, 29, 0.24);
    --bulk-bar-hover: rgba(17, 22, 29, 0.10);

    --skeleton-bg: #1f2731;
    --skeleton-sheen: rgba(255, 255, 255, 0.07);

    --text-empty: #8b97a8;

    --dot-neutral: #7b8798;
    --dot-positive: #56c281;
    --dot-info: #8fa8cd;
    --dot-attention: #e0a44a;
    --dot-critical: #f07a72;
    --dot-inactive: #6f7986;

    --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-2: 0 2px 6px rgba(0, 0, 0, 0.45);
    --shadow-3: 0 14px 34px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4);
}

/* ---------------------------------------------------------------------------------------------
   3. RESET AND BASE
   --------------------------------------------------------------------------------------------- */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body {
    margin: 0;
    /* An explicit background, so the page never inherits the user agent's white in dark mode. */
    background-color: var(--surface-page);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: var(--text-body);
    line-height: var(--leading-normal);

    /* TABULAR FIGURES, GLOBALLY. This is what lets one family replace two. IBM Plex Sans had
       tabular digits by default, so columns aligned for free; GEIST DOES NOT - its "1" is
       narrow. Measured in the browser against the real file at 13px: "1111111111" renders
       45.25px wide against 87.36px for "0000000000". With font-variant-numeric: tabular-nums
       every ten-digit string measures exactly 78px.

       It is set GLOBALLY rather than per-component on purpose. A CRM is almost entirely
       figures - money, counts, dates, percentages, ageing days - and a global default means
       nobody can forget it on a column somebody adds six months from now. Proportional figures
       are correct in running prose and nowhere else in this product, so .prose opts back out. */
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;

    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

h1,
h2,
h3,
h4,
h5,
h6 {
    margin: 0;
    /* NOT 600. 600 is reserved for uppercase micro-labels; a heading here is distinguished by
       size and by being LIGHTER than the label under it, which is the whole editorial idea. */
    font-weight: var(--weight-medium);
    line-height: var(--leading-tight);
    color: var(--text);
}

/* The ladder is 26 / 18 / 15 / 13, and the weight goes DOWN as the size goes up. */
h1 {
    font-size: var(--text-page);
    font-weight: var(--weight-light);
    letter-spacing: -0.02em;
    line-height: 1.15;
}

h2 {
    font-size: var(--text-head);
    font-weight: var(--weight-normal);
}

h3 {
    font-size: var(--text-title);
}

h4,
h5,
h6 {
    font-size: var(--text-body);
}

p {
    margin: 0 0 var(--space-3);
}

/* THE BROWSER'S OWN BOLD IS STILL A BOLD. <strong> and <b> resolve to 700 in the user-agent
   sheet, which is the one weight this design system bans, and no amount of discipline in our own
   rules removes it - the markup is what summons it. Measured in the dashboard: two elements
   rendering at 700 with nothing in any stylesheet asking for it. Emphasis here is 500 against a
   400 field, which is the same amount of emphasis at a fifth of the shout. */
b,
strong {
    font-weight: var(--weight-medium);
}

a {
    color: var(--accent);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

hr {
    border: 0;
    border-top: 1px solid var(--border);
    margin: var(--space-5) 0;
}

/* No separate monospace family any more, so no size compensation either: Plex Mono ran optically
   large next to Plex Sans and needed 0.925em to sit down. Geist code in Geist prose is the same
   face at the same size, which is the point of having one family. */
code,
kbd,
samp {
    font-family: var(--font-sans);
    font-size: var(--text-body);
}

/* Tabular figures are the GLOBAL default now (see body), so this list is no longer what makes
   columns line up. .tabular survives as the explicit opt-back-IN for figures that appear inside
   .prose, which is the one place proportional digits are correct. */
.tabular {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: var(--radius-xs);
}

::selection {
    background: var(--accent-soft);
    color: var(--accent-text);
}

/* Screen-reader-only, for labels an icon button still needs. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---------------------------------------------------------------------------------------------
   4. LAYOUT PRIMITIVES
   --------------------------------------------------------------------------------------------- */

/* The one content column. Data components fill it; they never set a width of their own. */
/* LEFT-ALIGNED, NOT CENTRED. margin-inline:auto strands the content in the middle of a wide
   monitor: at 2560px it left a 248px gap between the sidebar and the first KPI tile, which reads
   as a broken layout. Data hugs the navigation; the max-width only stops a table becoming
   unreadable on an ultrawide. */
.content-column {
    width: 100%;
    max-width: var(--content-max);
    margin-inline: 0 auto;
    padding-inline: var(--gutter);
}

/* Forms stay narrow: a 1800px-wide text input is unreadable and unfillable. */
.form-column {
    max-width: var(--form-column);
}

/* PROPORTIONAL FIGURES BELONG IN RUNNING PROSE and nowhere else here. A tabular "1" sits in a
   slot as wide as a "0", which is exactly right in a column and looks gappy in a sentence.
   A figure inside prose that genuinely needs to align - a small table, a list of amounts -
   takes .tabular explicitly. */
.prose {
    max-width: var(--prose-max);
    line-height: var(--leading-relaxed);
    font-variant-numeric: normal;
    font-feature-settings: normal;
}

.stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.stack--tight {
    gap: var(--space-2);
}

.row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.row--wrap {
    flex-wrap: wrap;
}

.spacer {
    flex: 1 1 auto;
}

.grid {
    display: grid;
    gap: var(--space-4);
}

.grid--kpi {
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

.grid--2 {
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}

.muted {
    color: var(--text-muted);
}

.subtle {
    color: var(--text-subtle);
}

.text-right {
    text-align: right;
}

.text-center {
    text-align: center;
}

.nowrap {
    white-space: nowrap;
}

.truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------------------------------------------------------------------------------------------
   5. CARD
   --------------------------------------------------------------------------------------------- */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-1);
}

.card__header {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
}

.card__title {
    font-size: var(--text-title);
    font-weight: var(--weight-medium);
    margin: 0;
}

.card__body {
    padding: var(--space-4);
}

.card__footer {
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border);
    background: var(--surface-sunken);
    border-radius: 0 0 var(--radius-md) var(--radius-md);
}

/* A card that wraps a table has no body padding: the table draws its own. */
.card--flush .card__body {
    padding: 0;
}

/* ---------------------------------------------------------------------------------------------
   6. PAGE HEADER
   --------------------------------------------------------------------------------------------- */

/* ONE HEAD GEOMETRY, ON EVERY ROUTE, IN EVERY STATE. The title sits at the same y on the dashboard,
   on a list, on a form and on a record; the line under it sits at the same y; the first content
   block starts at the same y. Navigating stops moving the page.

   THE TRICK IS THAT THE THREE ROWS ARE ALWAYS THERE. .page-header__text is a grid whose first and
   third tracks are FIXED and whose occupants are explicitly placed, so a page with no breadcrumb
   still reserves the breadcrumb's 22px and a page with no subtitle still reserves the subtitle's
   24px. Before this the head was a plain block: a missing breadcrumb pulled the title up 22px, a
   missing second line pulled the content up ~23px, and because a list page's Meta is null until
   its query returns, the table JUMPED DOWN the moment the data landed. Reserving the tracks fixes
   the across-pages case and the within-page case with one rule.

   align-items is flex-start and not baseline. Baseline alignment tied the title's y to the HEIGHT
   OF THE ACTION BUTTONS - measured, /tvrtke put its h1 at 60.8px with two buttons, 57.7px with one
   small one and 54px with none, so the same screen sat at three different heights depending on
   which buttons the viewer's permissions allowed. The actions are lined up with the title instead
   by the margin-top below, which is a constant. */
.page-header {
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding-block: var(--space-5) var(--space-4);
}

/* The two reserved tracks. Values are the natural heights of what goes in them, so a page that
   fills both is laid out exactly as it was before this rule existed:
     breadcrumb  12px x 1.5 = 18px line + 4px margin-bottom = 22px
     second line 24px, enough for .micro (11 x 1.4 = 15.4px) and .page-header__subtitle (12 x 1.5 = 18px) */
.page-header__text {
    min-width: 0;
    flex: 1 1 320px;
    display: grid;
    grid-template-rows: 1.375rem auto 1.5rem;
}

/* EXPLICIT PLACEMENT IS LOAD-BEARING. Without it auto-placement drops the h1 into row 1 whenever
   there is no breadcrumb, which is the whole defect coming straight back. */
.page-header .breadcrumbs {
    grid-row: 1;
}

/* ONE LINE, ALWAYS. A record title is data - "Hrvatska posta d.d. - Regionalni logisticki centar
   Zagreb Jankomir" wraps to two lines in a narrow column and takes the subtitle and the whole page
   down 30px with it, on that record only. The component puts the full text in a title attribute,
   so nothing is lost: it is on hover, in the browser tab, and in the record below. */
.page-header__title {
    grid-row: 2;
    font-size: var(--text-page);
    font-weight: var(--weight-light);
    letter-spacing: -0.02em;
    line-height: 1.15;
    margin: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Both second-line kinds share row 3 and sit on its floor, so the gap under the title is set by
   the track rather than by each one's own margin - which is what kept Meta and Subtitle 1.4px
   apart before. */
.page-header__subtitle,
.page-header__meta {
    grid-row: 3;
    align-self: end;
    margin: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* AN ABSOLUTE LINE BOX, SHARED BY BOTH KINDS. Bottom-aligning a 12px subtitle at 1.5 against an
       11px .micro at 1.4 lines up their descenders but leaves their TOPS 2.6px apart, so the line
       under the title still moved - by a little - between a list page and a form. 18px is the
       subtitle's natural box; naming it in px makes the 11px label adopt the same one. */
    line-height: 1.125rem;
}

.page-header__subtitle {
    color: var(--text-muted);
    font-size: var(--text-meta);
}

/* Lined up with the title rather than with the top of the block: one breadcrumb track down, then
   half the difference between a 32px button and the 29.9px title line box. A constant, so it
   cannot drift with the button count the way baseline alignment did. */
.page-header__actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    margin-top: calc(1.375rem - 1px);
}

/* .page-header--display AND ITS Display PARAMETER ARE GONE. All the modifier did by the end was
   the taller padding and the baseline alignment, and both were the defect: the padding put a list
   page's title 12px below every other page's, and the baseline tied it to the button row. The
   title was already --text-page at --weight-light in both, so there is nothing typographic left to
   express - there is one page-title role, and it is .page-header__title. */

.breadcrumbs {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-meta);
    color: var(--text-subtle);
    margin-bottom: var(--space-1);
}

.breadcrumbs a {
    color: var(--text-muted);
}

/* ---------------------------------------------------------------------------------------------
   7. TOOLBAR
   --------------------------------------------------------------------------------------------- */

.toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-4);
}

.toolbar--flush {
    border: 0;
    border-radius: 0;
    background: transparent;
    padding-inline: 0;
}

.toolbar__group {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.toolbar__spacer {
    flex: 1 1 auto;
}

.toolbar__divider {
    width: 1px;
    align-self: stretch;
    background: var(--border);
}

/* ---------------------------------------------------------------------------------------------
   8. BUTTONS
   --------------------------------------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 32px;
    padding: 0 var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: var(--text-body);
    font-weight: var(--weight-medium);
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    text-decoration: none;
    transition: background-color var(--transition-fast), border-color var(--transition-fast),
        color var(--transition-fast);
}

.btn:hover {
    background: var(--surface-hover);
    text-decoration: none;
}

.btn:active {
    background: var(--surface-active);
}

.btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}

.btn--primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--text-on-accent);
}

.btn--primary:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: var(--text-on-accent);
}

.btn--primary:active {
    background: var(--accent-active);
    border-color: var(--accent-active);
}

.btn--ghost {
    background: transparent;
    border-color: transparent;
    color: var(--text-muted);
}

.btn--ghost:hover {
    background: var(--surface-active);
    color: var(--text);
}

.btn--danger {
    background: var(--danger);
    border-color: var(--danger);
    color: #ffffff;
}

.btn--danger:hover {
    filter: brightness(0.94);
    background: var(--danger);
    border-color: var(--danger);
    color: #ffffff;
}

/* NEUTRAL AT REST, DANGEROUS ON APPROACH. A destructive action that sits in every row of a table
   cannot be red: fifty red buttons down a list is not a warning, it is wallpaper, and this design
   system's rule is that colour carries meaning. The colour arrives at the moment it means
   something - when the pointer is on the control, or the keyboard has landed on it - which is also
   the last moment it can still change the user's mind.

   It is the row-action delete, and it is deliberately NOT what confirms the deletion: that is a
   full .btn--danger in a dialog, where red is the only thing on the screen wearing it. */
.btn--ghost-danger:hover,
.btn--ghost-danger:focus-visible {
    background: var(--danger-bg);
    border-color: var(--danger-border);
    color: var(--danger-text);
}

.btn--sm {
    min-height: 26px;
    padding: 0 var(--space-2);
    font-size: var(--text-meta);
}

.btn--lg {
    min-height: 40px;
    padding: 0 var(--space-5);
    font-size: var(--text-title);
}

.btn--block {
    width: 100%;
}

.btn--icon {
    padding: 0;
    width: 32px;
    min-height: 32px;
}

.btn--icon.btn--sm {
    width: 26px;
    min-height: 26px;
}

/* A button sitting on the inverted bulk bar. It cannot use --surface or --text: on that bar the
   two are the wrong way round. */
.btn--on-inverse {
    background: transparent;
    border-color: var(--bulk-bar-border);
    color: var(--bulk-bar-fg);
}

.btn--on-inverse:hover {
    background: var(--bulk-bar-hover);
    border-color: var(--bulk-bar-border);
    color: var(--bulk-bar-fg);
}

.btn--on-inverse:active {
    background: var(--bulk-bar-hover);
}

/* ---------------------------------------------------------------------------------------------
   9. FORM CONTROLS
   --------------------------------------------------------------------------------------------- */

.field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-bottom: var(--space-4);
}

.field__label {
    font-size: var(--text-body);
    font-weight: var(--weight-medium);
    color: var(--text);
}

.field__required {
    color: var(--danger);
    margin-inline-start: 2px;
}

.field__hint {
    font-size: var(--text-meta);
    color: var(--text-subtle);
}

.field__error {
    font-size: var(--text-meta);
    color: var(--danger-text);
}

.input,
.select,
.textarea {
    width: 100%;
    min-height: 34px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: var(--text-body);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.textarea {
    min-height: 88px;
    resize: vertical;
    line-height: var(--leading-normal);
}

.input::placeholder,
.textarea::placeholder {
    color: var(--text-subtle);
}

.input:hover,
.select:hover,
.textarea:hover {
    border-color: var(--text-subtle);
}

.input:focus,
.select:focus,
.textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.input:disabled,
.select:disabled,
.textarea:disabled {
    background: var(--surface-sunken);
    color: var(--text-subtle);
    cursor: not-allowed;
}

.select {
    appearance: none;
    padding-inline-end: var(--space-7);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
        linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: right var(--space-4) center, right var(--space-3) center;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

.input--invalid,
.select--invalid,
.textarea--invalid,
.invalid.input,
.invalid.select,
.invalid.textarea {
    border-color: var(--danger);
}

.input--invalid:focus,
.invalid.input:focus {
    box-shadow: 0 0 0 3px var(--danger-bg);
}

.checkbox {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-body);
    cursor: pointer;
}

.checkbox input[type="checkbox"],
.checkbox input[type="radio"] {
    width: 16px;
    height: 16px;
    margin: 0;
    accent-color: var(--accent);
    cursor: pointer;
}

/* Blazor's own validation classes, mapped onto the house look. */
.modified.valid {
    border-color: var(--border-strong);
}

.invalid {
    border-color: var(--danger);
}

.validation-message {
    color: var(--danger-text);
    font-size: var(--text-meta);
}

/* ---------------------------------------------------------------------------------------------
   10. TABLE - dense, sticky header, no zebra, a hover row
   --------------------------------------------------------------------------------------------- */

.table-wrap {
    width: 100%;
    overflow-x: auto;
    border-radius: var(--radius-md);
}

.table {
    /* A page never writes a row height. It sets a density class; the table reads this. */
    --row-h: var(--row-height);

    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: var(--text-body);
}

/* Density. The command bar's toggle swaps one class and nothing else moves. */
.table--compact {
    --row-h: var(--row-height-compact);
}

.table th,
.table td {
    padding: var(--space-2) var(--space-3);
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

/* THE HEADER IS MICRO-TYPE ON THE TABLE'S OWN SURFACE, not a grey band. A filled header strip is
   a second surface tier the eye has to resolve before it reaches the data; a hairline says the
   same thing and keeps the promise the rest of the system makes.

   --table-sticky-top lets a full-bleed table park its header under the sticky top bar instead of
   under the top of the viewport. A table inside a card leaves it at 0. */
.table thead th {
    position: sticky;
    top: var(--table-sticky-top, 0px);
    z-index: 2;
    padding-block: var(--space-2);
    background: var(--surface);
    color: var(--text-muted);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    /* 0.08em, not the micro role's 0.1em: this is .micro--tight's tracking, for the same
       reason - at 0.1em a dense header starts breaking Croatian words apart. */
    letter-spacing: 0.08em;
    white-space: nowrap;
    border-bottom: 1px solid var(--border);
}

/* Zebra striping is banned here on purpose: a hover row plus a hairline rule reads better at this
   density, and stripes fight the stage and ageing colours that carry the real meaning. */
.table tbody tr {
    background: var(--surface);
    transition: background-color var(--transition-fast);
}

.table tbody td {
    /* A minimum, not a fixed height: a name cell that wraps to two lines still grows. */
    height: var(--row-h);
}

.table tbody tr:hover {
    background: var(--surface-hover);
}

.table tbody tr.is-selected {
    background: var(--row-selected-bg);
}

.table tbody tr:last-child td {
    border-bottom: 0;
}

.table--clickable tbody tr {
    cursor: pointer;
}

.table td.numeric,
.table th.numeric {
    text-align: right;
}

.table td.shrink,
.table th.shrink {
    width: 1%;
    white-space: nowrap;
}

/* THE WHOLE HEADER CELL IS THE CONTROL, not a word inside it. A 40px-wide hit target in a 140px
   cell is a miss waiting to happen, and a button that does not fill its cell leaves a dead strip
   that looks like a bug. */
.table__sort {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    width: 100%;
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    font-size: inherit;
    font-weight: inherit;
    color: inherit;
    text-align: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    cursor: pointer;
}

.table__sort:hover {
    color: var(--text);
}

.table th.numeric .table__sort {
    justify-content: flex-end;
}

/* A CARET ONLY WHERE IT MEANS SOMETHING: on the sorted column always, on a hovered or focused
   column as a promise. Twelve carets down a header row is noise that tells you nothing. */
.table__sort-caret {
    flex: none;
    opacity: 0;
    transition: opacity var(--transition-fast), transform var(--transition-fast);
}

.table__sort:hover .table__sort-caret,
.table__sort:focus-visible .table__sort-caret {
    opacity: 0.45;
}

.table__sort.is-sorted {
    color: var(--text);
}

.table__sort.is-sorted .table__sort-caret {
    opacity: 1;
}

.table__sort.is-sorted--desc .table__sort-caret {
    transform: rotate(180deg);
}

/* ---------------------------------------------------------------------------------------------
   11. BADGE, CHIP
   --------------------------------------------------------------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 1px var(--space-2);
    border-radius: var(--radius-full);
    background: var(--surface-sunken);
    color: var(--text-muted);
    font-size: var(--text-meta);
    font-weight: var(--weight-medium);
    line-height: 1.6;
    white-space: nowrap;
}

.badge--square {
    border-radius: var(--radius-xs);
}

/* Status variants */
.badge--neutral {
    background: var(--surface-sunken);
    color: var(--text-muted);
}

.badge--info {
    background: var(--info-bg);
    color: var(--info-text);
}

.badge--success {
    background: var(--success-bg);
    color: var(--success-text);
}

.badge--warning {
    background: var(--warning-bg);
    color: var(--warning-text);
}

.badge--danger {
    background: var(--danger-bg);
    color: var(--danger-text);
}

.badge--accent {
    background: var(--accent-soft);
    color: var(--accent-text);
}

/* Pipeline stage variants. The class name is the stage code with underscores turned into dashes,
   which is exactly what StatusBadge.razor emits. */
.badge--stage-new-inquiry {
    background: var(--stage-new-inquiry-bg);
    color: var(--stage-new-inquiry-fg);
}

.badge--stage-awaiting-client {
    background: var(--stage-awaiting-client-bg);
    color: var(--stage-awaiting-client-fg);
}

.badge--stage-questionnaire-sent {
    background: var(--stage-questionnaire-sent-bg);
    color: var(--stage-questionnaire-sent-fg);
}

.badge--stage-questionnaire-returned {
    background: var(--stage-questionnaire-returned-bg);
    color: var(--stage-questionnaire-returned-fg);
}

.badge--stage-quote-sent {
    background: var(--stage-quote-sent-bg);
    color: var(--stage-quote-sent-fg);
}

.badge--stage-meeting-scheduled {
    background: var(--stage-meeting-scheduled-bg);
    color: var(--stage-meeting-scheduled-fg);
}

.badge--stage-meeting-held {
    background: var(--stage-meeting-held-bg);
    color: var(--stage-meeting-held-fg);
}

.badge--stage-negotiation {
    background: var(--stage-negotiation-bg);
    color: var(--stage-negotiation-fg);
}

.badge--stage-contract-signed {
    background: var(--stage-contract-signed-bg);
    color: var(--stage-contract-signed-fg);
}

.badge--stage-won {
    background: var(--stage-won-bg);
    color: var(--stage-won-fg);
}

.badge--stage-lost {
    background: var(--stage-lost-bg);
    color: var(--stage-lost-fg);
}

.badge--stage-disqualified {
    background: var(--stage-disqualified-bg);
    color: var(--stage-disqualified-fg);
}

.badge__dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background: currentColor;
    flex: none;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-full);
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-body);
    white-space: nowrap;
}

.chip--accent {
    background: var(--accent-soft);
    border-color: var(--accent-soft-border);
    color: var(--accent-text);
}

/* AN APPLIED FILTER, AND ONLY AN APPLIED FILTER. A pill radius here would read as a status badge,
   which is the one thing this is not: square-ish corners say "control", round says "label". */
.chip--filter {
    gap: var(--space-1);
    padding: 3px var(--space-1) 3px var(--space-2);
    border-radius: var(--radius-sm);
    border-color: var(--border);
    background: var(--surface);
    font-size: var(--text-body);
    line-height: 1.3;
}

.chip--filter:hover {
    border-color: var(--border-strong);
}

.chip__label {
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-subtle);
}

.chip__value {
    color: var(--text);
    max-width: 22ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chip__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border: 0;
    border-radius: var(--radius-full);
    background: transparent;
    color: inherit;
    font: inherit;
    line-height: 1;
    cursor: pointer;
    opacity: 0.6;
}

.chip__remove:hover {
    opacity: 1;
    background: var(--surface-active);
}

/* ---------------------------------------------------------------------------------------------
   12. AGEING INDICATOR
   --------------------------------------------------------------------------------------------- */

.ageing {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 1px var(--space-2);
    border-radius: var(--radius-xs);
    font-size: var(--text-meta);
    font-weight: var(--weight-normal);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--ageing-fresh-fg);
    background: var(--ageing-fresh-bg);
}

.ageing--warn {
    color: var(--ageing-warn-fg);
    background: var(--ageing-warn-bg);
}

.ageing--late {
    color: var(--ageing-late-fg);
    background: var(--ageing-late-bg);
    font-weight: var(--weight-medium);
}

/* ---------------------------------------------------------------------------------------------
   13. KPI TILE
   --------------------------------------------------------------------------------------------- */

.kpi-tile {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-1);
}

.kpi-tile__label {
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-subtle);
}

.kpi-tile__value {
    font-size: var(--text-metric);
    font-weight: var(--weight-light);
    line-height: var(--leading-tight);
    letter-spacing: -0.015em;
}

.kpi-tile__meta {
    font-size: var(--text-meta);
    color: var(--text-muted);
}

.kpi-tile__delta--up {
    color: var(--success-text);
}

.kpi-tile__delta--down {
    color: var(--danger-text);
}

/* ---------------------------------------------------------------------------------------------
   14. EMPTY STATE
   --------------------------------------------------------------------------------------------- */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-9) var(--space-5);
    text-align: center;
    color: var(--text-muted);
}

.empty-state__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin-bottom: var(--space-1);
    border-radius: var(--radius-full);
    background: var(--surface-sunken);
    color: var(--text-subtle);
}

.empty-state__title {
    font-size: var(--text-title);
    font-weight: var(--weight-medium);
    color: var(--text);
}

.empty-state__description {
    max-width: 46ch;
    font-size: var(--text-body);
}

.empty-state__actions {
    margin-top: var(--space-3);
    display: flex;
    gap: var(--space-2);
}

/* An empty state standing in for a table, inside the table's own hairline-bounded band. Left
   aligned, because it replaces rows that were left aligned and a centred block in a full-bleed
   band reads as a different page. */
.empty-state--table {
    align-items: flex-start;
    text-align: left;
    padding: var(--space-9) var(--space-5);
}

.empty-state--table .empty-state__description {
    max-width: 52ch;
}

/* ---------------------------------------------------------------------------------------------
   15. ALERT - a notice set in type, not a coloured box

   THERE IS NO BOX, AND THAT IS THE POINT. A tinted panel with a matching tinted border and a
   coloured left rail is the default shape of a message in every generated admin template on the
   internet, and it costs this product twice over: it spends colour on chrome, which the pipeline
   stages and the ageing thresholds need for meaning, and it lifts the message off the page as
   though it had arrived from a different application. A message belongs to the thing it is about.
   So it is set as text, on the page it interrupts, under a rule.

   WHAT CARRIES THE SEVERITY, in the order the eye takes it:

     1. A RULE ABOVE THE MESSAGE, in the severity's own ink - not the pastel border, the ink.
        2px when something is wrong, 1px when it is not. A failure ALSO closes with a hairline
        underneath, so it is the one notice in the product fenced on both sides: the move a
        printed statement makes on the line it wants read twice. Rule weight is what keeps
        warning and danger apart once the hue is gone.
     2. A WORD, in the left column - NAPOMENA / GOTOVO / UPOZORENJE / GRESKA - set in the micro
        role the KPI tiles and the table column headers already use: 11px, 600, uppercase, 0.1em,
        in the severity ink. The word is what makes the four variants legible in a greyscale
        screenshot, to a red-green colourblind reader, and to anyone who never learned an icon
        vocabulary (WCAG 1.4.1). Nothing new is introduced to carry it.
     3. THE SENTENCE, in --text at body size. It is the most important text on the screen at the
        moment it appears, so it is set in the most legible ink the product has, not in a weaker
        tinted one. Hue here is redundancy, never the carrier.

   SIDE EFFECTS OF OWNING NO SURFACE, all of them wanted:
     - It renders identically on --surface, --surface-page, --surface-sunken and --surface-raised,
       so an alert inside a card, a drawer, a modal or a kanban column needs no special case.
     - Windows High Contrast keeps every cue. Forced colours discard author backgrounds and
       override border COLOUR, but not border WIDTH and not generated content, so the 2px/1px
       distinction and the word both survive with no @media block to maintain.
     - It prints. There is no background for a printer to drop.

   NO --*-bg TOKEN IS USED HERE ANY MORE. The four pastel fills stay in the token block for the
   badges and the ageing indicator, which do need a fill to read at 12px inside a table row.

   A VARIANT SETS FOUR VALUES and never a second block of layout: --alert-word, --alert-ink,
   --alert-rule, --alert-rule-width. A fifth severity, if one is ever justified, is four lines.

   THE FOUR WORDS LIVE IN CSS rather than in the markup, which is the one compromise here: the
   alternative is the same word typed into 117 elements across 61 files. They are Croatian because
   the product is (App.razor sets lang="hr"), and they are escaped because this sheet is ASCII and
   is served with no @charset - \000161 is s-with-caron. A second locale moves these four strings
   into the markup and the rest of this section does not change.
   --------------------------------------------------------------------------------------------- */

.alert {
    --alert-word: "Napomena";
    --alert-ink: var(--text-muted);
    --alert-rule: var(--border-strong);
    --alert-rule-width: 1px;

    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1) var(--space-4);

    margin: 0 0 var(--space-4);
    padding-block: var(--space-3);
    border: 0;
    border-block-start: var(--alert-rule-width) solid var(--alert-rule);
    border-radius: 0;
    background: none;

    color: var(--text);
    font-size: var(--text-body);
    line-height: var(--leading-normal);
}

/* THE LABEL COLUMN. 5.5rem is "UPOZORENJE" - the longest of the four - at the micro role with its
   0.1em tracking, rounded up to the next half-rem, so all four words occupy one column and two
   stacked messages start their sentences on the same vertical. flex: 0 0 auto lets a longer word
   push the column rather than clip, which is what happens when a reader enlarges text.

   The word is generated content, so a screen reader reading the page announces it as part of the
   message. That is wanted - it puts back the severity that StatusMessage strips out of the string
   when it removes the "Greska:" prefix. It is NOT a replacement for role="alert": generated
   content is not reliably carried into a live-region announcement, so the markup keeps its role
   and this is the visual channel plus a bonus. */
.alert::before {
    content: var(--alert-word);
    flex: 0 0 auto;
    min-width: 5.5rem;
    color: var(--alert-ink);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* 16rem of flex-basis is the width below which the word stops being a column and becomes a kicker
   above the sentence, because the flex line wraps by itself. On the 420px sign-in card and on a
   phone that is exactly what should happen, and it costs no media query.

   --prose-max is the same exemption the house rules already grant .prose and .form-column: a
   failure message set as one 1700px line across the content column is not a sentence, it is a
   ticker. */
.alert__body {
    flex: 1 1 16rem;
    min-width: 0;
    max-width: var(--prose-max);
}

.alert__title {
    font-weight: var(--weight-medium);
    margin-bottom: 2px;
}

.alert__list {
    margin: var(--space-1) 0 0;
    padding-inline-start: var(--space-4);
}

/* The discs become en dashes, which is what a list of things that went wrong looks like when a
   person sets it rather than a browser. Styling ::marker rather than switching list-style off
   keeps the element a LIST for VoiceOver, which drops list semantics the moment list-style is
   none; a browser that ignores content on ::marker falls back to a disc and loses nothing. */
.alert__list li::marker {
    content: "\2013\00a0";
}

.alert__list li + li {
    margin-top: 2px;
}

/* THE DISMISS BUTTON. Eight alerts carry one - the "Zatvori" on the pipeline action messages, on
   StageChangeAlert, and on the reset-password confirmation in Users. It sits at the end of the
   first line and stays at the end when the line wraps. It deliberately does NOT get pushed to the
   far edge with an auto margin: --prose-max already stops the sentence at 72ch, and a dismiss
   button stranded 1400px away from the message it dismisses belongs to nothing. */
.alert > .btn {
    flex: 0 0 auto;
    align-self: flex-start;
}

/* Anything else a page puts directly inside an alert is body content, not a second label. */
.alert > *:not(.alert__body):not(.btn) {
    flex: 1 1 16rem;
    min-width: 0;
}

.alert--info {
    --alert-word: "Napomena";
    --alert-ink: var(--info-text);
    --alert-rule: var(--info);
}

.alert--success {
    --alert-word: "Gotovo";
    --alert-ink: var(--success-text);
    --alert-rule: var(--success);
}

.alert--warning {
    --alert-word: "Upozorenje";
    --alert-ink: var(--warning-text);
    --alert-rule: var(--warning);
    --alert-rule-width: 2px;
}

.alert--danger {
    --alert-word: "Gre\000161ka";
    --alert-ink: var(--danger-text);
    --alert-rule: var(--danger);
    --alert-rule-width: 2px;

    /* The closing hairline is the whole difference between a refused sign-in and a hint, and it
       is spent HERE and nowhere else in the product. */
    border-block-end: 1px solid var(--alert-rule);
}

/* A link inside a message keeps the message's ink and is told apart by its underline, not by a
   second colour - the account link StageChangeAlert offers when an OIB is missing, above all. */
.alert a {
    color: inherit;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
}

/* .stack already spaces its children by --space-4, so an alert inside one was paying the gap
   twice and rendering 32px of air beneath itself. Eleven screens do this, the dashboard among
   them. */
.stack > .alert {
    margin-bottom: 0;
}

/* ---------------------------------------------------------------------------------------------
   16. TABS
   --------------------------------------------------------------------------------------------- */

.tabs {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-4);
    overflow-x: auto;
    scrollbar-width: thin;
}

.tabs__tab {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 0;
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-size: var(--text-body);
    font-weight: var(--weight-medium);
    white-space: nowrap;
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}

.tabs__tab:hover {
    color: var(--text);
    text-decoration: none;
}

.tabs__tab.is-active {
    color: var(--accent-text);
    border-bottom-color: var(--accent);
}

.tabs__count {
    font-size: var(--text-meta);
    color: var(--text-subtle);
    background: var(--surface-sunken);
    border-radius: var(--radius-full);
    padding: 0 var(--space-2);
}

/* SAVED VIEWS. The row that turns a list into a workspace. Quieter than a content tab strip -
   these switch the question being asked, not the panel being shown - so the type is smaller and
   the inactive tabs sit further back. The active one is ink with a 2px ink underline. */
/* NO RULE UNDER THIS ROW, and no negative margin on the tabs. Two reasons, one visual and one
   mechanical. Visually, the command bar directly beneath already draws a hairline, and two rules
   forty pixels apart is chrome arguing with itself; the active tab's own 2px underline is enough
   of an anchor. Mechanically, .tabs__tab pulls itself 1px past the strip with margin-bottom:-1px
   so that its border can straddle the strip's rule - with no rule to straddle, that 1px is pure
   overflow, and .tabs is a scroll container, so it produced a 1px vertical scrollbar at the end of
   the row. Dropping both the rule and the margin removes the artefact and the extra line at once. */
.tabs--views {
    gap: var(--space-4);
    margin-bottom: 0;
    border-bottom: 0;
}

.tabs--views .tabs__tab {
    padding: var(--space-2) 0 var(--space-3);
    margin-bottom: 0;
    font-size: var(--text-body);
    font-weight: var(--weight-normal);
    color: var(--text-subtle);
}

.tabs--views .tabs__tab:hover {
    color: var(--text);
}

.tabs--views .tabs__tab.is-active {
    color: var(--text);
    font-weight: var(--weight-medium);
    border-bottom-color: var(--text);
}

/* ---------------------------------------------------------------------------------------------
   17. PAGINATION
   --------------------------------------------------------------------------------------------- */

.pagination {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    padding: var(--space-3);
    font-size: var(--text-body);
    color: var(--text-muted);
}

.pagination__summary {
    margin-inline-end: auto;
}

/* A footer that sits on the page rather than in a card foot: no padding of its own, count in
   micro-type, controls quiet. */
.pagination--flush {
    width: 100%;
    padding: 0;
    gap: var(--space-4);
}

.pagination--flush .pagination__summary {
    font-size: var(--text-meta);
    color: var(--text-muted);
}

.pagination--flush .pagination__size {
    font-size: var(--text-meta);
    color: var(--text-subtle);
}

.pagination--flush .pagination__size .select {
    border-color: transparent;
    background-color: transparent;
    color: var(--text-muted);
}

.pagination--flush .pagination__size .select:hover {
    border-color: var(--border);
    background-color: var(--surface);
}

.pagination__pages {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

.pagination__page {
    min-width: 28px;
    height: 28px;
    padding: 0 var(--space-2);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: var(--text-body);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}

.pagination__page:hover:not(:disabled) {
    background: var(--surface-active);
    color: var(--text);
}

.pagination__page.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--text-on-accent);
    font-weight: var(--weight-medium);
}

.pagination__page:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.pagination__ellipsis {
    padding: 0 var(--space-1);
    color: var(--text-subtle);
}

.pagination__size {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

.pagination__size .select {
    width: auto;
    min-height: 28px;
    padding-block: 0;
    font-size: var(--text-meta);
}

/* ---------------------------------------------------------------------------------------------
   18. SPINNER
   --------------------------------------------------------------------------------------------- */

.spinner {
    display: inline-block;
    width: 18px;
    height: 18px;
    border: 2px solid var(--border-strong);
    border-top-color: var(--accent);
    border-radius: var(--radius-full);
    animation: crm-spin 700ms linear infinite;
    flex: none;
}

.spinner--sm {
    width: 13px;
    height: 13px;
    border-width: 2px;
}

.spinner--lg {
    width: 28px;
    height: 28px;
    border-width: 3px;
}

.spinner-block {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-8);
    color: var(--text-muted);
    font-size: var(--text-body);
}

@keyframes crm-spin {
    to {
        transform: rotate(360deg);
    }
}

@media (prefers-reduced-motion: reduce) {
    .spinner {
        animation-duration: 2s;
    }

    *,
    *::before,
    *::after {
        transition-duration: 1ms !important;
    }
}

/* ---------------------------------------------------------------------------------------------
   19. KANBAN
   --------------------------------------------------------------------------------------------- */

.kanban {
    display: flex;
    gap: var(--space-3);
    align-items: flex-start;
    overflow-x: auto;
    padding-bottom: var(--space-3);
    scrollbar-width: thin;
}

.kanban__column {
    flex: 0 0 280px;
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - 220px);
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.kanban__column-header {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3);
    border-bottom: 1px solid var(--border);
    background: var(--surface-sunken);
    border-radius: var(--radius-md) var(--radius-md) 0 0;
}

.kanban__column-title {
    font-size: var(--text-body);
    font-weight: var(--weight-medium);
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.kanban__column-count {
    font-size: var(--text-meta);
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    background: var(--surface);
    border-radius: var(--radius-full);
    padding: 0 var(--space-2);
}

.kanban__column-total {
    padding: var(--space-1) var(--space-3) var(--space-2);
    font-size: var(--text-meta);
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    background: var(--surface-sunken);
}

.kanban__cards {
    flex: 1 1 auto;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-2);
    scrollbar-width: thin;
}

.kanban__card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-1);
    cursor: pointer;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.kanban__card:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow-2);
}

.kanban__card.is-dragging {
    opacity: 0.55;
}

.kanban__card-title {
    font-size: var(--text-body);
    font-weight: var(--weight-medium);
    line-height: var(--leading-tight);
}

.kanban__card-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    font-size: var(--text-meta);
    color: var(--text-muted);
}

.kanban__column.is-drop-target {
    outline: 2px dashed var(--accent);
    outline-offset: -2px;
}

/* ---------------------------------------------------------------------------------------------
   20.1 THE BRAND MARK

   THE MARK IS THE 2FORGE LOGOTYPE, and the component that draws it is
   TwoForge.Crm.Web.Client/Components/Shared/BrandMark.razor - paths, not a file, and one copy of
   them. What used to be here was ".brand-mark": the letters "2F" in Geist 600 inside a rounded
   square, painted from a --brand-mark-bg/--brand-mark-fg pair. That chip existed because there was
   no logo file to draw. There is one now, it is on 2forge.io, and the product wears it.

   WHAT THE CHIP TAUGHT, KEPT. Its own comment recorded that it had once been defined in three
   scoped stylesheets, each asserting it matched the other two, and that they had drifted - one had
   a raw 6px radius, one painted itself from --accent and so turned BLUE in dark mode while the
   others stayed white. The logotype cannot repeat that: it is a component, its geometry is the
   site's untouched, and its only colours are currentColor and one token.

   THE ONLY RULES IT NEEDS ARE HERE. Height comes from the caller through
   --brand-logotype-height; width follows the artwork's own 320:59 ratio, because a wordmark's
   width is a consequence of its letters and not something a lockup should pick.
   --------------------------------------------------------------------------------------------- */

.brand-logotype {
    display: block;
    flex: none;
    height: var(--brand-logotype-height, 20px);
    width: auto;
    /* A wordmark this wide can be the widest thing in a narrow bar. It never grows past the room
       its lockup gives it, and the aspect ratio is preserved by preserveAspectRatio on the svg. */
    max-width: 100%;
}

/* FORCED COLOURS. Windows High Contrast replaces every colour in the page, which would repaint the
   wordmark in the system's text colour - correct, and leave the green swoosh repainted with it,
   which is the one part of the mark that carries meaning. Opting the mark out keeps it drawn as
   drawn; it is an image, and the system does not repaint those either. */
@media (forced-colors: active) {
    .brand-logotype {
        forced-color-adjust: none;
    }
}

/* PRINT. Browsers drop backgrounds when printing but they do not drop fills, so the wordmark
   prints in whatever colour it inherited - which on a printed page is the page's ink. The swoosh
   is a fill too and survives on its own. Nothing to correct here, unlike the old chip, whose white
   glyphs on a dropped background left an invisible 24-28px hole. */

/* ---------------------------------------------------------------------------------------------
   20. MISCELLANY THE PAGES WILL WANT
   --------------------------------------------------------------------------------------------- */

.definition-list {
    display: grid;
    grid-template-columns: minmax(120px, max-content) 1fr;
    gap: var(--space-2) var(--space-4);
    margin: 0;
    font-size: var(--text-body);
}

.definition-list dt {
    color: var(--text-muted);
}

.definition-list dd {
    margin: 0;
}

.avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex: none;
    border-radius: var(--radius-full);
    background: var(--accent-soft);
    color: var(--accent-text);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
}

.money--negative {
    color: var(--danger-text);
}

.money__unit {
    color: var(--text-muted);
    font-size: var(--text-meta);
}

/* Blazor's unhandled-error bar, restyled so it belongs to this product. It keeps a surface, unlike
   .alert, because it is an overlay pinned over live content and has to be opaque to be read at
   all - but it takes the same language as
   section 15 otherwise: a 2px rule in the severity's own ink, the sentence in --text, and no
   tint. Left as a pastel amber band it would have been the last tinted message in the product,
   and the one thing on screen still looking like the template it came from. */
#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1000;
    padding: var(--space-3) var(--space-5);
    background: var(--surface);
    color: var(--text);
    border-top: 2px solid var(--danger);
    box-shadow: var(--shadow-3);
    font-size: var(--text-body);
}

#blazor-error-ui .dismiss {
    position: absolute;
    right: var(--space-4);
    top: var(--space-3);
    cursor: pointer;
}

/* ---------------------------------------------------------------------------------------------
   21. ACCOUNT PAGES

   The /racun screens share LoginLayout, which supplies the centred card. These are the bits inside
   it, kept here rather than in a scoped stylesheet so every account page reads the same.
   --------------------------------------------------------------------------------------------- */

.login-title {
    font-size: var(--text-page);
    font-weight: var(--weight-light);
    letter-spacing: -0.02em;
    line-height: 1.15;
    margin: 0 0 var(--space-2);
}

.login-lead {
    margin: 0 0 var(--space-5);
    color: var(--text-muted);
}

.login-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-5);
    font-size: var(--text-body);
}

.login-note {
    margin: var(--space-5) 0 0;
    font-size: var(--text-meta);
    color: var(--text-muted);
    text-align: center;
}

/* Money and dates never break across a line: half an amount on each line reads as a different
   number, and "12." at the end of a line reads as an ordinal. */
.money,
.relative-date {
    white-space: nowrap;
}

/* ---------------------------------------------------------------------------------------------
   22. THE LIST WORKBENCH

   Five screens in this product are a list of records with filters over it, and they must be the
   same screen five times. Everything below is the vocabulary those five share. A page composes
   these classes; it does not invent a variant, and it never writes a colour, a height or a radius
   of its own.

   THE IDEA IN ONE LINE: a list page is a TOOL, not a report. So the chrome is three thin bands -
   head, saved views, command bar - stacked on hairlines with no fills and no boxes, and then the
   table is the surface. Affordances appear when they are relevant and take no space when they are
   not: an unset filter is not a dropdown, it is nothing at all.

   WHAT IS DELIBERATELY ABSENT, because each of them is a tell that nobody chose anything:
     - a card wrapped round the table;
     - drop shadows used as the depth cue (hairlines do it; shadows are for things that float);
     - zebra striping (a hairline and a hover row read better and do not fight stage colour);
     - uniform 8px corners on everything;
     - a row of always-visible empty selects;
     - a centred spinner where a skeleton belongs.
   --------------------------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------- 23.1 type scale */

/* THE MICRO-LABEL. 11px, 600, uppercase, wide-tracked. It is the counterweight to the 300-weight
   display title: the WEIGHT CONTRAST between the two is the whole editorial idea, and it is why
   neither is allowed to drift toward the other. Use it for a record count, a section label, a
   column heading - anything that names data rather than being data. */
.micro {
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    line-height: 1.4;
    color: var(--text-muted);
}

/* 0.08em, for a micro-label inside a dense table header where 0.1em starts to break words up. */
.micro--tight {
    letter-spacing: 0.08em;
}

.micro--strong {
    color: var(--text);
}

/* The display title on its own, for a head that is not built from .page-header. */
.display-title {
    margin: 0;
    font-size: var(--text-page);
    font-weight: var(--weight-light);
    letter-spacing: -0.02em;
    line-height: 1.15;
    color: var(--text);
}

/* --------------------------------------------------------------------------- 23.2 command bar */

/* ONE ROW, A HAIRLINE BENEATH IT, NO BOX. It holds a search field, the filters that are actually
   applied, the control that adds one, and the view controls. NOTHING EMPTY IS EVER SHOWN: an
   unset filter occupies no pixels, which is the difference between a command bar and a form. */
.command-bar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    min-height: 48px;
    padding-block: var(--space-2);
    border-bottom: 1px solid var(--border);
}

.command-bar__search {
    position: relative;
    display: flex;
    align-items: center;
    flex: none;
}

.command-bar__search-icon {
    position: absolute;
    left: 0;
    display: block;
    color: var(--text-subtle);
    pointer-events: none;
}

/* Borderless, and it EXPANDS ON FOCUS. A search field that is already 320px wide while empty is
   just a box; one that grows when you commit to it is an affordance appearing when it is
   relevant. The transition is on width only, so nothing else on the row moves. */
.command-bar__search-input {
    width: 200px;
    max-width: 100%;
    height: 30px;
    padding: 0 var(--space-2) 0 var(--space-6);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--text-body);
    transition: width var(--transition-base), background-color var(--transition-fast);
}

.command-bar__search-input::placeholder {
    color: var(--text-subtle);
}

.command-bar__search-input:hover {
    background: var(--surface-hover);
}

.command-bar__search-input:focus {
    outline: none;
    width: 320px;
    background: var(--surface);
}

.command-bar__search-input:focus-visible {
    box-shadow: var(--focus-ring);
}

/* Chrome and Edge paint their own clear button inside a type=search field. It is a second, differently
   styled affordance sitting in a control that has deliberately been stripped of chrome. */
.command-bar__search-input::-webkit-search-cancel-button {
    appearance: none;
}

.command-bar__filters {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    min-width: 0;
}

/* A FIELD DROPPED STRAIGHT INTO THE BAR SIZES TO ITS CONTENT, NOT TO THE COLUMN. app.css section
   10 gives .input/.select/.textarea width: 100% because in a form that is what they should do - but
   a 100%-wide flex item in a wrap container takes a LINE OF ITS OWN, so the administration bars,
   which put plain selects in the row rather than a filter menu, stacked one control per line and
   stood 180-250px tall against the 48px the CRM lists measure. The bar is not a form; a control in
   it is as wide as it needs to be. */
/* Descendant, not child: AuditTrail wraps each date field in a <label class="row"> so the control
   is a grandchild of the bar. .command-bar__search-input is untouched - it does not carry .input. */
.command-bar .input,
.command-bar .select {
    width: auto;

    /* AND THE SAME HEIGHT AS A BUTTON. A form field is 36.8px tall (34px min-height plus 8px of
       padding-block), a .btn is 32px and the search field is 30px, so a bar with selects in it
       stood 4-7px taller than one without and the table under it sat that much lower. 32px is the
       button's height, which is the tallest thing the CRM bars already put in the row, so every
       bar in the product now measures the same 48px. */
    min-height: 0;
    height: 32px;
}

.command-bar__end {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-inline-start: auto;
}

.command-bar__divider {
    width: 1px;
    height: 20px;
    flex: none;
    background: var(--border);
}

@media (max-width: 599.98px) {
    .command-bar__search-input,
    .command-bar__search-input:focus {
        width: 100%;
    }

    .command-bar__search {
        flex: 1 1 100%;
    }

    .command-bar__end {
        margin-inline-start: 0;
    }
}

/* --------------------------------------------------------------------------- 23.3 menu */

/* A SMALL ANCHORED POPOVER, script-free. Used by the "+ Filtar" control and by anything else that
   has to offer a short list of choices without spending a row on them.

   The backdrop is a transparent fixed button rather than a document click handler: it closes the
   menu on an outside click with no JavaScript module, no interop call and no listener to leak. */
.menu {
    position: relative;
    display: inline-flex;
    flex: none;
}

.menu__panel {
    position: absolute;
    top: calc(100% + var(--space-2));
    left: 0;
    z-index: 50;
    min-width: 224px;
    max-height: 340px;
    overflow-y: auto;
    padding: var(--space-1);
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-3);
    scrollbar-width: thin;
}

/* For a menu near the trailing edge, so the panel does not run off the content column. */
.menu__panel--end {
    left: auto;
    right: 0;
}

.menu__heading {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-2) var(--space-1);
}

.menu__item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding: var(--space-2);
    border: 0;
    border-radius: var(--radius-xs);
    background: none;
    color: var(--text);
    font: inherit;
    font-size: var(--text-body);
    text-align: left;
    cursor: pointer;
}

.menu__item:hover {
    background: var(--surface-hover);
    text-decoration: none;
}

.menu__item.is-active {
    background: var(--surface-active);
    font-weight: var(--weight-medium);
}

.menu__item-hint {
    margin-inline-start: auto;
    color: var(--text-subtle);
    font-size: var(--text-meta);
}

.menu__separator {
    height: 1px;
    margin: var(--space-1) 0;
    border: 0;
    background: var(--border);
}

.menu__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2) var(--space-2);
}

.menu__empty {
    padding: var(--space-2);
    font-size: var(--text-body);
    color: var(--text-subtle);
}

.menu__backdrop {
    position: fixed;
    inset: 0;
    z-index: 49;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: default;
}

/* --------------------------------------------------------------------------- 23.4 segmented */

/* Two or three mutually exclusive settings that are cheap enough to show at all times. The
   density toggle is the canonical use. */
.segmented {
    display: inline-flex;
    flex: none;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}

.segmented__option {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    min-width: 30px;
    height: 26px;
    padding: 0 var(--space-2);
    border: 0;
    background: transparent;
    color: var(--text-subtle);
    font: inherit;
    font-size: var(--text-body);
    cursor: pointer;
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.segmented__option + .segmented__option {
    border-inline-start: 1px solid var(--border);
}

.segmented__option:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.segmented__option.is-active {
    background: var(--surface-active);
    color: var(--text);
}

/* --------------------------------------------------------------------------- 23.5 table surface */

/* THE TABLE IS THE SURFACE - AND SECTION 27 OVERRIDES THAT. It was a white band bounded top and
   bottom by a hairline, full bleed, on the argument that a card around a table adds a second
   border, a radius that has to be clipped and a shadow that says "this floats". Alen asked for the
   border anyway, so §27 gives the surface all four sides and answers each of those three objections
   there rather than accepting them along with the card. What is left here is the part that did not
   change: the width, the background, and the scroller.

   THE HORIZONTAL SCROLLER IS CONDITIONAL, and that is not a detail. overflow-x on a container
   makes it a scroll container in BOTH axes, which silently kills position:sticky on the header -
   the header sticks to the top of a box that has no height limit, i.e. never. Above 1100px there
   is room for the columns, so there is no scroller and the sticky header works against the
   viewport, parked under the shell's top bar. Below it, the scroller comes back and the sticky
   offset returns to zero because the header is now sticky inside the scroller. */
.table-surface {
    --table-sticky-top: var(--topbar-height);

    width: 100%;
    background: var(--surface);
}

@media (max-width: 1099.98px) {
    .table-surface {
        --table-sticky-top: 0px;

        overflow-x: auto;
        scrollbar-width: thin;
    }
}

/* The leading selection column. 36px: 16px of gutter, a 16px control, and 4px before the
   monogram column starts. */
.table th.table__select,
.table td.table__select {
    width: 36px;
    padding-inline: var(--space-4) 0;
}

/* --------------------------------------------------------------------------- 23.6 check */

/* A DRAWN CHECKBOX, not the operating system's. Three reasons, in order of weight: the native
   control cannot be given an indeterminate state from markup (it is a property, not an
   attribute), so a header checkbox over a partial selection would need a JavaScript module to
   exist at all; the native control looks like a different product in every browser; and role=
   checkbox with aria-checked carries "mixed", which is exactly the state we need to express. */
.check {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    flex: none;
    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: 3px;
    background: var(--surface);
    color: transparent;
    cursor: pointer;
    transition: background-color var(--transition-fast), border-color var(--transition-fast);
}

.check:hover {
    border-color: var(--text-subtle);
}

.check[aria-checked="true"],
.check[aria-checked="mixed"] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--text-on-accent);
}

.check svg {
    display: block;
}

/* --------------------------------------------------------------------------- 23.7 monogram */

/* THE SINGLE HIGHEST-VALUE MARK ON THE PAGE. Twenty-five rows of plain text have no anchor: the
   eye has nothing to count down and nothing to come back to after looking away. A 22px monogram
   gives every row a fixed point at a fixed x, and it costs no colour.

   ONE FLAT NEUTRAL TINT FOR EVERY ROW. A hue derived from the name would be twenty-five different
   colours meaning nothing, competing with the twelve stage colours that mean something. The
   6px radius on a 22px square is deliberately not the system's 8px: it reads as a squircle, which
   is neither the circular avatar in the top bar nor another rounded rectangle. */
.monogram {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    flex: none;
    border-radius: 6px;
    background: var(--monogram-bg);
    color: var(--monogram-fg);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.02em;
    line-height: 1;
    text-transform: uppercase;
    user-select: none;
}

.monogram--sm {
    width: 18px;
    height: 18px;
    border-radius: 5px;
    font-size: var(--text-micro);
    letter-spacing: 0;
}

.monogram--lg {
    width: 32px;
    height: 32px;
    border-radius: 8px;
    font-size: var(--text-meta);
}

/* The name cell: monogram, then the name, then whatever the name is not. */
.cell-name {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
}

.cell-name__text {
    min-width: 0;
}

.cell-name__primary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    font-size: var(--text-body);
    font-weight: var(--weight-medium);
    line-height: 1.35;
}

.cell-name__link {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
}

.cell-name__link:hover {
    text-decoration: underline;
}

.cell-name__secondary {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-meta);
    line-height: 1.35;
    color: var(--text-subtle);
}

/* --------------------------------------------------------------------------- 23.8 values */

/* Figures, identifiers and codes. Medium weight, tabular, so a twelve-digit OIB does not
   out-measure the column heading above it.

   A PRESENT VALUE IS NOT A SIGNAL. An OIB that exists is data; rendering it as a grey chip says
   "look at me" about the most ordinary fact on the row. Tabular and muted is enough: it aligns,
   it reads as an identifier rather than a word, and it disappears until you need it.

   THE NAME IS NOW A LIE and that is deliberate for one wave only. There is no monospace family:
   .mono is Geist with the globally-on tabular figures doing the aligning that a monospace used
   to do. The class keeps its name so the markup does not have to change in the same wave as the
   typeface; rename it to .ident when the sweep is finished. */
.mono {
    font-family: var(--font-sans);
    font-size: var(--text-meta);
    font-weight: var(--weight-medium);
    /* The -0.01em that used to be here was tightening IBM Plex Mono, which ran wide. Geist
       is not wide, and an identifier is the last string you want compressed. */
    letter-spacing: normal;
}

.mono--muted {
    color: var(--text-muted);
}

/* THE ABSENCE OF A VALUE. A single en dash, recessive, at 12px - not an em dash at body weight,
   which reads as content. Always rendered aria-hidden: an empty cell should be empty to a screen
   reader, not announced as a punctuation mark, which is also why this colour is exempt from the
   contrast floor - there is no text here to read. */
.cell-empty {
    font-size: var(--text-meta);
    color: var(--text-empty);
}

/* THE SUPPORTING COLUMNS, IN A QUIETER INK. A table where every column is the same black reads as a
   wall: the eye has nothing to anchor on and has to read left to right, cell by cell, to find the
   row it wants. One column carries the identity - the name, already set in --weight-medium by
   .cell-name__primary - and everything else is context for it. Dropping the context to --text-muted
   is what turns a grid of text into a list of records, and it is the difference between the CRM
   lists in this product, which do it, and the administration lists, which did not.

   It is NOT for a value somebody scans down a column to compare, such as money or a count. Those
   stay in full ink; .tabular and .numeric are their job. */
.cell-quiet {
    color: var(--text-muted);
}

/* --------------------------------------------------------------------------- 23.9 dot label */

/* A 5px DOT AND A PLAIN LABEL, in place of a filled pill. A soft-beige badge round a word like
   "Klijent" is low contrast, muddy, and spends a filled shape on a fact that is neither a status
   nor a warning. The dot is the only colour on the row and the label carries the meaning, so the
   mark still works for a colour-blind reader and in a monochrome print.

   The variants are semantic, not domain names, so the other four list screens reuse them:
   a lead status, an activity outcome and an account type all resolve to one of these six. */
.dot-label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-body);
    color: var(--text);
    white-space: nowrap;
}

.dot-label__dot {
    width: 5px;
    height: 5px;
    flex: none;
    border-radius: var(--radius-full);
    background: var(--dot-neutral);
}

.dot-label--positive .dot-label__dot {
    background: var(--dot-positive);
}

.dot-label--info .dot-label__dot {
    background: var(--dot-info);
}

.dot-label--attention .dot-label__dot {
    background: var(--dot-attention);
}

.dot-label--critical .dot-label__dot {
    background: var(--dot-critical);
}

.dot-label--inactive .dot-label__dot {
    background: var(--dot-inactive);
}

.dot-label--inactive {
    color: var(--text-muted);
}

/* --------------------------------------------------------------------------- 23.10 row actions */

/* QUICK ACTIONS THAT FADE IN AT THE RIGHT EDGE. They are always in the DOM at opacity 0, so the
   row never changes width and the columns never jump - a cluster that is inserted on hover
   reflows the table under the cursor, which is the single most irritating thing a dense list can
   do.

   pointer-events is switched with the opacity, because an invisible button that still swallows
   clicks is worse than no button. :focus-within brings the cluster back for a keyboard user the
   moment the row's link takes focus, so it is reachable on the way through. */
.table td.row-actions,
.table th.row-actions {
    width: 1%;
    padding-inline-end: var(--space-3);
    text-align: right;
    white-space: nowrap;
}

.row-actions__cluster {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition-fast);
}

.table tbody tr:hover .row-actions__cluster,
.table tbody tr:focus-within .row-actions__cluster,
.table tbody tr.is-selected .row-actions__cluster,
/* AND WHILE A MENU INSIDE IT IS OPEN. The menu panel is in the top layer, so it is not a descendant
   of this row as far as :hover is concerned - move the pointer off the row to reach the menu and the
   cluster underneath fades out, taking the trigger with it. :focus-within does not cover it either,
   because a menu opened by POINTER deliberately leaves focus on the trigger. */
.row-actions__cluster:has(.row-menu__panel:popover-open) {
    opacity: 1;
    pointer-events: auto;
}

/* On a touch screen there is no hover, so a hover-only affordance does not exist at all. */
@media (hover: none) {
    .row-actions__cluster {
        opacity: 1;
        pointer-events: auto;
    }
}

/* --------------------------------------------------------------------------- 23.11 footer, bulk bar */

/* One band under the table. It holds the pagination, or - while a selection exists - the bulk
   bar. The min-height is what keeps the swap from moving the table by a pixel. */
.table-footer {
    display: flex;
    align-items: center;
    min-height: 52px;
    padding-block: var(--space-2);
}

/* THE ONE INVERTED SURFACE IN THE PRODUCT, and it earns the inversion by being temporary: it
   exists only while rows are selected, so it reads as a mode rather than as decoration. */
.bulk-bar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    width: 100%;
    min-height: 36px;
    padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--bulk-bar-bg);
    color: var(--bulk-bar-fg);
}

.bulk-bar__count {
    font-size: var(--text-body);
    font-weight: var(--weight-medium);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.bulk-bar__divider {
    width: 1px;
    height: 18px;
    flex: none;
    background: var(--bulk-bar-border);
}

.bulk-bar__actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    margin-inline-start: auto;
}

/* The global focus ring is drawn as two rings, the inner one in --surface so it separates from
   whatever is behind it. On an inverted bar that inner ring is the wrong colour, so it is
   restated here. Every control on this bar still shows a visible ring. */
.bulk-bar :focus-visible {
    box-shadow: 0 0 0 2px var(--bulk-bar-bg), 0 0 0 4px var(--bulk-bar-fg);
}

/* --------------------------------------------------------------------------- 23.12 skeleton */

/* A SKELETON OF ROWS, NOT A SPINNER. A centred spinner on a list page makes the whole screen feel
   like it restarted: the chrome is still there, the data is not, and a spinner in the middle of
   the empty space says "everything is gone" rather than "the rows are coming". A skeleton keeps
   the shape of the answer on screen, so the eye stays where it was.

   The widths are a fixed repeating pattern rather than random: a skeleton that reshuffles on
   every render draws attention to itself, and a random one cannot be reasoned about. */
.skeleton {
    display: block;
    height: 9px;
    border-radius: 2px;
    background-color: var(--skeleton-bg);
    background-image: linear-gradient(90deg, transparent, var(--skeleton-sheen), transparent);
    background-size: 180px 100%;
    background-repeat: no-repeat;
    animation: crm-sheen 1.2s ease-in-out infinite;
}

.skeleton--monogram {
    width: 22px;
    height: 22px;
    flex: none;
    border-radius: 6px;
}

.skeleton--check {
    width: 16px;
    height: 16px;
    flex: none;
    border-radius: 3px;
}

.skeleton--w-xs {
    width: 28%;
}

.skeleton--w-sm {
    width: 42%;
}

.skeleton--w-md {
    width: 58%;
}

.skeleton--w-lg {
    width: 74%;
}

.skeleton--w-full {
    width: 100%;
}

.skeleton-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

@keyframes crm-sheen {
    from {
        background-position: -180px 0;
    }

    to {
        background-position: calc(100% + 180px) 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    .skeleton {
        animation: none;
        background-image: none;
    }
}

/* ---------------------------------------------------------------------------------------------
   23. THE BRAND LOCKUP

   §20.1 is the product's OWN mark, the 2Forge logotype, drawn from paths in a component. THIS
   section is what happens when a customer supplies artwork of their own: on every surface the tenant
   has uploaded a file for, that file replaces the logotype - in the command bar, on the sign-in
   screen and in the branding settings preview.

   PER SURFACE, because the fallback runs ONE WAY. A dark mark is usually white, and white artwork on
   this product's paper-coloured command bar is an empty space where a customer's name should be, so
   a dark file is never borrowed for a light surface; the built-in 2Forge logotype stands in there
   instead. A light file IS borrowed for the dark surface, which is why a single upload is the common
   and correct case. That is what the .brand-logotype--light / .brand-logotype--dark pair below is
   for: the lockup renders what each theme should draw, artwork or logotype, and CSS picks between
   the two. See Components/Layout/BrandLockup.razor and BrandIdentity for the C# half.

   ONE RULE GOVERNS THE DRAWING: the logo occupies a fixed BAND, set per instance - 28px in the
   command bar, 36px on the sign-in screen, 40px in the settings preview's detail view - and whatever
   width its own aspect ratio asks for, up to a ceiling. A logo is somebody else's file: square, wide,
   short, a wordmark, a monogram. The one dimension a shell can promise is its height; constraining
   the width instead, or forcing a box, is how a customer's mark ends up letterboxed or stretched.

   It lives in app.css rather than in a scoped stylesheet because TWO assemblies draw it, and the
   second may not reference the first: the shell and the sign-in screen are in TwoForge.Crm.Web, and
   the branding settings preview is in TwoForge.Crm.Web.Client, which an architecture test forbids
   from referencing anything of ours but Contracts and the typed client. A second copy of these rules
   is what §20.1's own comment exists to warn about.
   --------------------------------------------------------------------------------------------- */

.brand-lockup {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    color: var(--text);
    text-decoration: none;
}

.brand-lockup:hover {
    text-decoration: none;
}

.brand-logo {
    display: block;
    flex: none;
    height: var(--brand-logo-height, 28px);
    width: auto;
    max-width: var(--brand-logo-max-width, 168px);
    /* contain, never cover: cover crops, and a cropped logo is a wrong logo rather than a small
       one. The object-position keeps a wide mark's left edge on the lockup's left edge, so the
       optical margin does not change when the file does. */
    object-fit: contain;
    object-position: left center;
}

/* THE WORDMARK THE PRODUCT FALLS BACK TO, beside the §20.1 chip: "2Forge CRM" with the product
   name carrying the weight.

   NOTHING HERE HIDES IT, and that is deliberate rather than an omission. Whether it is set at all is
   a question about the tenant's data - has EITHER variant been uploaded - which CSS cannot ask;
   BrandLockup decides it in C# and simply does not render the element. A rule here would be a second
   opinion on the same question, and the two would drift. */
.brand-lockup__text {
    font-size: var(--brand-lockup-text-size, var(--text-title));
    font-weight: var(--weight-normal);
    letter-spacing: -0.01em;
    color: var(--text-muted);
    white-space: nowrap;
}

.brand-lockup__text strong {
    font-weight: var(--weight-medium);
    color: var(--text);
}

/* LIGHT AND DARK ARE TWO SURFACES, AND ONLY ONE IS EVER SHOWN.
   The lockup renders what each theme should draw - a file where one was uploaded, the §20.1 type
   mark where none was - and CSS picks between them. The alternative, <picture> with a
   prefers-color-scheme <source>, follows the OPERATING SYSTEM and cannot be overridden, so it would
   show the wrong mark for anyone who chose a theme in the product against their OS. The guards
   mirror the token blocks in §2 exactly: an explicit light choice wins over the OS, and an explicit
   dark choice wins over everything.

   Both families are display: block, so one pair of rules covers the uploaded image and the built-in
   logotype alike. */
.brand-logo--dark,
.brand-logotype--dark {
    display: none;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .brand-logo--light,
    :root:not([data-theme="light"]) .brand-logotype--light {
        display: none;
    }

    :root:not([data-theme="light"]) .brand-logo--dark,
    :root:not([data-theme="light"]) .brand-logotype--dark {
        display: block;
    }
}

:root[data-theme="dark"] .brand-logo--light,
:root[data-theme="dark"] .brand-logotype--light {
    display: none;
}

:root[data-theme="dark"] .brand-logo--dark,
:root[data-theme="dark"] .brand-logotype--dark {
    display: block;
}

/* Windows High Contrast replaces every colour in the page but leaves images alone, so a logo drawn
   for a white header stays drawn for a white header on a black one. Nothing can be done about the
   artwork; what CAN be done is to stop it sitting on a surface that is no longer there. */
@media (forced-colors: active) {
    .brand-logo {
        forced-color-adjust: none;
    }
}

/* --------------------------------------------------------------------------- 24.1 keyboard hint */

/* The shortcut a control answers to, set in the control. It is a hint and never a target: pointer
   events pass through it to the input underneath. */
.kbd {
    display: inline-flex;
    align-items: center;
    height: 20px;
    padding: 0 var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-xs);
    background: var(--surface);
    color: var(--text-subtle);
    font-family: inherit;
    font-size: var(--text-micro);
    font-weight: var(--weight-medium);
    letter-spacing: 0.04em;
    white-space: nowrap;
}

/* --------------------------------------------------------------------------- 24.2 environment */

/* WHICH COPY OF THE PRODUCT THIS IS. Rendered only outside production, so the chip's presence is
   the signal and its absence is the normal state - nobody has to read it to know they are live.

   It is deliberately NOT coloured. A red DEV badge would be the loudest thing on the screen and
   would spend the one signal this product reserves for a status that means something. An outline
   and a micro-label is enough: the eye finds it once and then stops seeing it, which is correct
   for something that is true all day. */
.env-chip {
    display: inline-flex;
    align-items: center;
    height: 20px;
    flex: none;
    padding: 0 var(--space-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-full);
    color: var(--text-muted);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* =============================================================================================
   24. THE MODAL
   =============================================================================================

   Every create, every edit and every confirmation in the product happens in one of these.

   THE SELECTOR IS dialog.modal, NOT .modal, AND IT STAYS THAT WAY. There used to be a section 19
   here holding a hand-rolled overlay - a .scrim div, a .modal div and a .drawer - that the activity
   editor and four administration screens were built on. Anchoring this system to the ELEMENT is
   what let the two stand side by side while those screens were converted one at a time, with no
   !important and no ordering trick. They are all converted and section 19 is gone, but the element
   selector is kept: it says out loud that these rules describe a real <dialog> and not a div
   wearing role="dialog", and it is the reason the guard below can be written at all.

   WHAT THE BROWSER IS DOING THAT THIS STYLESHEET IS NOT. The panel is in the top layer because
   showModal() put it there, so there is no z-index in this section at all - it cannot lose to the
   command bar, to a sticky table header or to anything added later, because the top layer is above
   every stacking context on the page by definition. Focus containment and the inert background
   come from the same call. See Modal.razor.js for the four things the platform leaves undone. */

/* THE DIALOG ELEMENT IS THE WHOLE VIEWPORT; the panel inside it is the paper. No border and no
   background here, so everything this element covers outside the panel reads as backdrop - which
   is exactly what the pointer test in Modal.razor.js treats it as. It asks whether the click
   landed outside the PANEL's rectangle, so the padding below is backdrop too, and dismissing from
   it is correct rather than a coincidence.

   THE display RULE IS ON [open] AND MUST STAY THERE. The user-agent sheet hides a dialog with
   `dialog:not([open]) { display: none }`, and a bare `dialog.modal { display: flex }` outranks it:
   every closed dialog in the page would then paint over the screen as a panel with no backdrop.
   Blazor renders the element one frame before showModal() is called, so this is not hypothetical -
   it is a flash of unbackdropped dialog on every single open. */
dialog.modal {
    padding: var(--space-6);
    border: 0;
    background: transparent;
    max-width: none;
    max-height: none;
    width: 100%;
    height: 100%;
    inset: 0;
    overflow: visible;

    /* Explicit for the same reason as the display guard above: the deleted overlay set
       `pointer-events: none` on .modal and undid it on a .modal__dialog class this system does not
       use, so the whole panel was click-through. Stating it here means a future rule on .modal
       cannot quietly do that again. */
    pointer-events: auto;
}

/* KEPT AS A TRIPWIRE. The user agent hides a closed dialog by itself, so with the old section 19
   deleted this rule currently changes nothing. It is here because the bug it fixes is invisible
   until it is catastrophic: ANY author rule that sets a display on .modal outranks the UA sheet
   whatever its specificity, and every closed dialog in the page then paints over the screen as a
   panel with no backdrop. Measured in the harness when section 19 still existed: a closed
   confirmation covering the viewport at 1274px tall. Two lines to make that unreachable. */
dialog.modal:not([open]) {
    display: none;
}

/* FLEX, NOT GRID, AND THE REASON IS MEASURED. With `display: grid; place-items: center` the single
   auto-sized column takes its width from the item's max-content contribution, and the panel's
   `width: 100%` is cyclic against it, so the column collapses to the natural width of the longest
   line in the form: a --lg dialog measured 460px instead of 720px, and the size modifiers did
   nothing at all. A flex container resolves the item's percentage against its own content box. */
dialog.modal[open] {
    display: flex;
    align-items: center;
    justify-content: center;
}

dialog.modal::backdrop {
    background: rgba(17, 22, 29, 0.42);

    /* Enough to separate the panel from a dense table without turning the page behind it into a
       smear. The user is meant to keep their place, not lose it. */
    backdrop-filter: blur(2px);
}

.modal__panel {
    display: flex;
    flex-direction: column;
    width: 100%;

    /* 100% is now the dialog's PADDED box, so the panel cannot touch the edge of the screen and the
       900px cap is what stops a long form from becoming a full-height wall on a tall monitor. */
    max-height: min(100%, 900px);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);

    /* --shadow-3 is the product's "floating above everything" shadow, already used by the drawer
       and the command palette. A dialog is the same distance off the page. */
    box-shadow: var(--shadow-3);
    overflow: hidden;
}

.modal__panel:focus {
    outline: none;
}

dialog.modal--sm .modal__panel {
    max-width: 420px;
}

dialog.modal--md .modal__panel {
    max-width: 560px;
}

dialog.modal--lg .modal__panel {
    max-width: 720px;
}

dialog.modal--xl .modal__panel {
    max-width: 960px;
}

/* ---------------------------------------------------------------------------------- the header */

.modal__header {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    flex: none;
    padding: var(--space-5) var(--space-5) var(--space-4);
}

.modal__heading {
    flex: 1 1 auto;
    min-width: 0;
}

.modal__eyebrow {
    margin: 0 0 var(--space-1);
    font-size: var(--text-micro);
    font-weight: var(--weight-medium);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* --text-head, not --text-title. A dialog covers the screen the user was reading, so its heading is
   answering "what am I looking at now" from scratch rather than labelling a card in a page that
   already has a title above it. */
.modal__title {
    margin: 0;
    font-size: var(--text-head);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: -0.01em;
    color: var(--text);
}

.modal__subtitle {
    margin: var(--space-1) 0 0;
    font-size: var(--text-body);
    color: var(--text-muted);

    /* The subtitle is the record's name and names run long. One line, elided, because a dialog
       header that grows to three lines pushes the first field below the fold. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.modal__header-aside {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex: none;
}

/* Pulled up and out by a quarter of its own box so the glyph optically aligns with the title's cap
   height and sits the same distance from the corner as the padding, rather than a button's worth
   further in. */
.modal__close {
    flex: none;
    margin: -2px -4px 0 0;
}

/* ------------------------------------------------------------------------------------ the body */

.modal__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0 var(--space-5) var(--space-5);

    /* Momentum scrolling inside the panel on a touch device, and no scroll chaining out to the page
       behind when the body hits its end. */
    overscroll-behavior: contain;
}

/* THE TWO HAIRLINES ARE DRAWN ONLY WHEN THEY MEAN SOMETHING. Modal.razor.js adds these classes when
   there is content hidden above or below, so a short form is one uninterrupted sheet and a long one
   tells you, at a glance, that it continues. A permanent rule under the header says "there is more"
   even when there is not, which is how a two-field dialog ends up looking like a wizard. */
.modal__body--above {
    border-top: 1px solid var(--border);
}

.modal__body--below {
    border-bottom: 1px solid var(--border);
}

/* The banner sits above the fields with the same gap the .stack would have given it. */
.modal__error {
    margin-bottom: var(--space-4);
}

/* ---------------------------------------------------------------------------------- the footer */

.modal__footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
    flex: none;
    padding: var(--space-4) var(--space-5);
    background: var(--surface-sunken);
    border-top: 1px solid var(--border);
}

/* A footer band is a row of buttons pushed right. Anything that has to sit at the LEFT end - a
   delete on an edit dialog, a "what happens next" note - carries this and pushes the rest away. */
.modal__footer-start {
    margin-right: auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.modal__footer-note {
    font-size: var(--text-meta);
    color: var(--text-muted);
}

/* ----------------------------------------------------------------------------------- the entry */

/* 140ms and 6px. The panel is ALREADY the thing the user asked for, so the animation's job is to
   say where it came from, not to make them wait for it; anything longer than about 150ms on a
   dialog that opens dozens of times an afternoon becomes the reason people say software feels
   slow. The backdrop fades on its own curve because a blur that scales looks like a camera move. */
@keyframes modal-panel-in {
    from {
        opacity: 0;
        transform: translateY(6px) scale(0.99);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

@keyframes modal-backdrop-in {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

dialog.modal[open] .modal__panel {
    animation: modal-panel-in 140ms cubic-bezier(0.2, 0, 0.13, 1);
}

dialog.modal[open]::backdrop {
    animation: modal-backdrop-in 140ms linear;
}

/* --------------------------------------------------------------------------- the document lock */

/* Set by Modal.razor.js for as long as ANY dialog is open. The padding replaces the scrollbar that
   hiding the overflow took away, so the page visible around the panel does not jump sideways at the
   moment the dialog appears - which reads as the whole application flinching. */
html.modal-open {
    overflow: hidden;
    padding-right: var(--modal-scrollbar-width, 0);
}

/* ----------------------------------------------------------------------------------- the phone */

/* FULL SCREEN, NOT A SMALLER CARD. Below this width a centred panel with a margin is mostly
   backdrop, the keyboard covers the half of it that matters, and the first field ends up under the
   thumb rest. Filling the screen gives the form the room it needs and puts the footer where a phone
   user already expects the primary action to be. */
@media (max-width: 599.98px) {
    dialog.modal {
        padding: 0;
    }

    dialog.modal[open] {
        align-items: stretch;
        justify-content: stretch;
    }

    .modal__panel,
    dialog.modal--sm .modal__panel,
    dialog.modal--md .modal__panel,
    dialog.modal--lg .modal__panel,
    dialog.modal--xl .modal__panel {
        max-width: none;
        max-height: none;
        height: 100%;
        border: 0;
        border-radius: 0;
    }

    .modal__header {
        padding: var(--space-4) var(--space-4) var(--space-3);
    }

    .modal__body {
        padding: 0 var(--space-4) var(--space-4);
    }

    .modal__footer {
        padding: var(--space-3) var(--space-4);

        /* The two buttons share the width rather than huddling on the right, because on a phone the
           right edge is the far corner from the thumb that is holding it. */
        padding-bottom: max(var(--space-3), env(safe-area-inset-bottom));
    }

    .modal__footer .btn {
        flex: 1 1 0;
        justify-content: center;
    }

    .modal__footer-start {
        flex: 0 0 auto;
    }

    dialog.modal[open] .modal__panel {
        animation-name: modal-panel-sheet-in;
    }

    @keyframes modal-panel-sheet-in {
        from {
            opacity: 0;
            transform: translateY(12px);
        }

        to {
            opacity: 1;
            transform: none;
        }
    }
}

/* ------------------------------------------------------------------------------------- reduced */

@media (prefers-reduced-motion: reduce) {
    dialog.modal[open] .modal__panel,
    dialog.modal[open]::backdrop {
        animation: none;
    }

    .modal__body {
        scroll-behavior: auto;
    }
}

/* --------------------------------------------------------------------------------------- dark */

/* Everything else in this section is written in tokens and follows the theme without being told.
   The backdrop cannot be: it is a wash over the page, not a surface, and the same 42% of near-black
   that separates a white panel from a white page is invisible over a dark one. It goes darker and
   more opaque, which is the opposite of what a naive inversion would do.

   TWO BLOCKS, KEPT IN SYNC, for the reason stated at the top of this stylesheet: the media query
   follows the operating system and the attribute follows the switch in the command bar. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) dialog.modal::backdrop {
        background: rgba(0, 0, 0, 0.62);
    }
}

:root[data-theme="dark"] dialog.modal::backdrop {
    background: rgba(0, 0, 0, 0.62);
}

/* ------------------------------------------------------------------------------ the confirmation */

/* ConfirmDialog's prose. It is the one dialog body that is a sentence rather than a form, and it
   reads at --prose-max nowhere near its limit, so the only thing it needs is for its last line not
   to sit against the footer: the body's own bottom padding does that, and the margin here only has
   to go. */
.confirm__message {
    margin: 0;
    color: var(--text);
}

.confirm__message + * {
    margin-top: var(--space-3);
}

/* ------------------------------------------------------------------ a form wearing dialog chrome */

/* What .card is on a page, this is in a dialog: the grouping of a handful of fields under a name.
   It is FLAT - no border, no shadow, no filled header band - because the panel around it is already
   a raised surface and a raised thing inside a raised thing is where enterprise software starts
   looking like a filing cabinet. A rule and a small label is the whole device, and the first
   section does without even that, since the dialog's own title already named what this is. */
.form-section + .form-section {
    margin-top: var(--space-5);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

.form-section__title {
    margin: 0 0 var(--space-4);
    font-size: var(--text-meta);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* The form itself inside a dialog carries no width of its own. .form-column's 640px is a PAGE
   measure - it exists because a form alone on an 1800px screen would otherwise stretch to the
   window - and in here the panel has already decided how wide the reading line is. */
.modal-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    max-width: none;
}

/* Two fields on one line where the pair is really one fact: a postcode and a town, a value and its
   currency, a date and a time. It collapses on a phone, where there is no room for the argument. */
.form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-4);
}

@media (max-width: 599.98px) {
    .form-row {
        grid-template-columns: 1fr;
    }
}

/* ------------------------------------------------------------ a console wearing dialog chrome */

/* UserConsole is one screen rendered in two places: at its own route, and inside UserModal over the
   users table. The page draws a PageHeader and puts the four account commands on the title's
   baseline; in here there is no title to sit beside, because the dialog has already drawn the name
   and the e-mail in its own header. So the commands become a toolbar with a rule under it, which is
   the same separation the header gives them on the page. */
.user-console__commands {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}

/* The console's first card would otherwise sit hard against the header's rule. */
.modal__body--console {
    padding-top: var(--space-1);
}

/* =============================================================================================
   25. THE ROW MENU
   =============================================================================================

   One quiet glyph per row, and everything you can do to that record behind it.

   IT REPLACED A ROW OF BUTTONS, and the reason is worth keeping. A table is for READING. Every
   button drawn in a row is ink spent on an action nobody has asked for yet, repeated once per row -
   and when one of them is destructive, the loudest thing on a screen full of data is the delete.
   Nine rows of red "Obriši" is not nine affordances; it is a texture the eye has to filter out
   before it can read a single name.

   THE PANEL IS A NATIVE popover, WHICH IS THE ONLY REASON IT IS VISIBLE AT ALL. .table-wrap is
   overflow-x: auto, and an absolutely positioned menu inside a scrolling box is clipped by it - the
   last row's menu would simply not be there. popover="auto" puts the panel in the top layer, above
   every clip and every stacking context, and brings light dismiss, Escape and mutual exclusion with
   any other open menu along with it. Where it goes is decided in RowMenu.razor.js, because CSS
   anchor positioning is not portable yet. */

.row-menu {
    display: inline-flex;
}

/* A MENU WITH NOTHING IN IT IS NOT A MENU. Every item is wrapped in the caller's AuthorizeView, so
   a reader-only account - an Auditor, a Support user on somebody else's records - can gate every
   single one out and be left with a glyph that opens an empty white box. The check is on the items
   rather than on :empty, because the panel is full of whitespace text nodes from the markup and
   :empty would never match. */
.row-menu:not(:has(.row-menu__item)) {
    display: none;
}

/* THE TRIGGER IS ALWAYS IN THE DOM, at opacity 0, and that is the same decision .row-actions made:
   inserting a control on hover reflows the row under the cursor, which is the single most
   irritating thing a dense list can do to somebody trying to click something.

   It stays visible while its own menu is open, or the thing you just clicked disappears from under
   the pointer the moment the panel takes focus. */
.row-menu__trigger {
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition-fast);
}

.table tbody tr:hover .row-menu__trigger,
.table tbody tr:focus-within .row-menu__trigger,
.table tbody tr.is-selected .row-menu__trigger,
.row-menu__trigger:focus-visible,
/* :has(), NOT [aria-expanded]. A button with popovertarget gets an implicit expanded state in the
   ACCESSIBILITY TREE, but no attribute is written to the DOM, so an attribute selector matches
   nothing - the trigger would vanish from under the pointer the moment its own menu took focus. */
.row-menu:has(.row-menu__panel:popover-open) .row-menu__trigger {
    opacity: 1;
    pointer-events: auto;
}

/* On a touch screen there is no hover, so a hover-only affordance does not exist at all. */
@media (hover: none) {
    .row-menu__trigger {
        opacity: 1;
        pointer-events: auto;
    }
}

/* ------------------------------------------------------------------------------------ the panel */

/* The user-agent sheet gives a popover a border, padding and `inset: 0` with auto margins, which
   centres it in the viewport like a dialog. All three have to go before the coordinates the module
   writes mean anything. */
.row-menu__panel {
    position: fixed;
    inset: auto;
    margin: 0;
    padding: var(--space-1);
    min-width: 180px;
    max-width: 280px;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    box-shadow: var(--shadow-3);
    color: var(--text);
    font-size: var(--text-body);
    overflow: visible;
}

.row-menu__panel:popover-open {
    display: flex;
    flex-direction: column;
    gap: 1px;

    /* 90ms, and only downward. A menu is a continuation of the click that opened it, not an event
       of its own; anything slower puts a wait between the intent and the list. */
    animation: row-menu-in 90ms cubic-bezier(0.2, 0, 0.13, 1);
}

@keyframes row-menu-in {
    from {
        opacity: 0;
        transform: translateY(-2px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/* ------------------------------------------------------------------------------------ the items */

.row-menu__item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    min-height: 30px;
    padding: 0 var(--space-2);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text);
    font: inherit;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--transition-fast);
}

.row-menu__item:hover,
.row-menu__item:focus-visible {
    background: var(--surface-hover);
    text-decoration: none;
}

.row-menu__item:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.row-menu__icon {
    display: inline-flex;
    flex: none;
    color: var(--text-muted);
}

.row-menu__text {
    flex: 1 1 auto;
    min-width: 0;
}

/* DESTRUCTIVE ITEMS SIT BEHIND A LINE, at the bottom, and they are the only coloured thing in the
   menu. The rule is not decoration: it is the pause between "the things you meant to do" and "the
   thing you cannot undo", and it is what stops a mis-aimed click one row too low from deleting. */
.row-menu__item:not(.row-menu__item--danger) + .row-menu__item--danger {
    margin-top: var(--space-1);
    padding-top: calc(var(--space-1) + 1px);
    border-top: 1px solid var(--border);
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

.row-menu__item--danger {
    color: var(--danger-text);
}

.row-menu__item--danger:hover,
.row-menu__item--danger:focus-visible {
    background: var(--danger-bg);
    color: var(--danger-text);
}

/* aria-disabled, not [disabled]: a disabled button leaves the tab order, and an item nobody can
   reach is an item nobody can read the reason on. The reason is the whole point of drawing it. */
.row-menu__item--disabled {
    color: var(--text-muted);
    cursor: default;
}

.row-menu__item--disabled:hover {
    background: transparent;
}

@media (prefers-reduced-motion: reduce) {
    .row-menu__panel:popover-open {
        animation: none;
    }

    .row-menu__trigger {
        transition: none;
    }
}

/* =============================================================================================
   27. THE TABLE AS AN OBJECT

   §23.5 said the table IS the surface: a white band bounded top and bottom by a hairline, full
   bleed, no card. Alen looked at it and asked for the opposite - a full border, the table visibly
   INSIDE it, and the whole thing to read as premium - so this section overrides that decision
   rather than pretending it was never made. The argument §23.5 gives is still a real one and it is
   why this is done the way it is below: what makes a bordered table look cheap is a second surface
   tier the eye has to resolve, a radius that fights the content, and a shadow that claims the table
   floats. Each of those is answered here instead of being accepted along with the border.

   IT IS SCOPED TO .table-surface, never to .table. The same table markup is used inside cards, in
   the record tabs and in dialogs, where it is already inside something and a second border would be
   exactly the doubling §23.5 warned about. Only the full-bleed list surface becomes an object.
   ============================================================================================= */

/* --------------------------------------------------------------------------- 27.1 the container */

/* A BORDER ON ALL FOUR SIDES, a large radius, and the lightest shadow in the system. --shadow-1 is
   a 1px ambient shade rather than a drop: it separates the card from the page without the lift that
   would say this floats over the content instead of being it. */
.table-surface {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
}

/* THE RADIUS IS NOT CLIPPED WITH overflow: hidden, and that is deliberate. overflow on this
   container makes it a scroll container in both axes, which silently kills position:sticky on the
   header - the header would stick to the top of a box with no height limit, i.e. never. §23.5
   learned that once already. The corners are rounded on the CELLS instead, which needs four rules
   and breaks nothing. */
.table-surface > .table > thead > tr:first-child > th:first-child {
    border-start-start-radius: var(--radius-lg);
}

.table-surface > .table > thead > tr:first-child > th:last-child {
    border-start-end-radius: var(--radius-lg);
}

.table-surface > .table > tbody > tr:last-child > td:first-child {
    border-end-start-radius: var(--radius-lg);
}

.table-surface > .table > tbody > tr:last-child > td:last-child {
    border-end-end-radius: var(--radius-lg);
}

/* --------------------------------------------------------------------------- 27.2 the head */

/* A TINTED HEAD, WHICH §23.5 REFUSED AND THIS SECTION ALLOWS. The refusal was right for a band
   floating on the page: there, a filled strip is a second surface tier between the reader and the
   data. Inside a border it is the opposite - the head is visibly part of one object, and the tint
   is what tells you where the object's chrome ends and its contents begin. --surface-sunken is one
   step, not a grey band: #f4f4f2 against #ffffff.

   IT MUST STAY OPAQUE. The head is sticky, so rows scroll UNDER it; a translucent tint would show
   them through. */
.table-surface > .table > thead > tr > th {
    background: var(--surface-sunken);
    border-bottom-color: var(--border-strong);
}

/* --------------------------------------------------------------------------- 27.3 the gutters */

/* SIXTEEN PIXELS AT THE EDGES, not twelve. A cell hard against a border reads as a mistake, and the
   difference between a table that is inside something and one that has merely been given an outline
   is almost entirely this. The selection column already sets its own leading gutter. */
.table-surface > .table > thead > tr > th:first-child:not(.table__select),
.table-surface > .table > tbody > tr > td:first-child:not(.table__select) {
    padding-inline-start: var(--space-4);
}

.table-surface > .table > thead > tr > th:last-child,
.table-surface > .table > tbody > tr > td:last-child {
    padding-inline-end: var(--space-4);
}

/* --------------------------------------------------------------------------- 27.4 the footer */

/* THE PAGINATION IS PART OF THE OBJECT. It counts the rows above it and turns their pages; sitting
   it outside the border would leave the card describing a list it does not contain. Both shapes the
   product uses are joined: the CRM lists wrap it in .table-footer, the administration lists drop a
   bare .pagination under the surface.

   :has() is what makes this possible without touching ten pages of markup. Where it is unsupported
   the surface simply keeps its own bottom edge and the pagination sits under it - which is a
   perfectly ordinary layout, not a broken one. */
.table-surface:has(+ .table-footer),
.table-surface:has(+ .pagination) {
    border-end-start-radius: 0;
    border-end-end-radius: 0;
    border-bottom: 0;
    box-shadow: none;
}

.table-surface:has(+ .table-footer) > .table > tbody > tr:last-child > td,
.table-surface:has(+ .pagination) > .table > tbody > tr:last-child > td {
    border-end-start-radius: 0;
    border-end-end-radius: 0;
}

.table-surface + .table-footer,
.table-surface + .pagination {
    padding-inline: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-end-start-radius: var(--radius-lg);
    border-end-end-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
}

/* The hairline between the last row and the footer. It is the footer's, not the last row's, because
   the last row's own border is what gets removed when the table ends without one. */
.table-surface + .table-footer,
.table-surface + .pagination {
    border-top: 1px solid var(--border);
}

/* --------------------------------------------------------------------------- 27.5 the head, stuck */

/* WHAT HAPPENS WHEN THE HEAD STICKS. It leaves the card's rounded corner behind and parks under the
   command bar, where a 1px line on its own would be the only thing separating it from the page. The
   shadow appears only then - :not at the top is not expressible in CSS, so it is drawn always and
   is invisible while the head sits on its own border. */
.table-surface > .table > thead > tr > th {
    box-shadow: 0 1px 0 var(--border);
}

/* --------------------------------------------------------------------------- 27.6 the filters */

/* THE TEXT WAS BEING CUT OFF, and this is the whole of it: §23.2 pins a select in the command bar
   to 32px so every bar in the product measures the same 48px, but it left section 10's
   padding: var(--space-2) var(--space-3) in place. Sixteen pixels of vertical padding inside a
   32px box leaves a 16px content box for a 20px line, and a <select> does not scroll - it clips.
   The height is right and the padding is what has to go: a control with a fixed height centres its
   own single line, which is what every native select does. */
.command-bar .input,
.command-bar .select {
    padding-block: 0;
}
