/* GENERATED by the ConcatCssFiles build task - do not edit. */
/* Source files are listed in EzyPlatform.UI.csproj. */

/* ===== ez-platform-fonts.css ===== */
/* =============================================================
   Self-hosted webfonts: Inter (body) and JetBrains Mono (code).

   GENERATED - do not hand-edit. Regenerate with the recipe in
   wwwroot/lib/fonts/README.md if a weight or subset is added.

   Self-hosted rather than loaded from fonts.googleapis.com so that
   no visitor IP is disclosed to a third party on page load. The
   platform ships a full GDPR/consent module; leaking every visitor
   to Google's CDN before any consent decision contradicts it (and
   has been ruled unlawful in the EU). Self-hosting also removes two
   render-blocking cross-origin round-trips from every page.

   Every @font-face keeps Google's original unicode-range split, so
   a browser downloads ONLY the subsets it actually renders - latin
   for most users, cyrillic/greek/vietnamese only when needed.

   font-display: swap matches the previous &display=swap behaviour:
   text paints immediately in the fallback and swaps when the font
   arrives, so a slow font never blocks content.

   Fonts are licensed under the SIL Open Font License 1.1; the
   licence texts sit beside the files in wwwroot/lib/fonts/.
   ============================================================= */

/* Inter 400 - cyrillic-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/inter-400-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter 400 - cyrillic */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/inter-400-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter 400 - greek-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/inter-400-greek-ext.woff2') format('woff2');
    unicode-range: U+1F00-1FFF;
}
/* Inter 400 - greek */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/inter-400-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* Inter 400 - vietnamese */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/inter-400-vietnamese.woff2') format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* Inter 400 - latin-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/inter-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;
}
/* Inter 400 - latin */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/inter-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;
}
/* Inter 500 - cyrillic-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/inter-500-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter 500 - cyrillic */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/inter-500-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter 500 - greek-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/inter-500-greek-ext.woff2') format('woff2');
    unicode-range: U+1F00-1FFF;
}
/* Inter 500 - greek */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/inter-500-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* Inter 500 - vietnamese */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/inter-500-vietnamese.woff2') format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* Inter 500 - latin-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/inter-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;
}
/* Inter 500 - latin */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/inter-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;
}
/* Inter 600 - cyrillic-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../lib/fonts/inter-600-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter 600 - cyrillic */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../lib/fonts/inter-600-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter 600 - greek-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../lib/fonts/inter-600-greek-ext.woff2') format('woff2');
    unicode-range: U+1F00-1FFF;
}
/* Inter 600 - greek */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../lib/fonts/inter-600-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* Inter 600 - vietnamese */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../lib/fonts/inter-600-vietnamese.woff2') format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* Inter 600 - latin-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../lib/fonts/inter-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;
}
/* Inter 600 - latin */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../lib/fonts/inter-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;
}
/* Inter 700 - cyrillic-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/inter-700-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter 700 - cyrillic */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/inter-700-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter 700 - greek-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/inter-700-greek-ext.woff2') format('woff2');
    unicode-range: U+1F00-1FFF;
}
/* Inter 700 - greek */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/inter-700-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* Inter 700 - vietnamese */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/inter-700-vietnamese.woff2') format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* Inter 700 - latin-ext */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/inter-700-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;
}
/* Inter 700 - latin */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/inter-700-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;
}
/* JetBrains Mono 400 - cyrillic-ext */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-400-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* JetBrains Mono 400 - cyrillic */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-400-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* JetBrains Mono 400 - greek */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-400-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* JetBrains Mono 400 - vietnamese */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-400-vietnamese.woff2') format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* JetBrains Mono 400 - latin-ext */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-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;
}
/* JetBrains Mono 400 - latin */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-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;
}
/* JetBrains Mono 500 - cyrillic-ext */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-500-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* JetBrains Mono 500 - cyrillic */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-500-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* JetBrains Mono 500 - greek */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-500-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* JetBrains Mono 500 - vietnamese */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-500-vietnamese.woff2') format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* JetBrains Mono 500 - latin-ext */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-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;
}
/* JetBrains Mono 500 - latin */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-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;
}
/* JetBrains Mono 700 - cyrillic-ext */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-700-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* JetBrains Mono 700 - cyrillic */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-700-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* JetBrains Mono 700 - greek */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-700-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* JetBrains Mono 700 - vietnamese */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-700-vietnamese.woff2') format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* JetBrains Mono 700 - latin-ext */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-700-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;
}
/* JetBrains Mono 700 - latin */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../lib/fonts/jetbrains-mono-700-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;
}


/* ===== ez-platform-utilities.css ===== */
/* =============================================================
   Utility classes for the handful of patterns Bootstrap 5 has no
   utility for, and which were therefore being written as inline
   style="" attributes across the app.

   Inline styles are banned (dynamic values go out as CSS custom
   properties, state as data attributes), so anything repeated in
   markup needs a name here instead.

   Keep this list SHORT and purpose-named. It is not a substitute
   for Bootstrap utilities - reach for those first.
   ============================================================= */

/* ── Interaction ─────────────────────────────────────────────── */
/* Bootstrap 5.3 ships no cursor utility. Use on non-button elements
   that are click targets (a clickable list row, a <summary>). */
.ez-cursor-pointer {
    cursor: pointer;
}

/* ── Progress bars ───────────────────────────────────────────── */
/* The slim meter used for storage quotas, onboarding progress and
   multi-step form progress. Bootstrap's default .progress is 1rem. */
.ez-progress-thin {
    height: 0.5rem;
}

/* ── Table column sizing ─────────────────────────────────────── */
/* Purpose-named rather than measurement-named, so the intent survives
   a change of value. Apply to the <th>; the <td> inherits the width. */

/* Checkbox / radio selector column. */
.ez-col-select {
    width: 2rem;
}

/* Single-icon or small-thumbnail column. */
.ez-col-icon {
    width: 3rem;
}

/* Free-text cell that must truncate rather than stretch the table.
   Pair with Bootstrap's .text-truncate. */
.ez-col-truncate {
    max-width: 16rem;
}

/* Cell holding a numeric input, kept wide enough that the spinner and
   value stay readable. */
.ez-col-numeric {
    min-width: 7rem;
}


/* ===== ez-platform-forms.css ===== */
/* =============================================================
   ez-platform FORM styling theme (reversible)

   Scope: EVERY rule is gated on <html data-theme-forms="on">, set in App.razor
   from the "ez-forms" cookie (default "on"). Set the cookie to "off" - or flip
   the App.razor default - and the whole file goes inert, falling back to stock
   Bootstrap form styling. Nothing else in the app changes either way.

   Goal: lift form fields well above default Bootstrap - a calmer, brand-tinted
   focus ring instead of the heavy blue glow, roomier padding, a softer radius,
   responsive hover/focus borders, brand-coloured checks/switches, and clearer
   invalid states. All colours come from live --bs-* tokens, so light + dark
   parity is automatic (the brand primary is #af85ff).

   Targets the exact classes the Ez* field components emit (all stock Bootstrap):
   .form-label, .form-control, .form-select, textarea.form-control,
   .form-check / .form-switch / .form-check-input, .form-text, .input-group,
   plus Blazor's .valid/.invalid classes it adds directly to <input>.
   ============================================================= */

[data-theme-forms="on"] {
    /* Brand focus ring - the focused field reads in the brand colour: a solid brand
       border plus the shared 3px brand halo (not Bootstrap's blue glow). Aliases the
       one canonical --ez-focus-ring so there is a single primary-focus definition. */
    --ez-field-ring:        var(--ez-focus-ring);
    --ez-field-ring-danger: 0 0 0 var(--bs-focus-ring-width) color-mix(in srgb, var(--bs-danger)  20%, transparent);
    /* Hover border nudges toward the brand so a field feels responsive on approach. */
    --ez-field-border-hover: var(--border-strong);
    --ez-field-border-focus: var(--bs-primary);
}

/* ── Labels ─────────────────────────────────────────────────── */
/* Heavier, tighter to their field, and a hair smaller so the input reads first. */
[data-theme-forms="on"] .form-label {
    font-weight: 600;
    font-size: 0.8125rem;
    letter-spacing: 0.01em;
    margin-bottom: 0.375rem;
    color: var(--bs-body-color);
}

/* ── Text inputs, selects, textareas ────────────────────────── */
[data-theme-forms="on"] .form-control,
[data-theme-forms="on"] .form-select {
/*     padding: 0.5rem 0.75rem;
    border-radius: var(--radius-sm);
    border-color: var(--bs-border-color);
    background-color: var(--bs-body-bg);
 */    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

/* Hover (not while focused, not disabled): brand-leaning border. */
[data-theme-forms="on"] .form-control:hover:not(:focus):not(:disabled):not([readonly]),
[data-theme-forms="on"] .form-select:hover:not(:focus):not(:disabled) {
    border-color: var(--ez-field-border-hover);
}

/* Focus: brand border + the soft ring (replaces Bootstrap's blue box-shadow). */
[data-theme-forms="on"] .form-control:focus,
[data-theme-forms="on"] .form-select:focus {
    border-color: var(--ez-field-border-focus);
    box-shadow: var(--ez-field-ring);
}

/* Textareas: comfortable height + line-height, vertical resize only. */
[data-theme-forms="on"] textarea.form-control {
    min-height: 6.5rem;
    line-height: 1.6;
    resize: vertical;
}

/* Selects show a pointer to read as a menu, not an editable field. */
[data-theme-forms="on"] .form-select { cursor: pointer; }

/* Disabled / read-only: muted surface + a clear not-allowed cursor. */
[data-theme-forms="on"] .form-control:disabled,
[data-theme-forms="on"] .form-control[readonly],
[data-theme-forms="on"] .form-select:disabled {
    background-color: var(--bs-secondary-bg);
    cursor: not-allowed;
    opacity: 0.85;
}

/* ── Grouped fields (icon + input) ──────────────────────────── */
/* The .form-control/.form-select border-color transition above does not reach
   .input-group-text (it is a sibling, not the same element), so without this its
   border snaps instantly on hover/focus while the input eases in over 0.15s -
   the two halves of the box visibly change at different speeds. */
[data-theme-forms="on"] .input-group-text {
    transition: border-color 0.15s ease-in-out;
}
/* Keep the whole .input-group ringing as one, but in the brand colour so a
   grouped field matches a standalone one. Overrides the blue base-theme ring. */
[data-theme-forms="on"] .input-group:focus-within {
    box-shadow: var(--ez-field-ring);
}
[data-theme-forms="on"] .input-group:focus-within .input-group-text,
[data-theme-forms="on"] .input-group:focus-within .form-control,
[data-theme-forms="on"] .input-group:focus-within .form-select {
    border-color: var(--ez-field-border-focus);
}
/* Hover: same "ring the whole group as one" idea as focus-within above. The lone
   .form-control:hover rule further up only recolors the input half of the box - the
   input-group-text (icon/prefix) sibling keeps its own resting border, so hovering
   the input half visibly splits the box into two different border colours. */
[data-theme-forms="on"] .input-group:hover:not(:focus-within) .input-group-text,
[data-theme-forms="on"] .input-group:hover:not(:focus-within) .form-control:not(:disabled):not([readonly]),
[data-theme-forms="on"] .input-group:hover:not(:focus-within) .form-select:not(:disabled) {
    border-color: var(--ez-field-border-hover);
}

/* EzSearchInput needs no resting-border override any more: it matches the OUTLINE TIER,
   whose border is the --bs-border-color hairline .form-control already carries. What it
   does still need is these hover and focus rules - the input-group-text (icon) half keeps
   its own resting border otherwise, so hovering the input half visibly splits the box into
   two different border colours. */
[data-theme-forms="on"] .ez-search-input:hover .input-group-text,
[data-theme-forms="on"] .ez-search-input:hover .form-control:not(:focus):not(:disabled):not([readonly]) {
    border-color: var(--ez-field-border-hover);
}
/* Same reasoning for focus: scope directly to .ez-search-input rather than relying
   solely on the generic .input-group:focus-within rule above, so the icon's border
   is never left behind when the input gets focus. */
[data-theme-forms="on"] .ez-search-input:focus-within .input-group-text {
    border-color: var(--ez-field-border-focus);
}

/* ── Checkboxes, radios, switches ───────────────────────────── */
/* Brand-colour the checked state everywhere they appear (forms + table selects). */
[data-theme-forms="on"] .form-check-input:checked {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
}
[data-theme-forms="on"] .form-check-input:focus {
    border-color: var(--ez-field-border-focus);
    box-shadow: var(--ez-field-ring);
}
/* Size + affordance bump only for real form checks (leaves table/matrix geometry alone). */
[data-theme-forms="on"] .form-check .form-check-input {
    width: 1.15em;
    height: 1.15em;
    margin-top: 0.15em;
    cursor: pointer;
}
[data-theme-forms="on"] .form-check .form-check-label { cursor: pointer; }
/* Switch: a longer, easier-to-read track. */
[data-theme-forms="on"] .form-switch .form-check-input { width: 2.25em; }

/* ── Hints ──────────────────────────────────────────────────── */
/* Give the muted help text a little breathing room below the field. */
[data-theme-forms="on"] .form-text { margin-top: 0.375rem; }

/* ── Invalid state ──────────────────────────────────────────── */
/* Blazor adds .invalid to the <input>; swap the base-theme outline for a clean
   danger border, and show the danger ring on focus. Success stays quiet. */
[data-theme-forms="on"] .form-control.invalid,
[data-theme-forms="on"] .form-select.invalid {
    outline: none;
    border-color: var(--bs-danger);
}
[data-theme-forms="on"] .form-control.invalid:focus,
[data-theme-forms="on"] .form-select.invalid:focus {
    border-color: var(--bs-danger);
    box-shadow: var(--ez-field-ring-danger);
}
[data-theme-forms="on"] .valid.modified:not([type=checkbox]) { outline: none; }


/* ===== ez-platform-nav.css ===== */
/* =================================================================
   ez-platform-nav.css
   Navigation components: shared nav items, side-menu, user-menu,
   and notification bell. Both NavMenu.razor and MainLayout.razor
   use the same .menu-link class so styles are defined once here.
   ================================================================= */

/* ============ Shared nav item ============ */
/* .menu-link - used in both .side-menu (NavMenu) and .user-menu (MainLayout) */
.menu-link {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.4375rem 0.625rem;
    color: var(--bs-tertiary-color);
    border-radius: var(--radius-sm);
    text-decoration: none;
    transition: background 0.15s, color 0.15s;
}
.menu-link:hover {
    background: rgba(var(--bs-primary-rgb), .08);
    color: var(--bs-primary);
    text-decoration: none;
}
.menu-link.active {
    background: rgba(var(--bs-primary-rgb), .13);
    color: var(--bs-primary);
    font-weight: 600;
}
.menu-link:hover i,
.menu-link.active i { color: var(--bs-primary); }
/* 1.25em icon box (matches FA's fa-fw): wide glyphs (buildings, user-shield) stay inside
   the box instead of eating into the icon/text gap, and all icons align in one column. */
.menu-link i { width: 1.25em; text-align: center; color: var(--bs-secondary-color); transition: color 0.15s; }

/* ============ Mobile nav overlay ============ */
/* Compose with .ez-backdrop in markup; nav-backdrop adds only the hide/show logic. */
.nav-backdrop {
    display: none;
    z-index: 1046;
}
.nav-backdrop.show { display: block; }

/* ============ Toolbar buttons ============ */
/* EzButton-rendered elements require to pierce component scope */
.toolbar-btn { position: relative; display: flex; align-items: center; justify-content: center; width: 2.625rem; height: 2.625rem; padding: 0; }
.toolbar-btn i { font-size: 1.0625rem; }
/* Ghost appearance for toolbar icon buttons (works with both ez-btn and Bootstrap btn classes) */
.toolbar-btn.btn { background: transparent; border-color: var(--bs-border-color); color: var(--bs-body-color); box-shadow: none; }
.toolbar-btn.btn:hover { background: rgba(var(--bs-primary-rgb), .08); border-color: var(--bs-border-color); color: var(--bs-body-color); }
.toolbar-btn.btn:focus { box-shadow: none; }
.toolbar-btn.btn:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; box-shadow: none; }
.toolbar-btn.btn[aria-expanded="true"],
.toolbar-btn.btn[aria-pressed="true"] { background: var(--bs-primary); border-color: var(--bs-primary); color: #fff; }
.toolbar-btn.btn[aria-expanded="true"]:hover,
.toolbar-btn.btn[aria-pressed="true"]:hover { background: var(--bs-primary); border-color: var(--bs-primary); color: #fff; }

/* Animated hamburger (nav toggle): borderless, no padding/background; three thin lines with
   the middle line at 60% width growing to full width on hover (adapted from the reference site).
   Opens the off-canvas side-menu via the delegated #navToggle handler in layout.js. */
.ez-burger {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: stretch;
    /* Matches the marketing site's .vm-burger (source of truth) so the mobile header is identical. */
    width: 30px;
    height: 20px;
    padding: 0;
    margin: 0;
    border: 0;
    background: none;
    cursor: pointer;
}
.ez-burger-line {
    width: 100%;
    height: 2px;
    border-radius: 2px;
    /* Identical to the marketing site's .vm-burger-line: bright ink lines (#fafafa = --bs-body-color
       = the site's --vm-ink) that only widen on hover, with no colour change. */
    background: var(--bs-body-color);
    transition: width .4s cubic-bezier(.4, 0, .2, 1);
    transform-origin: center center;
}
.ez-burger-line:nth-child(2) { width: 60%; align-self: flex-end; }
.ez-burger:hover .ez-burger-line:nth-child(2) { width: 100%; }

/* ============ Notification bell panel ============ */
/* anchor-name feeds the phone-size anchored panel below (inert elsewhere). */
.notif-wrap { position: relative; anchor-name: --notif-bell; }
.notif-menu {
    position: absolute;
    top: calc(100% + 0.375rem);
    right: 0;
    background: var(--bs-body-bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    padding: 0; width: 22.5rem; max-width: 90vw;
    box-shadow: var(--bs-box-shadow);
    overflow: hidden;
    z-index: 1050;
}
.notif-head {
    display: flex; justify-content: space-between; align-items: center;
    padding: 0.875rem 1rem; border-bottom: 1px solid var(--bs-border-color);
}
.notif-title { font-weight: 600; color: var(--bs-body-color); }
.notif-sub { color: var(--bs-secondary-color); margin-top: 0.125rem; }
.notif-mark { font-size: 0.875em; font-weight: 500; padding: 0.25rem 0.5rem; height: auto; }
.notif-list { max-height: 22.5rem; overflow-y: auto; }
.notif-item {
    display: flex; gap: 1rem; padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--bs-border-color);
    position: relative; align-items: center;
}
.notif-item:last-child { border-bottom: 0; }
.notif-item.dismissing {
    overflow: hidden;
    pointer-events: none;
    animation: notif-bell-dismiss 0.35s ease forwards;
}
@keyframes notif-bell-dismiss {
    0%   { opacity: 1; max-height: 6rem; }
    40%  { opacity: 0; max-height: 6rem; padding-top: 0.75rem; padding-bottom: 0.75rem; border-bottom-width: 1px; }
    100% { opacity: 0; max-height: 0;    padding-top: 0;       padding-bottom: 0;       border-bottom-width: 0; }
}
.notif-item-link {
    display: flex; gap: 1rem; flex: 1; min-width: 0;
    text-decoration: none; color: var(--bs-tertiary-color);
}
.notif-item-link:hover { color: var(--bs-body-color); }
.notif-dismiss {
    flex-shrink: 0; width: 1.5rem; height: 1.5rem;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%; border: none; background: transparent;
    color: var(--bs-secondary-color); cursor: pointer;
    transition: background 0.15s, color 0.15s;
}
.notif-dismiss:hover { background: var(--bs-border-color); color: var(--bs-body-color); }
.notif-item.unread::before {
    content: ""; position: absolute; left: 0.375rem; top: 1.25rem;
    width: 0.375rem; height: 0.375rem; border-radius: 50%; background: var(--bs-primary);
}
.notif-ic {
    width: 2rem; height: 2rem; border-radius: 0.5rem;
    display: grid; place-items: center; flex-shrink: 0;
}
.notif-ic.warn   { background: rgba(var(--bs-warning-rgb), .15); color: var(--bs-warning); }
.notif-ic.info   { background: rgba(var(--bs-primary-rgb), .15); color: var(--bs-link-color); }
.notif-body { min-width: 0; flex: 1; }
.notif-text { line-height: 1.35; color: var(--bs-body-color); }
.notif-text b { font-weight: 600; }
.notif-time { color: var(--bs-secondary-color); margin-top: 0.1875rem; }
.notif-foot { padding: 0.625rem 1rem; border-top: 1px solid var(--bs-border-color); text-align: center; }
.notif-foot a { color: var(--bs-link-color); font-weight: 500; text-decoration: none; }
.notif-item--center { justify-content: center; }
.notif-empty { color: var(--bs-secondary-color); justify-content: center; }

/* ============ Notifications page (/account/notifications) ============ */
/* The full-page list. Same visual language as the bell dropdown above - tinted
   icon tile, unread dot, checkmark dismiss - but roomier, because a row here has
   a separate title and body rather than the dropdown's single run of text. */
.notif-page-list {
    border-top: 1px solid var(--bs-border-color);
}
.notif-page-item {
    display: flex; gap: 1rem; padding: 0.875rem 0.25rem 0.875rem 1rem;
    border-bottom: 1px solid var(--bs-border-color);
    position: relative; align-items: flex-start;
}
.notif-page-item.dismissing {
    overflow: hidden;
    pointer-events: none;
    animation: notif-page-dismiss 0.35s ease forwards;
}
@keyframes notif-page-dismiss {
    0%   { opacity: 1; max-height: 12rem; }
    40%  { opacity: 0; max-height: 12rem; padding-top: 0.875rem; padding-bottom: 0.875rem; border-bottom-width: 1px; }
    100% { opacity: 0; max-height: 0;     padding-top: 0;        padding-bottom: 0;        border-bottom-width: 0; }
}
/* Unread marker, mirroring .notif-item.unread in the dropdown. */
.notif-page-item.unread::before {
    content: ""; position: absolute; left: 0.1875rem; top: 1.375rem;
    width: 0.375rem; height: 0.375rem; border-radius: 50%; background: var(--bs-primary);
}
.notif-page-ic {
    width: 2rem; height: 2rem; border-radius: 0.5rem;
    display: grid; place-items: center; flex-shrink: 0;
    background: rgba(var(--bs-primary-rgb), .15); color: var(--bs-link-color);
}
.notif-page-body { min-width: 0; flex: 1; }
.notif-page-title { font-weight: 600; line-height: 1.35; color: var(--bs-body-color); }
.notif-page-text {
    line-height: 1.4; color: var(--bs-secondary-color); margin-top: 0.125rem;
    /* Bodies carry arbitrary template text, so a long unbroken token must not
       push the row wider than the card. */
    overflow-wrap: anywhere;
}
/* Timestamp and call-to-action share one line, and wrap onto separate lines
   rather than colliding when the card is narrow. */
.notif-page-meta {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: 0.25rem 0.75rem; margin-top: 0.375rem;
}
.notif-page-time { color: var(--bs-secondary-color); }
.notif-page-link {
    color: var(--bs-link-color); font-weight: 500; text-decoration: none;
    font-size: 0.875em;
}
.notif-page-link:hover { text-decoration: underline; }
.notif-page-pagination { margin-top: 1rem; }
.notif-empty-alert { margin-bottom: 0; }

/* ============ Notification arrival banner ============ */
/* Fixed card that flips in from the very top when a push arrives.
   Always dark background for max contrast regardless of app theme -
   distinct from the announcement banner which uses the brand primary. */
/* The push-notification banner uses a fixed dark slate/sky palette (like the
   #blazor-error-ui bar) - intentionally theme-independent so the floating banner
   reads identically over light and dark mode. Do not convert to --bs-* tokens. */
.notif-banner {
    position: fixed;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    transform-origin: top center;
    width: 22rem;
    max-width: calc(100vw - 2.5rem);
    background: #0f172a;
    border: 1px solid rgba(56, 189, 248, 0.3);
    border-top: none;
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    box-shadow: 0 14px 44px rgba(0, 0, 0, 0.5), 0 3px 10px rgba(0, 0, 0, 0.35);
    z-index: 1055;
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 0.875rem 1rem;
    animation: notif-banner-in .45s cubic-bezier(0.34, 1.4, 0.64, 1);
    /* Named so the phone toast stack can sit exactly below this banner rather than on top of it -
       both own the top strip on a small screen, and this one cannot move (it is drawn hanging off
       the top edge: square top corners, no top border). See .ez-toast-host in ez-platform.css. */
    anchor-name: --ez-notif-banner;
}
@keyframes notif-banner-in {
    0%   { opacity: 0; transform: translateX(-50%) perspective(600px) rotateX(-50deg) translateY(-2rem); }
    55%  { opacity: 1; transform: translateX(-50%) perspective(600px) rotateX(7deg)   translateY(0.25rem); }
    100% { opacity: 1; transform: translateX(-50%) perspective(600px) rotateX(0deg)   translateY(0); }
}
.notif-banner.hiding {
    animation: notif-banner-out .22s ease-in forwards;
    pointer-events: none;
}
@keyframes notif-banner-out {
    from { opacity: 1; transform: translateX(-50%) translateY(0); }
    to   { opacity: 0; transform: translateX(-50%) translateY(-110%); }
}
.notif-banner-content {
    flex: 1;
    min-width: 0;
    text-decoration: none;
    cursor: pointer;
}
.notif-banner-label {
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #38bdf8;
    margin-bottom: 0.125rem;
}
.notif-banner-title {
    font-weight: 600;
    color: #f8fafc;
    line-height: 1.35;
}
.notif-banner-body {
    color: #94a3b8;
    line-height: 1.4;
    margin-top: 0.125rem;
    /* Clamp to two lines instead of a single nowrap-ellipsis line so short
       notification bodies stay readable on narrow phones. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}

/* ============ User card (header trigger) ============ */
.user-card-wrap { position: relative; }
.user-menu-backdrop { position: fixed; inset: 0; z-index: 1049; }
.user-card {
    background: transparent;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius);
    height: 2.625rem;
    display: flex;
    align-items: center;
    text-align: left;
    transition: background 0.15s, border-color 0.15s;
    max-width: 15rem;
    gap: 0.5rem;
    padding: 0.375rem 0.75rem;
    font-size: inherit;
    font-weight: 500;
    line-height: 1;
}
.user-card-wrap button.user-card:hover {
    background: rgba(var(--bs-primary-rgb), .08);
    border-color: var(--bs-border-color);
    color: var(--bs-body-color);
}
.user-card-wrap button.user-card[aria-expanded="true"] {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: #fff;
}
.user-card-wrap button.user-card[aria-expanded="true"]:hover {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: #fff;
}
.user-card-ava-wrap { position: relative; flex-shrink: 0; display: inline-block; }
.user-card-ava {
    width: 1.7rem;
    height: 1.7rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bs-primary);
    color: #ffffff;
    font-weight: 700;
}
.user-card-text { flex: 1; min-width: 0; }
.user-card-name-row { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.user-card-name { font-weight: 600; line-height: 1.1; min-width: 0; flex: 0 1 auto; }
.user-card-biz { color: var(--bs-secondary-color); margin-top: 0.1875rem; }
.user-card-biz i { color: var(--bs-secondary-color); margin-right: 0.25rem; }
.user-card-chev { color: var(--bs-secondary-color); transition: .2s; flex-shrink: 0; }
.user-card[aria-expanded="true"] .user-card-chev { transform: rotate(180deg); }

/* Admin role badge inside user card */
.user-menu-role {
    font-size: 0.5625rem;
    font-weight: 700;
    letter-spacing: .5px;
    text-transform: uppercase;
    background: rgba(var(--bs-primary-rgb), .18);
    color: var(--bs-link-color);
    padding: 0.125rem 0.375rem;
    border-radius: var(--radius-pill);
    flex-shrink: 0;
    line-height: 1.2;
}

/* ============ User menu panel ============ */
.user-menu {
    position: absolute;
    top: calc(100% + 0.375rem);
    right: 0;
    background: var(--bs-body-bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    padding: 0.375rem;
    width: 15rem;
    max-width: 90vw;
    box-shadow: var(--bs-box-shadow);
    z-index: 1050;
}
.user-menu-logout { color: var(--bs-danger); }
.user-menu-logout i { color: var(--bs-danger); }
.user-menu-logout:hover { background: rgba(var(--bs-danger-rgb), .1); color: var(--bs-danger); }
.user-menu-logout:hover i { color: var(--bs-danger); }
.user-menu .menu-link.active { box-shadow: none; }
.user-menu-divider { border-top: 1px solid var(--bs-border-color); margin: 0.375rem 0.25rem; }

/* Current-business block inside user menu */
.user-menu-biz {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.625rem;
    margin: 0.25rem 0;
    border-radius: var(--radius);
    background: rgba(var(--bs-primary-rgb), .06);
    border: 1px solid rgba(var(--bs-primary-rgb), .15);
}
.user-menu-biz-ic {
    width: 2.125rem; height: 2.125rem; border-radius: 0.5rem;
    background: var(--bs-primary); color: #fff;
    display: grid; place-items: center; flex-shrink: 0;
}
.user-menu-biz-label { font-size: 0.5625rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: var(--bs-secondary-color); margin-bottom: 0.125rem; }
.user-menu-biz-name { font-weight: 600; line-height: 1.1; }
.user-menu-biz-name--empty { opacity: 0.6; }
.user-menu-biz-meta { color: var(--bs-secondary-color); margin-top: 0.125rem; }
.user-menu-switch {
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: 1px solid var(--border-strong);
    color: var(--bs-tertiary-color);
    width: 1.875rem; height: 1.875rem; border-radius: 0.4375rem; text-decoration: none;
    flex-shrink: 0; cursor: pointer; padding: 0;
}
.user-menu-switch:hover { background: var(--bs-primary); color: #fff; border-color: var(--bs-primary); }
.user-menu-switch.is-open { background: var(--bs-primary); color: #fff; border-color: var(--bs-primary); }

/* Business switcher list */
.user-menu-biz-list {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius);
    overflow: hidden;
    margin: 0.25rem 0;
}
.user-menu-biz-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.5rem 0.625rem;
    background: none;
    border: none;
    border-bottom: 1px solid var(--bs-border-color);
    cursor: pointer;
    text-align: left;
}
.user-menu-biz-item:last-child { border-bottom: 0; }
.user-menu-biz-item:hover { background: rgba(var(--bs-primary-rgb), .06); }
.user-menu-biz-item.is-active { background: rgba(var(--bs-primary-rgb), .08); }
.user-menu-biz-item-name { font-weight: 500; color: var(--bs-body-color); }
.user-menu-biz-item-type { color: var(--bs-secondary-color); }
.user-menu-biz-item-check { color: var(--bs-primary); flex-shrink: 0; }

/* Appearance / dark-mode toggle row */
.user-menu-theme-toggle {
    display: inline-flex;
    background: var(--bs-secondary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 0.5rem;
    padding: 0.125rem;
    gap: 0.125rem;
}
.user-menu-theme-option {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 0.375rem;
    background: transparent;
    color: var(--bs-secondary-color);
    pointer-events: none;
}
.user-menu-theme-option.active { background: var(--bs-primary); color: #fff; }

.user-menu button.user-menu-toggle-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.4375rem 0.625rem;
    color: var(--bs-tertiary-color);
    border-radius: var(--radius-sm);
    cursor: pointer;
    width: 100%;
    height: auto;
    text-align: left;
    background: none;
    border: none;
    font-weight: 400;
}
.user-menu button.user-menu-toggle-row:hover { background: rgba(var(--bs-primary-rgb), .08); color: var(--bs-primary); }
.user-menu button.user-menu-toggle-row:hover > i { color: var(--bs-primary); }
.user-menu-toggle-row > i { width: 1.25em; text-align: center; color: var(--bs-secondary-color); }

/* Privacy-mode toggle pill */
/* Same pill-toggle design as .reply-mode-toggle in ez-platform.css - keep the
   two in sync if the toggle look ever changes. */
.user-menu-toggle {
    width: 2.25rem;
    height: 1.25rem;
    border-radius: var(--radius-pill);
    background: var(--border-strong);
    position: relative;
    flex-shrink: 0;
    transition: background 0.2s;
}
.user-menu-toggle::after {
    content: '';
    position: absolute;
    top: 0.1875rem;
    left: 0.1875rem;
    width: 0.875rem;
    height: 0.875rem;
    border-radius: 50%;
    background: #fff;
    transition: transform 0.2s;
    box-shadow: var(--ez-shadow-knob);
}
.user-menu-toggle.is-on { background: var(--bs-primary); }
.user-menu-toggle.is-on::after { transform: translateX(1rem); }

/* Avatar-only user card below md: the name/business text and the chevron are hidden by the
   d-none/d-md-* utilities on the markup; here the button shrinks to just the avatar. */
@media (max-width: 767.98px) {
    .user-card { padding: 0.25rem 0.375rem; height: 2.375rem; max-width: none; }
}

/* Notification / user-menu panels become viewport-pinned sheets on phones - Bootstrap < sm */
@media (max-width: 575.98px) {
    /* The notification panel becomes a viewport-wide sheet: its bell anchor sits inboard of
       the user card, so a right-anchored absolute panel would push its left edge off screen.
       The top pin below is only the fallback for browsers without CSS Anchor Positioning -
       the @supports block underneath drops the sheet just below the bell instead. */
    .notif-menu {
        position: fixed;
        top: 0.75rem;
        left: 0.75rem;
        right: 0.75rem;
        width: auto;
        max-width: none;
        max-height: calc(100vh - 1.5rem);
        overflow-y: auto;
    }
    /* CSS Anchor Positioning (same approach as .ez-action-menu): the fixed-layer sheet hangs
       0.375rem below the bell button, mirroring the user menu's drop below the avatar. The
       anchor() fallback value reproduces the 0.75rem viewport pin if the anchor is missing.
       max-height accounts for the 7.5rem mobile header bar + status-bar inset above the panel. */
    @supports (anchor-name: --test) {
        .notif-menu {
            position-anchor: --notif-bell;
            top: calc(anchor(bottom, 0.375rem) + 0.375rem);
            max-height: calc(100vh - env(safe-area-inset-top, 0px) - 6.5rem);
        }
    }
    /* The user menu keeps the SAME anchored drop as on large screens (the base absolute, right-0
       position under the avatar) - the user card sits at the far right, so the panel never runs
       off the left edge. Only cap its height so a tall menu scrolls within a short phone viewport. */
    .user-menu {
        max-height: calc(100vh - 6rem);
        overflow-y: auto;
    }
}

/* ============ Side menu ============ */
/* Mobile/tablet default (< xl): the side-menu is an off-canvas overlay parked just off the left
   edge; the #navToggle hamburger slides it in via .open. At >=xl it becomes the persistent in-flow
   column (see the min-width block below). Authored mobile-first to match the marketing site. */
.side-menu {
    position: fixed;
    top: 0;
    left: calc(var(--side-w) * -1 - 0.25rem);
    width: var(--side-w);
    height: 100vh;
    z-index: 1047;
    /* Fixed positioning pulls the sidebar out of .layout, so it no longer inherits that wrapper's
       background - give it its own opaque fill (same token), and sit above the announcement banner
       (.annc-group, z-index 1045) so the open menu covers it instead of showing through. */
    background: var(--bs-body-bg);
    box-shadow: var(--bs-box-shadow);
    transition: left .25s ease;
    overflow-y: auto;
    padding: 0.875rem;
    border-right: 1px solid var(--bs-border-color);
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
}
.side-menu.open { left: 0; }
.side-menu.open #navClose { display: inline-flex !important; }
/* Mobile close button - hidden except in the open overlay sidebar (rule above). */
#navClose { display: none; }

/* Sidebar header (shared SidebarMenuHeader component): logo on the left, close button pinned
   to the TOP right. Used by both the app side-menu and the marketing slide-out sidebar. */
.side-menu-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.5625rem 0.375rem;
}
.side-menu-close { flex: none; margin-top: 0.125rem; }

.side-menu-nav { flex: 1; margin-top: 1rem; padding-right: 0.25rem; }

.side-menu-section {
    font-size: 0.625rem;
    color: var(--bs-secondary-color);
    letter-spacing: 1.5px;
    text-transform: uppercase;
    padding: 0.875rem 0.5rem 0.375rem;
}

.side-menu-section-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    background: none;
    border: none;
    cursor: pointer;
    gap: 0.5rem;
}
.side-menu-section-toggle:hover { color: var(--bs-tertiary-color); }

.side-menu-lock { width: auto !important; margin-left: auto; font-size: 0.625rem; opacity: .35; }
.side-menu-divider { border: 0; border-top: 1px solid var(--bs-border-color); margin: 0.625rem 2px; }

.side-menu-pad { height: 3rem; }

/* At >=xl the side-menu is a persistent in-flow column: undo the mobile overlay (static flow, no
   fixed height / shadow / slide transition / own scroll), keeping the shared box metrics above. */
@media (min-width: 1200px) {
    .side-menu {
        position: static;
        height: auto;
        box-shadow: none;
        transition: none;
        overflow-y: visible;
    }
}

/* ============ EzTabs (nav-tabs) - font and icon treatment matching .menu-link ============
   Tabs keep the tighter 0.5rem icon gap: they are compact horizontal pills, unlike the
   roomier 0.75rem used by menu items (.menu-link / .dropdown-item). */
.nav-tabs {
    --bs-nav-tabs-border-color:             var(--bs-border-color);
    --bs-nav-tabs-link-hover-border-color:  transparent;
    --bs-nav-tabs-link-active-color:        var(--bs-primary);
    --bs-nav-tabs-link-active-bg:           rgba(var(--bs-primary-rgb), .09);
    --bs-nav-tabs-link-active-border-color: transparent;
    gap: 0.25rem;
}
.nav-tabs .nav-link {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: var(--bs-body-font-size);
    font-weight: 400;
    color: var(--bs-tertiary-color);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    padding: 0.4375rem 0.625rem;
    transition: background 0.15s, color 0.15s;
}
.nav-tabs .nav-link i {
    color: var(--bs-secondary-color);
    transition: color 0.15s;
}
.nav-tabs .nav-link:hover:not(.active) {
    background: rgba(var(--bs-primary-rgb), .06);
    color: var(--bs-primary);
}
.nav-tabs .nav-link:hover:not(.active) i { color: var(--bs-primary); }
.nav-tabs .nav-link.active {
    font-weight: 600;
    color: var(--bs-primary);
    box-shadow: inset 0 -2px 0 var(--bs-primary);
}
.nav-tabs .nav-link.active i { color: var(--bs-primary); }

/* ============ EzActionMenu - Bootstrap dropdown styled like user-menu / menu-link ============ */

.ez-action-menu { position: relative; display: inline-flex; }

/* Toggle button - suppress Bootstrap caret since we use our own ellipsis icon */
.ez-action-menu .dropdown-toggle::after { display: none; }

/* Toggle button - transparent, borderless "quiet chrome" (app-wide default for every
   action menu). The trigger reads as a muted glyph at rest and only takes on the default
   body text color on hover, keyboard focus, or while its panel is open. Bootstrap's
   focus-visible ring is left intact for accessibility. The sm (icon-only, table-row)
   variant additionally trims its horizontal padding below. */
/* The trigger is Variant="borderless-secondary" now, which sets exactly this - transparent
   background, transparent border, muted glyph going to body colour - through --bs-btn-*
   tokens that Bootstrap composes. The fifteen lines of !important that used to force it
   here are not merely redundant: they would WIN over the tier and pin the rest colour to
   --bs-tertiary-color, which in this theme is the stronger of the two muted tokens and so
   rendered the borderless glyph louder than its bordered neighbours. */

/* sm (icon-only, table-row) trigger: trim left/right whitespace so it hugs the row
   edge, but keep a min square target so it stays comfortable to tap. The trigger is
   borderless now, so the old btn-sm font shrink just made the glyph look undersized;
   render it at body font-size so the ellipsis matches the row text beside it. */
.ez-action-menu--sm .ez-action-menu__toggle {
    padding-inline: 0.25rem !important;
    min-width: 2rem;
    min-height: 2rem;
    font-size: var(--bs-body-font-size) !important;
}

/* Full-size "More" trigger (rendered when TriggerVariant is set, e.g. the page-header
   "More" button): a normal solid button. Add a Bootstrap-style down caret so it reads as
   a menu-opening button, without needing the .dropdown-toggle class. */
.ez-action-menu__trigger::after {
    content: "";
    display: inline-block;
    margin-left: 0.4em;
    vertical-align: 0.15em;
    border-top: 0.3em solid;
    border-right: 0.3em solid transparent;
    border-bottom: 0;
    border-left: 0.3em solid transparent;
}

/* Panel - visual styles shared by all browsers */
.ez-action-menu .dropdown-menu {
    font-size: var(--bs-body-font-size);
    background: var(--bs-body-bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    padding: 0.375rem;
    min-width: 11rem;
    box-shadow: var(--bs-box-shadow);
    z-index: 1050;
    margin: 0;
}
.ez-action-menu--sm .dropdown-menu { min-width: 9rem; }

/* Fallback positioning for browsers without CSS Anchor Positioning support.
   Panel is absolutely positioned inside the relative wrapper - will be clipped
   by any ancestor with overflow:hidden/auto, but at least renders somewhere. */
@supports not (anchor-name: --test) {
    .ez-action-menu .dropdown-menu {
        position: absolute;
        top: calc(100% + 0.375rem);
    }
    .ez-action-menu__panel--right { right: 0; left: auto; }
    .ez-action-menu__panel--left  { left: 0; right: auto; }
}

/* CSS Anchor Positioning - Chrome 125+, Firefox 133+, Safari 18+.
   The panel is rendered in the fixed layer so it escapes overflow:hidden/auto
   clipping on any ancestor (tables, cards, modals, scrolling containers).
   anchor-name is emitted as an inline style on the wrapper div per instance.
   position-anchor is emitted as an inline style on the panel div per instance.
   Both use a unique GUID-based dashed-ident generated in EzActionMenu.razor. */
@supports (anchor-name: --test) {
    .ez-action-menu .dropdown-menu {
        position: fixed;
        top: calc(anchor(bottom) + 0.375rem);
        position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
    }
    /* Right-aligned: panel's right edge lines up with the trigger's right edge.
       anchor(right) in the right inset already resolves from the containing block's
       right edge, so it aligns right-to-right directly - no 100vw offset. */
    .ez-action-menu__panel--right {
        right: anchor(right);
        left: auto;
    }
    /* Left-aligned: panel's left edge lines up with the trigger's left edge */
    .ez-action-menu__panel--left {
        left: anchor(left);
        right: auto;
    }
}

/* Items - override Bootstrap .dropdown-item to match .menu-link */
.ez-action-menu .dropdown-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    padding: 0.4375rem 0.625rem;
    font-size: var(--bs-body-font-size);
    font-weight: 400;
    line-height: var(--bs-body-line-height);
    color: var(--bs-tertiary-color);
    border-radius: var(--radius-sm);
    background: transparent;
    text-decoration: none;
    white-space: nowrap;
    transition: background 0.15s, color 0.15s;
}
.ez-action-menu .dropdown-item i {
    width: 1.25em;
    text-align: center;
    color: var(--bs-secondary-color);
    transition: color 0.15s;
    flex-shrink: 0;
}
.ez-action-menu .dropdown-item:not(:disabled):not(.disabled):hover {
    background: rgba(var(--bs-primary-rgb), .08);
    color: var(--bs-primary);
}
.ez-action-menu .dropdown-item:not(:disabled):not(.disabled):hover i {
    color: var(--bs-primary);
}
.ez-action-menu .dropdown-item:disabled,
.ez-action-menu .dropdown-item.disabled {
    opacity: 0.45;
    pointer-events: none;
}

/* Variant: danger - matches .user-menu-logout */
.ez-action-menu .dropdown-item.menu-link--danger { color: var(--bs-danger); }
.ez-action-menu .dropdown-item.menu-link--danger i { color: var(--bs-danger); }
.ez-action-menu .dropdown-item.menu-link--danger:not(:disabled):hover {
    background: rgba(var(--bs-danger-rgb), .1);
    color: var(--bs-danger);
}
.ez-action-menu .dropdown-item.menu-link--danger:not(:disabled):hover i { color: var(--bs-danger); }

/* Variant: success */
.ez-action-menu .dropdown-item.menu-link--success { color: var(--bs-success); }
.ez-action-menu .dropdown-item.menu-link--success i { color: var(--bs-success); }
.ez-action-menu .dropdown-item.menu-link--success:not(:disabled):hover {
    background: rgba(var(--bs-success-rgb), .1);
    color: var(--bs-success);
}
.ez-action-menu .dropdown-item.menu-link--success:not(:disabled):hover i { color: var(--bs-success); }

/* Variant: warning */
.ez-action-menu .dropdown-item.menu-link--warning { color: var(--bs-warning); }
.ez-action-menu .dropdown-item.menu-link--warning i { color: var(--bs-warning); }
.ez-action-menu .dropdown-item.menu-link--warning:not(:disabled):hover {
    background: rgba(var(--bs-warning-rgb), .1);
    color: var(--bs-warning);
}
.ez-action-menu .dropdown-item.menu-link--warning:not(:disabled):hover i { color: var(--bs-warning); }

/* Divider - matches .user-menu-divider */
.ez-action-menu .dropdown-divider {
    border-top: 1px solid var(--bs-border-color);
    margin: 0.375rem 0.25rem;
}

/* ============ Mobile shell: bottom tab bar + safe-area (< xl) ============
   The app surface runs edge-to-edge inside the native WebView shell (and edge-to-edge on
   Android 15+ browsers), so the fixed chrome reserves the system bars via env(safe-area-inset-*).
   env() resolves to 0 on normal desktop browsers, so these rules are inert there. */

/* Bottom tab bar (BottomTabBar.razor) - mobile-only primary nav, thumb-reachable. Hidden at >=xl
   where the persistent sidebar takes over (component also carries d-xl-none as a belt-and-braces). */
.bottom-tabs {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1046;
    display: flex;
    background: var(--bs-body-bg);
    border-top: 1px solid var(--bs-border-color);
    /* Reserve the gesture-nav inset so the row sits above the system nav bar. */
    padding-bottom: env(safe-area-inset-bottom);
}
.bottom-tab {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.1875rem;
    padding: 0.5rem 0.25rem 0.4375rem;
    color: var(--bs-secondary-color);
    text-decoration: none;
    transition: color 0.15s;
}
.bottom-tab i { font-size: 1.25rem; line-height: 1; }
.bottom-tab-label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.bottom-tab.active { color: var(--bs-primary); }
.bottom-tab:hover { color: var(--bs-primary); }

/* Header plan/credit pills (EzSubscriptionBadge > EzPlanBadge / EzCreditBalance in the app-bar
   actions). The stock Bootstrap .badge renders these too small to read comfortably and too short
   to tap; bump the type up a notch and give the pill a >=24px min height so it is a legible,
   comfortable target next to the notifications and user-menu controls. */
.billing-badge {
    display: inline-flex;
    align-items: center;
    min-height: 1.75rem;
    padding-inline: 0.6rem;
    font-size: 0.75rem;
}

@media (min-width: 1200px) {
    .bottom-tabs { display: none; }
}

/* Safe-area + bottom-bar clearance on the app surface (< xl only). Top inset pushes the header
   clear of the status bar; bottom padding keeps the last content above the fixed tab bar and the
   gesture-nav inset. Scoped to .surface-app so the admin console (AdminLayout) is untouched. */
@media (max-width: 1199.98px) {
    .surface-app .main {
        padding-top: env(safe-area-inset-top);
        padding-bottom: calc(4.25rem + env(safe-area-inset-bottom));
    }

    /* The off-canvas drawer also spans under the status bar - inset its top so the logo/close row
       clears it. Keeps the existing 0.875rem padding as the baseline. */
    .side-menu { padding-top: calc(0.875rem + env(safe-area-inset-top)); }

    /* The app bar uses the shared base mobile header (7.5rem tall, 1.5rem gap, full-size logo) so
       it is identical to the marketing site's nav bar - no .surface-app compacting overrides. */
}


/* ===== ez-platform-privacy.css ===== */
/* ──────────────────────────────────────────────────────────────────────────
   Privacy mode masking.

   Driven by the `data-privacy` attribute on <html> (set during SSR from the
   `ez-privacy` cookie in App.razor, and flipped live by ezPrivacy.applyAndSave
   in js/layout.js). When privacy mode is ON, anything wrapped in the
   <EzSensitive> component (class .ez-sensitive) is blurred and made
   unselectable so it is safe to screen-share or work in public.

   Content stays masked until the user turns privacy mode off - there is
   deliberately no :hover reveal rule here.

   Text remains selectable/copyable while blurred (so users can still paste a
   value they need), and elements stay interactive. Works on both wrapped text
   (the <EzSensitive> component) and form fields given the .ez-sensitive class
   directly (e.g. <EzTextInput Class="ez-sensitive" />) - blurring the field
   matches the rest of the masked text.
   ────────────────────────────────────────────────────────────────────────── */

.ez-sensitive {
    /* Smooth the transition when privacy mode is toggled on/off. */
    transition: filter .12s ease, color .12s ease, text-shadow .12s ease;
}

/* Plain text/markup (spans, divs, table cells): blur the element itself. Form
   fields are handled separately below so their border/background stay crisp. */
[data-privacy="on"] .ez-sensitive:not(input):not(textarea):not(select) {
    filter: blur(9px);
    /* Keep the blurred smear inside a tidy rounded box. */
    border-radius: 3px;
}

/* Form fields: blur ONLY the typed value, not the input box. We hide the real
   glyphs (transparent text) and render a blurred copy via text-shadow, so the
   field's border, background and caret remain sharp. --bs-body-color is the
   themed text colour Bootstrap uses for .form-control, so the smear matches
   the real text in both light and dark themes. The value stays selectable and
   copyable, and the caret stays visible for editing. The shadow is large and
   doubled so the smear is dense enough to be unreadable. */
[data-privacy="on"] input.ez-sensitive,
[data-privacy="on"] textarea.ez-sensitive,
[data-privacy="on"] select.ez-sensitive {
    color: transparent;
    text-shadow: 0 0 11px var(--bs-body-color, #888), 0 0 11px var(--bs-body-color, #888);
    caret-color: var(--bs-body-color);
}

/* Selecting the value would otherwise repaint the real glyphs (the selection
   colour overrides the transparent text), making it readable while highlighted.
   Force the selected text transparent with no shadow so highlighting can never
   reveal it - the value is still copyable. */
[data-privacy="on"] input.ez-sensitive::selection,
[data-privacy="on"] textarea.ez-sensitive::selection,
[data-privacy="on"] select.ez-sensitive::selection {
    color: transparent;
    text-shadow: none;
}
[data-privacy="on"] input.ez-sensitive::-moz-selection,
[data-privacy="on"] textarea.ez-sensitive::-moz-selection,
[data-privacy="on"] select.ez-sensitive::-moz-selection {
    color: transparent;
    text-shadow: none;
}

/* ──────────────────────────────────────────────────────────────────────────
   Fixed-width masking - <EzSensitive Mask="EzMask.FixedWidth">.

   For numbers, the blur above is the wrong mask. `filter` does not change
   layout, so the masked element keeps the real value's exact width, and for a
   number the width IS the magnitude: a blurred "7" is visibly narrower than a
   blurred "12,480". The credit pill in the header therefore announced the
   user's order of magnitude while "masked", and resized as the balance grew.
   At badge type sizes the 9px blur also smears past the glyph to the pill edge
   and reads as a broken render rather than a deliberate mask.

   So numeric values swap to a constant-width token instead. The component
   renders BOTH the real value and the token; these rules pick which one is
   displayed. Doing the swap in CSS rather than C# is deliberate: <html
   data-privacy> is written during SSR from the ez-privacy cookie, so the mask
   is right in the first painted frame and on surfaces that never get a circuit
   - exactly like the blur above. The real value is display:none while masked,
   so it cannot be selected, copied or read out by a screen reader.
   ────────────────────────────────────────────────────────────────────────── */

.ez-sensitive-token { display: none; }

[data-privacy="on"] .ez-sensitive-fixed > .ez-sensitive-real { display: none; }
[data-privacy="on"] .ez-sensitive-fixed > .ez-sensitive-token { display: inline; }

/* Respect users who disable animations. */
@media (prefers-reduced-motion: reduce) {
    .ez-sensitive { transition: none; }
}


/* ===== ez-platform-error-ui.css ===== */
/* ============================================================
   ez-platform Error UI styles
   Blazor circuit error bar (#blazor-error-ui) - single source;
   never redeclare it elsewhere. (The reconnect modal lives in
   ez-platform.css with the other overlay components.)
   ============================================================ */

/* -- MainLayout -- */
/* Error UI overlay - themed to match the dark app shell. */
#blazor-error-ui {
    display: none; /* Blazor JS sets display:flex when an error occurs */
    flex-direction: column;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    z-index: 9998;
    background: #1a0f0f;
    border-top: 2px solid #ff6868;
    box-shadow: 0 -4px 24px rgba(255, 104, 104, 0.18);
    font-family: inherit;
    color: #e6e8ef;
}

.error-ui-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.75rem 1.25rem;
    flex-wrap: wrap;
}

.error-ui-left {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex: 1 1 auto;
    min-width: 0;
}

.error-ui-icon {
    font-size: 1.25rem;
    color: #ff6868;
    flex-shrink: 0;
}

.error-ui-text {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    min-width: 0;
}

.error-ui-title {
    font-weight: 600;
    color: #e6e8ef;
}

.error-ui-sub {
    color: #8a90a3;
}

.error-ui-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 0;
}

/* Error UI buttons use Bootstrap btn-outline-light / btn-outline-danger.
   Override to match the dark error bar theme. */
#blazor-error-ui .btn-outline-light {
    --bs-btn-color: #bcc1d3;
    --bs-btn-border-color: #2e323d;
    --bs-btn-hover-color: #e6e8ef;
    --bs-btn-hover-bg: #2e323d;
    --bs-btn-hover-border-color: #2e323d;
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--bs-primary);
    --bs-btn-active-border-color: var(--bs-primary);
}

.error-ui-detail {
    padding: 0.5rem 1.25rem 0.75rem;
    border-top: 1px solid rgba(255, 104, 104, .2);
    color: #8a90a3;
    background: rgba(255, 104, 104, .05);
    font-family: var(--bs-font-monospace);
}


/* ===== ez-platform-stat-cards.css ===== */
/* =============================================================
   Stat cards - the KPI tile family used inside EzCard.
   Shared, production styling: the Support module's TicketStatusCards
   renders these, and the optional demo dashboard reuses them. Covers
   visual details Bootstrap does not provide natively.
   ============================================================= */

.dash-stat-icon {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--bs-border-radius);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.125rem;
    color: #fff;
    flex-shrink: 0;
}

.dash-stat-value {
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: -0.5px;
    line-height: 1.1;
    margin-top: 1rem;
}

.dash-stat-label {
    color: var(--bs-secondary-color);
    font-size: 0.8125rem;
    margin-top: 0.25rem;
}

.dash-stat-bar-wrap {
    height: 0.3125rem;
    border-radius: 0.1875rem;
    background: var(--bs-tertiary-bg);
    margin-top: 0.75rem;
    overflow: hidden;
}

.dash-stat-bar {
    height: 100%;
    border-radius: 0.1875rem;
}

.dash-stat-trend--up {
    color: var(--bs-success);
    font-weight: 500;
}

.dash-stat-trend--down {
    color: var(--bs-warning);
    font-weight: 500;
}


/* ===== ez-platform-form-builder.css ===== */
/* =============================================================
   Form Builder mock - custom styles for the drag-and-drop form
   builder concept page (/admin/ui/custom/form-builder).

   Everything visual is driven by the live --bs-* theme tokens, so
   the builder tracks light and dark automatically. These rules only
   cover the builder-specific chrome (3-pane shell, field palette,
   canvas blocks, drag affordances) that Bootstrap does not provide.
   ============================================================= */

/* ── Workspace toolbar (full-width, above the panes) ─────────────
   Sits directly under EzPageTop and spans the whole builder, so the form
   name, status and workspace actions (Save / Preview / More) have the full
   page width and never clip - unlike a toolbar trapped inside the narrow
   canvas column. */
.fb-workbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem 0.75rem;
    padding: 0.5rem 0.75rem 0.5rem 1rem;
    margin-bottom: 1rem;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-lg);
}
.fb-workbar-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
}
.fb-workbar-name {
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: min(40vw, 20rem);
}
.fb-workbar-actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* ── 3-pane shell ───────────────────────────────────────────── */
.fb-shell {
    display: grid;
    grid-template-columns: 13rem minmax(0, 1fr) 17rem;
    gap: 1rem;
    align-items: start;
}
.fb-shell--preview { grid-template-columns: minmax(0, 1fr); }
.fb-shell--preview .fb-palette,
.fb-shell--preview .fb-props { display: none; }

/* Responsive columns. The canvas is always visible; the side panels drop out
   as they become drawers (see the offcanvas rules below):
     xl-down  - palette becomes a drawer, settings stay docked (canvas + settings)
     lg-down  - settings also becomes a drawer (canvas only) */
@media (max-width: 1199.98px) {
    .fb-shell:not(.fb-shell--preview) { grid-template-columns: minmax(0, 1fr) 17rem; }
}
@media (max-width: 991.98px) {
    .fb-shell { grid-template-columns: minmax(0, 1fr); }
}

/* ── Docked / drawer panes (Bootstrap responsive offcanvas) ──────
   .fb-palette is .offcanvas-xl and .fb-props is .offcanvas-lg, so the same
   markup renders as an inline grid column above the breakpoint and slides in
   as a drawer below it - no bespoke JS. Two tweaks are needed:
     1. Bootstrap forces the offcanvas background transparent at the docked
        size (with !important); restore the pane background there.
     2. In drawer mode the pane is a fixed-height flex column, so let the body
        scroll. */
@media (min-width: 1200px) {
    .fb-palette.offcanvas-xl { background-color: var(--bs-body-bg) !important; }
}
@media (min-width: 992px) {
    .fb-props.offcanvas-lg { background-color: var(--bs-body-bg) !important; }
}
@media (max-width: 1199.98px) {
    .fb-palette.offcanvas-xl .fb-pane-body { flex: 1 1 auto; overflow-y: auto; }
}
@media (max-width: 991.98px) {
    .fb-props.offcanvas-lg .fb-pane-body { flex: 1 1 auto; overflow-y: auto; }
}

/* ── Pane chrome ────────────────────────────────────────────── */
.fb-pane {
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-lg);
    overflow: hidden;
}
/* The address autocomplete's suggestion list (Google shadow DOM) would be clipped by the
   pane; un-clip only while one is hosted, and round the head/toolbar corners that the
   pane's overflow clipping normally handles. */
.fb-pane:has(gmp-place-autocomplete) { overflow: visible; }
.fb-pane:has(gmp-place-autocomplete) > .fb-pane-head,
.fb-pane:has(gmp-place-autocomplete) > .fb-toolbar {
    border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
}
.fb-pane-head {
    padding: 0.75rem 1rem;
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--bs-secondary-color);
    background: var(--bs-tertiary-bg);
    border-bottom: 1px solid var(--bs-border-color);
}
.fb-pane-body { padding: 0.75rem; }

/* ── Palette (field catalogue) ──────────────────────────────── */
.fb-palette-group-label {
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--bs-secondary-color);
    margin: 0.75rem 0.25rem 0.375rem;
}
.fb-palette-group-label:first-child { margin-top: 0.25rem; }
.fb-palette-item {
    display: flex;
    align-items: center;
    gap: 0;
    width: 100%;
    padding: 0.5rem 0.625rem 0.5rem 0.2rem;
    margin-bottom: 0.25rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    color: var(--bs-body-color);
    text-align: left;
    cursor: pointer; /* body click adds the field; only the grip drags */
    transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}
.fb-palette-item:hover {
    border-color: var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.06);
}
/* The field-type icon (first <i>; the grip's icon is nested in a span). */
.fb-palette-item > i:first-of-type {
    width: 1.1rem;
    margin-right: 0.5rem;
    text-align: center;
    color: var(--bs-primary);
}
.fb-palette-item > span:not(.ez-drag-handle) {
    flex: 1;
    font-size: 0.8125rem;
}

/* The drag handle itself is EzDragHandle (.ez-drag-handle in ez-platform.css) - it owns the
   grab/grabbing cursor, the faint colour and the layout, all of which started here. The
   .fb-field--chosen grabbing rule moved there with it, since that class is the form builder
   canvas telling the shared handle a drag is in progress. */
.fb-palette-add {
    color: var(--bs-secondary-color);
    font-size: 0.75rem;
    opacity: 0;
    transition: opacity 0.15s ease-in-out;
}
.fb-palette-item:hover .fb-palette-add { opacity: 1; }

/* ── Canvas ─────────────────────────────────────────────────── */
.fb-canvas { display: flex; flex-direction: column; }
.fb-canvas-scroll {
    padding: 1rem 1.25rem;
    background: var(--bs-secondary-bg);
}
/* In preview the canvas should read like the page, not an editor pane: drop the
   pane border/background (so only the form's own EzCard border shows) and let the
   row/col inside set the width + left alignment, matching the published form. */
/* Preview drops the canvas padding too, so the row/col alone positions the form -
   left-aligned at the same place as the published page. */
.fb-shell--preview .fb-canvas-scroll {
    padding: 0;
    background: var(--bs-body-bg);
}
.fb-shell--preview .fb-canvas {
    border-color: transparent;
    background: transparent;
    overflow: visible; /* pane is borderless in preview - don't clip the flush alert corner */
    border-radius: 0;
}
.fb-shell--preview .fb-form {
    max-width: none;
    margin: 0;
}
.fb-form {
    max-width: 42rem;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

/* Canvas heading - the form's own title + description atop the fields. Uses the
   standard heading markup (h6.ez-title-2 + .text-muted.small, same as EzCard) so
   it matches the heading shown in Preview / on the published form; this rule only
   adds the divider before the fields. */
.fb-form-header {
    padding: 0.875rem 1rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius);
    cursor: pointer;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.fb-form-header:hover { border-color: var(--border-strong); }
.fb-form-header--selected {
    border-color: var(--bs-primary);
    box-shadow: var(--ez-focus-ring);
}
/* Keyboard focus indicator for the role="button" card (mouse clicks do not trigger :focus-visible). */
.fb-form-header:focus-visible {
    outline: none;
    border-color: var(--bs-primary);
    box-shadow: var(--ez-focus-ring);
}

/* ── Field block ────────────────────────────────────────────── */
.fb-field {
    position: relative;
    display: flex;
    gap: 0;
    padding: 1rem 1.125rem 1rem 0.2rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius);
    cursor: pointer;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.fb-field:hover { border-color: var(--border-strong); }
.fb-field--selected {
    border-color: var(--bs-primary);
    box-shadow: var(--ez-focus-ring);
}
/* Keyboard focus indicator for the role="button" card (mouse clicks do not trigger :focus-visible). */
.fb-field:focus-visible {
    outline: none;
    border-color: var(--bs-primary);
    box-shadow: var(--ez-focus-ring);
}

/* ── Sortable canvas (SortableJS drag states) ───────────────── */
.fb-field-list {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    min-height: 3rem; /* keep an empty form a valid drop target */
}
/* Placeholder gap while dragging. Both the reorder and the palette->canvas drops
   use this same class, so both read as an identical clean dashed drop zone with a
   centred +. Only opacity / colour / visibility / a pseudo-element are touched -
   never spacing or border-width, which resize the box and break SortableJS's sort. */
.fb-field--ghost {
    position: relative;
    border-style: dashed;
    border-color: var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.08);
    box-shadow: none;
}
/* Hide the real content (field card or palette tile) so it reads as an empty drop
   zone. visibility keeps the box, so the sort math is unchanged. */
.fb-field--ghost > * { visibility: hidden; }
/* The drop indicator: a + centred in the zone, shown wherever a drop can land. */
.fb-field--ghost::after {
    content: "+";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--bs-primary);
    font-size: 1.5rem;
    font-weight: 300;
    line-height: 1;
    visibility: visible;
}
.fb-field--chosen { cursor: grabbing; }
/* The floating clone that follows the pointer (forceFallback): show what's being
   dragged, but not the row's duplicate/delete actions. */
.fb-field--fallback {
    opacity: 0.95;
    box-shadow: var(--bs-box-shadow-lg);
}
.fb-field--fallback .fb-field-actions { display: none; }
/* Positional only: nudges the shared handle to the top of a tall field card. Colour and
   cursor come from .ez-drag-handle. */
.fb-field-handle {
    align-items: flex-start;
    margin-right: 0.2rem;
    padding-top: 0.3rem;
}
.fb-field-main { flex: 1; min-width: 0; }
.fb-field-main .form-label { margin-bottom: 0.375rem; }
.fb-field-main .form-text { margin-top: 0.375rem; }
/* Field previews are non-interactive while editing, so a click selects the
   block; preview mode re-enables them so the form feels live. */
.fb-field-preview { pointer-events: none; }
.fb-shell--preview .fb-field-preview { pointer-events: auto; }
/* The shared renderer wraps each field in a .mb-3; drop that trailing gap inside a card. */
.fb-field-preview > .mb-3 { margin-bottom: 0; }
.fb-req { color: var(--bs-danger); }

.fb-field-actions {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    display: flex;
    gap: 0.25rem;
    opacity: 0;
    transition: opacity 0.15s ease-in-out;
}
.fb-field:hover .fb-field-actions,
.fb-field--selected .fb-field-actions { opacity: 1; }

/* Preview mode: strip the editing chrome so the form reads as the end user sees it. */
.fb-shell--preview .fb-field {
    border-color: transparent;
    padding-left: 1rem;
    cursor: default;
}
.fb-shell--preview .fb-field:hover { border-color: transparent; }
.fb-shell--preview .fb-field-handle,
.fb-shell--preview .fb-field-actions,
.fb-shell--preview .fb-dropzone { display: none; }

/* ── Small icon buttons (duplicate / delete / remove option) ──
   .fb-icon-btn is gone. It was the ORIGINAL quiet-outline button - the thing EzIconButton was
   modelled on, and EzIconButton is now the btn-ez-outline tier of EzButton. So this file was
   carrying the ancestor of the platform's whole button system as a private 28px copy, one that
   could only ever drift from the 31px sm it was duplicating.

   The field-row actions are EzButton Variant="outline-secondary" Size="sm" now (outline-danger
   for delete), which is also what gave them StopPropagation - the reason they had stayed raw. */

/* ── Section break, address, file, dropzone ─────────────────── */
.fb-section-break {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.8125rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}
.fb-section-break::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--bs-border-color);
}
.fb-file-drop {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 1rem;
    border: 1.5px dashed var(--bs-border-color);
    border-radius: var(--radius-sm);
    color: var(--bs-secondary-color);
    font-size: 0.8125rem;
}
.fb-dropzone {
    padding: 1.25rem;
    text-align: center;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
    border: 1.5px dashed var(--bs-border-color);
    border-radius: var(--radius);
}

/* ── Accordion nesting on the canvas ────────────────────────────
   Each accordion card holds a real nested Sortable list (.fb-accordion-body). It
   is indented behind a left rail so the nesting is obvious, keeps a min-height so
   an empty accordion is still a drop target, and its rows are ordinary .fb-field
   cards - so they make room and show the drop ghost exactly like the top level. */
.fb-accordion-body {
    margin-top: 0.75rem;
    padding-inline-start: 0.85rem;
    border-inline-start: 2px solid rgba(var(--bs-primary-rgb), 0.35);
    min-height: 2.75rem;
}
/* The empty-accordion hint lives inside the nested list: a quieter, smaller dropzone. */
.fb-accordion-empty {
    padding: 0.6rem 0.75rem;
    font-size: 0.75rem;
}

/* ── Submit-button picker (Form settings) ───────────────────────
   Each option is a live preview of the actual submit button (icon + text); the
   author clicks the one they want, and the chosen tile gets a primary ring. */
.fb-submit-options {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.fb-submit-option {
    padding: 0.35rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius);
    cursor: pointer;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.fb-submit-option:hover { border-color: var(--border-strong); }
.fb-submit-option--selected {
    border-color: var(--bs-primary);
    box-shadow: var(--ez-focus-ring);
}

/* ── Pane tab headers (palette "Add a field" + settings Field/Form) ───── */
.fb-pane-tabs {
    display: flex;
    align-items: flex-end;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem 0;
}
.fb-pane-tabs .fb-pane-tabs-strip {
    flex: 1 1 auto;
    flex-wrap: nowrap;
    margin-bottom: 0 !important;
}
/* The palette's single tab spans the full width of the header. */
.fb-palette .fb-pane-tabs-strip .nav-item { flex: 1 1 auto; }
.fb-palette .fb-pane-tabs-strip .nav-link { width: 100%; }
.fb-pane-tabs .fb-pane-tabs-strip .nav-link {
    padding: 0.5rem 0.625rem;
    white-space: nowrap;
}
.fb-pane-tabs .btn-close { margin-bottom: 0.5rem; }

/* ── Properties panel ───────────────────────────────────────── */
.fb-props-empty {
    padding: 2.5rem 1rem;
    text-align: center;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
}
.fb-props-empty i {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 1.5rem;
    color: var(--bs-tertiary-color);
}
.fb-option-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}
.fb-add-option {
    padding: 0;
    font-size: 0.8125rem;
    text-decoration: none;
}

/* ── Field editor ───────────────────────────────────────────── */
/* Reuses the builder field blocks for the real Standard / Custom field admin
   lists (user-fields, business-fields). A vertical stack of draggable rows. */
.fb-editor-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.fb-field--dragging {
    opacity: 0.5;
    border-style: dashed;
}
.fb-field--muted .fb-field-main { opacity: 0.55; }
.fb-field-title { font-weight: 500; }
.fb-field-meta { margin-top: 0.15rem; }
.fb-field-meta code { color: var(--bs-secondary-color); }

/* Fill-page confirmation tick. */
.fb-fill-done { font-size: 2.75rem; color: var(--bs-success); }
.fb-multi { display: flex; flex-direction: column; gap: 0.25rem; }


/* ===== ez-platform-buttons.css ===== */
/* ============================================================================
   Button system - .btn-ez-*
   ----------------------------------------------------------------------------
   Component: EzyPlatform.Components/EzButton.razor (the ONLY button component).
   Reference / decision aid: /admin/ui/custom/button-lab

   The quiet-outline look is entirely a matter of COLOUR, and Bootstrap already
   exposes colour through --bs-btn-* custom properties. So it rides on .btn as a
   token remap with almost no layout CSS: padding, font-size, line-height and
   radius stay stock Bootstrap, which is what keeps every button height-matched
   to .form-control / .form-select / .input-group at all three sizes.

       sm       31px   (partners .form-control-sm)
       default  38px   (partners .form-control)
       lg       48px   (partners .form-control-lg)

   THREE TIERS, ONE ORDINAL AXIS - filled > outline > borderless. They live in a
   single Variant string, never a variant plus a Border bool: a bool manufactures
   invalid combinations, where "primary with no border" would silently render a
   solid filled primary rather than the transparent brand-glyph button meant.

   REST IS IDENTICAL ACROSS ALL COLOURS within the quiet tiers. The colour only
   identifies what happens on HOVER, so borderless-danger at rest is
   indistinguishable from borderless-secondary until hovered.
   ========================================================================== */


/* ============================================================================
   1. STRUCTURE - three rules, and all three are fixes rather than features
   ========================================================================== */

/* 1a. CENTRING. ez-platform.css makes .btn display:inline-flex but never set
   justify-content, and Bootstrap's own centring is text-align, which stops
   applying the moment the element becomes a flex container. Auto-width buttons
   hid it; w-100 submit buttons on all five auth pages were left-aligned. */
.btn { justify-content: center; }

/* 1b. HEIGHT. Bootstrap's .btn declares no height; it inherits it from the
   tallest child's line box. A <span> label gets line-height 1.5; a Font Awesome
   glyph gets line-height 1. So dropping the label silently shortened the button
   by 8px - the old Size="icon" measured 23.8px against a 37.8px input.

   Derived from Bootstrap's own tokens rather than hardcoded, so it is a no-op
   for labelled buttons (it equals the height they already had) and it tracks
   .btn-sm and .btn-lg automatically. Nothing to keep in sync by hand. */
.btn {
    min-height: calc(var(--bs-btn-line-height) * 1em
                   + var(--bs-btn-padding-y) * 2
                   + var(--bs-btn-border-width) * 2);
}

/* Icon-only squares. EzButton emits .btn-ez-icon itself whenever Label and
   ChildContent are both empty - inferred, never a parameter, because a
   label-less non-square button is always wrong and a flag with exactly one
   correct value is not a flag. */
.btn-ez-icon {
    --bs-btn-padding-x: 0;
    min-width: calc(var(--bs-btn-line-height) * 1em
                  + var(--bs-btn-padding-y) * 2
                  + var(--bs-btn-border-width) * 2);
}

/* 1c. THE LEADING SLOT. Swapping a glyph for a spinner used to RESIZE the
   button: .spinner-border-sm is a flat 1rem while a Font Awesome glyph is only
   as wide as its own advance, which varies per glyph. One fixed 1em square for
   whichever occupies the slot means busy never changes the width, at any size,
   any tier, icon-only or labelled. The spinner is sized through Bootstrap's own
   custom properties rather than by overriding width/height directly. */
.btn > i,
.btn > .spinner-border {
    flex: 0 0 auto;
    width: 1em;
    height: 1em;
    font-size: inherit;
}
.btn > i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.btn > .spinner-border {
    --bs-spinner-width: 1em;
    --bs-spinner-height: 1em;
    --bs-spinner-border-width: 0.14em;
}

/* An <a class="btn"> cannot carry the disabled attribute, so EzButton marks it
   with .disabled instead. Bootstrap styles that but does not make it inert. */
a.btn.disabled { pointer-events: none; }


/* ============================================================================
   2. FILLED TIER - .btn-ez-{colour}
   ----------------------------------------------------------------------------
   The single dominant action per page or form. Hover and active darken the fill
   through color-mix, so every colour behaves identically with no per-variant
   hover values to maintain.
   ========================================================================== */

.btn-ez-filled {
    --bs-btn-font-weight:    500;
    --bs-btn-color:          #fff;
    --bs-btn-hover-color:    #fff;
    --bs-btn-active-color:   #fff;
    --bs-btn-disabled-color: #fff;
}

/* #fff and #000 below are blend endpoints for color-mix, not literal surface
   colours - the result tracks the token through both themes. */
.btn-ez-primary   { --ez-btn-tint: var(--bs-primary); }
.btn-ez-secondary { --ez-btn-tint: var(--bs-secondary); }
.btn-ez-success   { --ez-btn-tint: var(--bs-success); }
.btn-ez-danger    { --ez-btn-tint: var(--bs-danger); }
.btn-ez-warning   { --ez-btn-tint: var(--bs-warning); }
.btn-ez-info      { --ez-btn-tint: var(--bs-info); }

.btn-ez-primary,
.btn-ez-secondary,
.btn-ez-success,
.btn-ez-danger,
.btn-ez-warning,
.btn-ez-info {
    --bs-btn-bg:                    var(--ez-btn-tint);
    --bs-btn-border-color:          var(--ez-btn-tint);
    --bs-btn-hover-bg:              color-mix(in srgb, var(--ez-btn-tint) 88%, #000);
    --bs-btn-hover-border-color:    color-mix(in srgb, var(--ez-btn-tint) 85%, #000);
    --bs-btn-active-bg:             color-mix(in srgb, var(--ez-btn-tint) 80%, #000);
    --bs-btn-active-border-color:   color-mix(in srgb, var(--ez-btn-tint) 78%, #000);
    --bs-btn-disabled-bg:           var(--ez-btn-tint);
    --bs-btn-disabled-border-color: var(--ez-btn-tint);
}

/* Amber and cyan are far too light for white text - measured 9.60:1 and 1.96:1.
   This dark amber is the value the theme already used on btn-warning and is
   WCAG-compliant on --warning in both light (#d97706) and dark (#fbbf24). The
   same treatment rescues info, which fails hardest with white. */
.btn-ez-warning,
.btn-ez-info {
    --bs-btn-color:          #431407;
    --bs-btn-hover-color:    #431407;
    --bs-btn-active-color:   #431407;
    --bs-btn-disabled-color: #431407;
}


/* ============================================================================
   3. QUIET TIERS - shared rest state
   ----------------------------------------------------------------------------
   Outline and borderless look identical at rest: a muted glyph on the page
   background. Only the border differs, and only hover carries colour.
   ========================================================================== */

.btn-ez-outline,
.btn-ez-borderless {
    --bs-btn-font-weight:    500;
    --bs-btn-color:          var(--bs-secondary-color);
    --bs-btn-bg:             var(--bs-body-bg);
    --bs-btn-disabled-color: var(--bs-secondary-color);
    --bs-btn-disabled-bg:    var(--bs-body-bg);
}


/* ── 3a. OUTLINE - hairline border, no fill ─────────────────────────────── */

.btn-ez-outline {
    --bs-btn-border-color:          var(--bs-border-color);
    --bs-btn-hover-bg:              var(--bs-body-bg);
    --bs-btn-active-bg:             var(--bs-secondary-bg);
    --bs-btn-disabled-border-color: var(--bs-border-color);
}

/* Neutral is the odd one out: "secondary hovering to secondary" would be
   invisible, so it resolves to body colour plus the stronger border. */
.btn-ez-outline-secondary {
    --bs-btn-hover-color:         var(--bs-body-color);
    --bs-btn-hover-border-color:  var(--border-strong);
    --bs-btn-active-color:        var(--bs-body-color);
    --bs-btn-active-border-color: var(--border-strong);
}

.btn-ez-outline-primary { --ez-btn-tint: var(--bs-primary); }
.btn-ez-outline-success { --ez-btn-tint: var(--bs-success); }
.btn-ez-outline-danger  { --ez-btn-tint: var(--bs-danger); }
.btn-ez-outline-warning { --ez-btn-tint: var(--bs-warning); }
.btn-ez-outline-info    { --ez-btn-tint: var(--bs-info); }

.btn-ez-outline-primary,
.btn-ez-outline-success,
.btn-ez-outline-danger,
.btn-ez-outline-warning,
.btn-ez-outline-info {
    --bs-btn-hover-color:         var(--ez-btn-tint);
    --bs-btn-hover-border-color:  var(--ez-btn-tint);
    --bs-btn-active-color:        var(--ez-btn-tint);
    --bs-btn-active-border-color: var(--ez-btn-tint);
}


/* ── 3b. BORDERLESS - no border, no fill, glyph colour only ─────────────── */

/* The border is kept and made TRANSPARENT rather than removed: border:0 would
   take 2px off the height and desynchronise the button from the input beside
   it, which is the exact problem this system exists to fix.

   Nothing but the glyph colour changes on hover - no border, no background, in
   any state. This was already the shipped behaviour of the table action-menu
   trigger, which needed 15 lines of !important in ez-platform-nav.css to force
   it. Assigning --bs-btn-* lets Bootstrap compose it instead, so no !important
   is needed anywhere. */
.btn-ez-borderless {
    --bs-btn-border-color:          transparent;
    --bs-btn-bg:                    transparent;
    --bs-btn-hover-bg:              transparent;
    --bs-btn-hover-border-color:    transparent;
    --bs-btn-active-bg:             transparent;
    --bs-btn-active-border-color:   transparent;
    --bs-btn-disabled-bg:           transparent;
    --bs-btn-disabled-border-color: transparent;
}

.btn-ez-borderless-secondary {
    --bs-btn-hover-color:  var(--bs-body-color);
    --bs-btn-active-color: var(--bs-body-color);
}

.btn-ez-borderless-primary { --ez-btn-tint: var(--bs-primary); }
.btn-ez-borderless-success { --ez-btn-tint: var(--bs-success); }
.btn-ez-borderless-danger  { --ez-btn-tint: var(--bs-danger); }
.btn-ez-borderless-warning { --ez-btn-tint: var(--bs-warning); }
.btn-ez-borderless-info    { --ez-btn-tint: var(--bs-info); }

.btn-ez-borderless-primary,
.btn-ez-borderless-success,
.btn-ez-borderless-danger,
.btn-ez-borderless-warning,
.btn-ez-borderless-info {
    --bs-btn-hover-color:  var(--ez-btn-tint);
    --bs-btn-active-color: var(--ez-btn-tint);
}


/* ============================================================================
   4. FOCUS AND PRESSED
   ========================================================================== */

/* One focus treatment for every tier. Borderless depends on it most, having no
   border of its own to thicken. */
.btn:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
    box-shadow: none;
}

/* Segmented toggles (EzFileBrowser list/grid, time-range pickers). A subtle
   fill rather than the old solid brand fill, which shouted beside quiet
   neighbours. */
.btn-ez-outline[aria-pressed="true"],
.btn-ez-borderless[aria-pressed="true"],
.btn-ez-outline[aria-expanded="true"],
.btn-ez-borderless[aria-expanded="true"] {
    --bs-btn-bg:           var(--bs-secondary-bg);
    --bs-btn-color:        var(--bs-body-color);
    --bs-btn-border-color: var(--border-strong);
}


/* ============================================================================
   5. COMPOSITE CONTENT - a two-line user card inside a button
   ----------------------------------------------------------------------------
   Everything below exists because .user-card-* was authored against a
   fixed-height card and does not fit a line box unaided. Applies only where
   EzButton is given ChildContent carrying those classes.
   ========================================================================== */

/* Sized in em so the avatar tracks sm / default / lg. As a fixed 1.7rem it was
   TALLER than a default button's 24px line box and dragged the button to 41px -
   which is where the old bespoke 42px header controls came from. The button
   never chose 42px; the avatar did.

   Written as 2.45em because em on width resolves against the avatar's OWN
   reduced font-size: 0.55 x 2.45 = 1.35 of the button. Keep the pair in step. */
.btn .user-card-ava {
    font-size: 0.55em;
    width: 2.45em;
    height: 2.45em;
    line-height: 1;
    letter-spacing: 0.02em;
}
/* inline-block picks up descender space from the button's 1.5 line-height and
   measured 31.9px around a 27px circle - which becomes the tallest child the
   moment the text block is brought to size. */
.btn .user-card-ava-wrap { display: flex; align-items: center; line-height: 1; }

.btn:has(.user-card-ava) { gap: 0.625em; }

/* Bootstrap's .btn centres its text; a two-line card reads left-aligned, with
   the chevron hard right rather than hugging the name. */
.btn .user-card-text { text-align: start; flex: 1 1 auto; min-width: 0; }
.btn .user-card-chev { margin-left: auto; }

/* Left alone the name takes .btn's 1rem/1.5 and the business line takes .small
   of that - about 41.6px stacked inside a 24px line box. 0.70em and 0.60em at
   1.15 total 1.495em, just inside the 1.5em line box .btn already reserves.
   The 3px margin-top .user-card-biz ships is harmless against a fixed-height
   card and fatal against a line box it has to fit inside. */
.btn .user-card-name { font-size: 0.70em; line-height: 1.15; }
.btn .user-card-biz  { font-size: 0.60em; line-height: 1.15; margin-top: 0; }
.btn .user-card-name-row { gap: 0.35em; }


/* ===== ez-platform-upload.css ===== */
/* ez-platform-upload.css - styling for the shared EzUpload drag-and-drop uploader.
   Loaded after the brand theme, so every colour resolves through the brand's --bs-* tokens and
   adapts to light/dark automatically. Kept in its own file so the core theme stays untouched. */

.ez-upload {
    display: block;
}

/* -- Preview strip ------------------------------------------------------- */

.ez-upload-previews {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}

.ez-upload-thumb {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 84px;
    height: 84px;
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-secondary-bg);
    color: var(--bs-secondary-color);
    font-size: 1.5rem;
}

.ez-upload-thumb-link {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer; /* reset for the <button> rendered when the in-app viewer is available */
}

.ez-upload-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Large previews show the whole image, uncropped, sized to a taller tile. */
.ez-upload-thumb--lg {
    width: auto;
    height: 140px;
}

.ez-upload-thumb--lg .ez-upload-thumb-link {
    position: static;
}

.ez-upload-thumb--lg img {
    width: auto;
    height: 100%;
    max-width: 240px;
    object-fit: contain;
}

/* Remove button: an opaque circle wrapping a bare btn-close. The circle must be a SEPARATE element
   from btn-close because in dark mode the theme applies filter:invert(1) to .btn-close, which would
   also invert a background-colour set on it (light glyph on a light circle = invisible). Wrapping
   keeps the filter on the glyph only, so the circle stays opaque and the X stays visible. */
.ez-upload-thumb-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.25rem;
    line-height: 1;
    background-color: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 50%;
}

.ez-upload-thumb-remove:hover {
    background-color: var(--bs-secondary-bg);
}

.ez-upload-thumb-remove .btn-close {
    width: 0.7em;
    height: 0.7em;
    padding: 0;
    font-size: 0.9rem;
    opacity: 1;
}

.ez-upload-thumb > .ez-upload-thumb-remove {
    position: absolute;
    top: 0.25rem;
    right: 0.25rem;
    z-index: 3;
}

/* -- Dropzone ------------------------------------------------------------ */

.ez-upload-zone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    width: 100%;
    min-height: 8rem;
    margin-bottom: 0;
    padding: 1.5rem 1rem;
    text-align: center;
    cursor: pointer;
    border: 2px dashed var(--bs-border-color);
    border-radius: var(--bs-border-radius-lg);
    background: var(--bs-secondary-bg);
    color: var(--bs-body-color);
    transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

.ez-upload-zone:hover {
    border-color: var(--bs-primary);
}

/* Toggled from ezUpload.js while a file is dragged over the zone. */
.ez-upload-zone--dragging {
    border-color: var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.08);
}

.ez-upload-icon {
    font-size: 1.75rem;
    line-height: 1;
    color: var(--bs-secondary-color);
}

.ez-upload-zone:hover .ez-upload-icon,
.ez-upload-zone--dragging .ez-upload-icon {
    color: var(--bs-primary);
}

.ez-upload-title {
    font-weight: 500;
}

.ez-upload-browse {
    color: var(--bs-primary);
}

/* The auto hint is "TYPES\n(up to N MB)" - keep the newline as a line break (size on its own line). */
.ez-upload-hint {
    white-space: pre-line;
}

/* The transparent native <InputFile> fills the zone so click AND native drop both hit it. */
.ez-upload-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}

.ez-upload-input:disabled {
    cursor: default;
}

/* -- Tile variant -------------------------------------------------------- */

/* Fixed 3:4 so every tile is the same size regardless of the image's aspect. */
.ez-upload-zone--tile {
    aspect-ratio: 3 / 4;
    min-height: 15rem;
    max-height: 60vh;
}

.ez-upload-zone--tile.ez-upload-zone--filled {
    padding: 0;
    overflow: hidden;
    cursor: default;
    border-style: solid;
    background: var(--bs-secondary-bg);
}

/* TileOpensViewer: the filled tile is a photo you can look at, so say so. Placed after the rule
   above, which sets the inert default. */
.ez-upload-zone--tile.ez-upload-zone--viewable {
    cursor: zoom-in;
}

/* Blurred backdrop: fills the letterbox around a contain-fitted image with a darkened blow-up of
   the image itself, so a portrait in a fixed 3:4 tile has no dead space. Driven by --tile-fill. */
.ez-upload-zone--tile.ez-upload-zone--filled::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: var(--tile-fill);
    background-size: cover;
    background-position: center;
    transform: scale(1.2);
    filter: blur(24px) brightness(0.55) saturate(1.25);
}

.ez-upload-tile-link {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem;
}

.ez-upload-tile-img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--bs-border-radius);
}

/* Tile action cluster: positioned top-right. Reveal behaviour (faint at rest, solid on hover/touch)
   comes from the shared .ez-media-actions class the markup also applies. */
.ez-upload-tile-bar {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 3;
}
.ez-upload-zone--tile:hover .ez-media-actions {
    opacity: 1;
}
/* No actions while the upload is disabled (e.g. locked during generation). */
.ez-upload--disabled .ez-upload-tile-bar {
    display: none;
}

/* Busy overlay: a flat semi-transparent scrim + centred spinner shown over a FILLED tile while a
   replacement image uploads (the empty zone shows its spinner in the prompt instead). Flat by design -
   no blur, no shadow. */
.ez-upload-busy {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Fully opaque: while an upload runs, the prompt/old image underneath must disappear
       completely - a see-through scrim left icons and text bleeding through the spinner. */
    background: var(--bs-body-bg);
    border-radius: inherit;
}

/* Instant busy: pre-rendered in every zone and revealed by ezUpload.js the moment the native
   change/drop event fires - before ANY server round trip or client-side resize - so a phone user
   sees a spinner the instant they pick a photo. Stays visible through the server Busy phase too
   (one continuous overlay from pick to done), fully opaque like .ez-upload-busy. */
.ez-upload-instant { display: none; }
.ez-upload-zone--instant .ez-upload-instant,
.ez-upload-zone--busy .ez-upload-instant {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bs-body-bg);
    border-radius: inherit;
}

/* The overlay indicator earns a bigger stage than the inline default: a proper spinner and
   readable label, centred in the (large) zone. */
.ez-upload-instant .ez-uploading,
.ez-upload-busy .ez-uploading {
    font-size: 1.125rem;
    gap: 0.625rem;
}
.ez-upload-instant .spinner-border,
.ez-upload-busy .spinner-border {
    width: 1.75rem;
    height: 1.75rem;
    border-width: 0.2em;
}

/* Pointer-adaptive prompt copy: mice read the drag & drop wording + the full format list; touch
   screens read a tap instruction + just the size cap (phones pick from a gallery sheet and never
   think in file formats). */
.ez-upload-copy-touch { display: none; }
@media (pointer: coarse) {
    .ez-upload-copy-full { display: none; }
    .ez-upload-copy-touch { display: inline; }
}

/* -- Disabled / busy ----------------------------------------------------- */

.ez-upload--disabled .ez-upload-zone,
.ez-upload-zone--busy {
    cursor: default;
}
/* Only the disabled state dims the whole zone; the busy states no longer do - element opacity
   would wash out the (opaque) uploading overlay that now covers the zone. */
.ez-upload--disabled .ez-upload-zone {
    opacity: 0.65;
}

.ez-upload--disabled .ez-upload-zone:hover,
.ez-upload-zone--busy:hover {
    border-color: var(--bs-border-color);
}

.ez-upload--disabled .ez-upload-zone:hover .ez-upload-icon,
.ez-upload-zone--busy:hover .ez-upload-icon {
    color: var(--bs-secondary-color);
}


/* ===== ez-platform-image-viewer.css ===== */
/* ez-platform-image-viewer.css - the bare, app-wide image lightbox (EzImageViewer).
   Loaded after the brand theme; kept in its own file so the core theme stays untouched. */

.ez-imgviewer {
    position: fixed;
    inset: 0;
    z-index: 1090; /* above modals (1055) and toasts - it is a full-screen takeover */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    background: rgba(0, 0, 0, 0.82);
    cursor: zoom-out; /* the backdrop closes on click */
    animation: ez-imgviewer-fade 0.15s ease-out;
}

@keyframes ez-imgviewer-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

.ez-imgviewer-img {
    max-width: 90vw;
    max-height: 90vh;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--bs-border-radius);
    box-shadow: var(--bs-box-shadow-lg);
    cursor: default; /* clicking the image itself does not close */
    animation: ez-imgviewer-pop 0.2s cubic-bezier(0.2, 1.2, 0.4, 1);
}

@keyframes ez-imgviewer-pop {
    from { transform: scale(0.94); opacity: 0; }
    to { transform: scale(1); opacity: 1; }
}

/* Close button: an EzButton in the shared .ez-media-actions family (faint at rest, solid on
   hover/focus, solid on touch). Backdrop-click and Esc also close, so the quiet rest state is safe. */
.ez-imgviewer-close {
    position: fixed;
    top: 1rem;
    right: 1.25rem;
    z-index: 1;
}
/* Hovering anywhere in the lightbox brings the faint-at-rest close button to full. */
.ez-imgviewer:hover .ez-media-actions {
    opacity: 1;
}

/* ---- zoom (pinch / wheel / drag / double-click - ezImageViewer.js) ----
   The module writes ONLY the three custom properties below and the two state attributes; every
   declaration lives here. Data attributes rather than classes because Blazor owns the class attribute
   and rewrites it wholesale on re-render, which would wipe a class the module had set. */
.ez-imgviewer {
    /* A zoomed image spills past the viewport: clip it rather than let it grow the scrollable area. */
    overflow: hidden;
    /* The lightbox handles pinch itself (nothing here scrolls), so the browser must not claim it. */
    touch-action: none;
}

.ez-imgviewer-img[data-ezv-zoomed] {
    transform: translate(var(--ezv-x, 0px), var(--ezv-y, 0px)) scale(var(--ezv-z, 1));
    /* Only advertise the drag once there is somewhere to drag to. */
    cursor: grab;
    /* The open "pop" above is a transform animation too, and a running animation beats this rule -
       without this it would stamp on the first zoom of a freshly opened image. */
    animation: none;
}

.ez-imgviewer-img[data-ezv-panning] {
    cursor: grabbing;
}

@media (prefers-reduced-motion: reduce) {
    .ez-imgviewer,
    .ez-imgviewer-img {
        animation: none;
    }
}


/* ===== ez-platform-onboarding.css ===== */
/* =============================================================
   Onboarding dashboard card (DashboardOnboardingCard.razor)
   The dismissible "get set up" card shown on the home dashboard.
   Layout/progress/steps use Bootstrap utilities in the component;
   only the hero icon tile needs custom CSS.
   (The old Getting Started page and its step/rail/dot/line classes
   were removed with the page - see docs/agents/css.md.)
   ============================================================= */

.dash-onboard-hero {
    width: 3rem;
    height: 3rem;
    border-radius: var(--bs-border-radius-lg);
    background: rgba(var(--bs-primary-rgb), 0.12);
    color: var(--bs-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.375rem;
    flex-shrink: 0;
}


/* ===== ez-platform-button-lab.css ===== */
/* ============================================================================
   BUTTON LAB - candidate styles, NOT yet the platform standard
   ----------------------------------------------------------------------------
   Scratch sheet backing /admin/ui/custom/button-lab. Every rule is either a
   brand-new class name (.btn-ez-*) that nothing in the app uses yet, or is
   scoped under the .btnlab wrapper. Loading this file changes NOTHING about the
   811 existing EzButton call sites.

   Delete this file (and its PlatformThemeBundleInput line in EzyPlatform.UI.csproj)
   once a direction is chosen, promoting the winning rules into ez-platform.css.

   THE THESIS: the quiet-outline look of .ez-icon-btn is entirely a matter of
   COLOUR, and Bootstrap already exposes colour through --bs-btn-* custom
   properties. So the look moves onto .btn as a token remap with zero layout CSS,
   leaving padding, font-size, line-height, radius and therefore HEIGHT as stock
   Bootstrap - which is what keeps buttons agreeing with .form-control,
   .form-select and .input-group at every size.

   THREE TIERS, ONE ORDINAL AXIS. filled > outline > borderless is a single scale
   of weight, so it lives in one Variant string rather than a variant plus a
   Border bool. A bool would manufacture invalid combinations: "primary with no
   border" would silently render a solid filled primary rather than the
   transparent brand-glyph button the caller meant.

   REST IS IDENTICAL ACROSS ALL COLOURS within the quiet tiers. The colour only
   identifies what happens on HOVER. borderless-danger at rest is
   indistinguishable from borderless-secondary until hovered.
   ========================================================================== */


/* ============================================================================
   1. STRUCTURE - the only layout rules, and both are fixes rather than features
   ========================================================================== */

/* 1a. CENTRING. ez-platform.css makes .btn display:inline-flex but never sets
   justify-content. Bootstrap's own .btn centres via text-align:center, which
   stops applying the moment the element becomes a flex container. Auto-width
   buttons hide it; anything with a forced width does not - w-100 submit buttons
   on all five auth pages are left-aligned today, and so is any icon-only square. */
.btnlab .btn { justify-content: center; }

/* 1b. HEIGHT. Bootstrap's .btn declares no height; it inherits it from the
   tallest child's line box. A <span> label gets line-height 1.5; a Font Awesome
   glyph gets line-height 1. So dropping the label silently shortens the button
   by 8px, which is why EzButton Size="icon" measures 23.8px against a 37.8px
   input today.

   Derived from Bootstrap's own tokens rather than hardcoded, so it is a no-op
   for labelled buttons (it equals the height they already have) and it tracks
   .btn-sm and .btn-lg automatically. Nothing to keep in sync by hand. */
.btnlab[data-lock="on"] .btn {
    min-height: calc(var(--bs-btn-line-height) * 1em
                   + var(--bs-btn-padding-y) * 2
                   + var(--bs-btn-border-width) * 2);
}

/* Icon-only squares. EzButton emits .btn-ez-icon itself whenever Label is empty -
   inferred, never a parameter, because a label-less non-square button is always
   wrong and a flag with one correct value is not a flag. */
.btnlab[data-lock="on"] .btn-ez-icon {
    min-width: calc(var(--bs-btn-line-height) * 1em
                  + var(--bs-btn-padding-y) * 2
                  + var(--bs-btn-border-width) * 2);
    --bs-btn-padding-x: 0;
}

/* 1c. THE LEADING SLOT. Swapping a glyph for a spinner currently RESIZES the
   button: .spinner-border-sm is a flat 1rem while a Font Awesome glyph is only
   as wide as its own advance, which varies per glyph. One fixed 1em square for
   whichever occupies the slot means busy never changes the width, at any size,
   any tier, icon-only or labelled. Lifted from .ez-btn2, which already proved it.
   The spinner is sized through Bootstrap's own custom properties rather than by
   overriding width/height directly. */
.btnlab .btn > i,
.btnlab .btn > .spinner-border {
    flex: 0 0 auto;
    width: 1em;
    height: 1em;
    font-size: inherit;
}
.btnlab .btn > i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.btnlab .btn > .spinner-border {
    --bs-spinner-width: 1em;
    --bs-spinner-height: 1em;
    --bs-spinner-border-width: 0.14em;
}


/* ============================================================================
   2. FILLED TIER - .btn-ez-{colour}
   ----------------------------------------------------------------------------
   The single dominant action. Hover and active darken the fill via color-mix so
   every colour behaves identically without per-variant hover values.
   ========================================================================== */

.btn-ez-filled {
    --bs-btn-font-weight: 500;
    --bs-btn-color:          #fff;
    --bs-btn-hover-color:    #fff;
    --bs-btn-active-color:   #fff;
    --bs-btn-disabled-color: #fff;
}

/* One pattern per colour: fill and border take the token, hover/active darken it.
   #fff and #000 here are blend endpoints for color-mix, not literal surface
   colours - the result tracks the token through both themes. */
.btn-ez-primary   { --ez-btn-tint: var(--bs-primary); }
.btn-ez-secondary { --ez-btn-tint: var(--bs-secondary); }
.btn-ez-success   { --ez-btn-tint: var(--bs-success); }
.btn-ez-danger    { --ez-btn-tint: var(--bs-danger); }
.btn-ez-warning   { --ez-btn-tint: var(--bs-warning); }
.btn-ez-info      { --ez-btn-tint: var(--bs-info); }

.btn-ez-primary,
.btn-ez-secondary,
.btn-ez-success,
.btn-ez-danger,
.btn-ez-warning,
.btn-ez-info {
    --bs-btn-bg:                    var(--ez-btn-tint);
    --bs-btn-border-color:          var(--ez-btn-tint);
    --bs-btn-hover-bg:              color-mix(in srgb, var(--ez-btn-tint) 88%, #000);
    --bs-btn-hover-border-color:    color-mix(in srgb, var(--ez-btn-tint) 85%, #000);
    --bs-btn-active-bg:             color-mix(in srgb, var(--ez-btn-tint) 80%, #000);
    --bs-btn-active-border-color:   color-mix(in srgb, var(--ez-btn-tint) 78%, #000);
    --bs-btn-disabled-bg:           var(--ez-btn-tint);
    --bs-btn-disabled-border-color: var(--ez-btn-tint);
}

/* Amber is too light for white text. This dark amber is the same value the
   existing theme uses on btn-warning and is WCAG-compliant on the --warning
   token in both light (#d97706) and dark (#fbbf24) themes. */
.btn-ez-warning {
    --bs-btn-color:          #431407;
    --bs-btn-hover-color:    #431407;
    --bs-btn-active-color:   #431407;
    --bs-btn-disabled-color: #431407;
}


/* ============================================================================
   3. QUIET TIERS - shared rest state
   ----------------------------------------------------------------------------
   Outline and borderless are identical at rest: a muted glyph on the page
   background. Only the border differs, and only the hover carries colour.
   ========================================================================== */

.btn-ez-outline,
.btn-ez-borderless {
    --bs-btn-font-weight:    500;
    --bs-btn-color:          var(--bs-secondary-color);
    --bs-btn-bg:             var(--bs-body-bg);
    --bs-btn-disabled-color: var(--bs-secondary-color);
    --bs-btn-disabled-bg:    var(--bs-body-bg);
}


/* ── 3a. OUTLINE - hairline border, no fill ─────────────────────────────── */

.btn-ez-outline {
    --bs-btn-border-color:          var(--bs-border-color);
    --bs-btn-hover-bg:              var(--bs-body-bg);
    --bs-btn-active-bg:             var(--bs-secondary-bg);
    --bs-btn-disabled-border-color: var(--bs-border-color);
}

/* Neutral is the odd one out: "secondary hovering to secondary" would be
   invisible, so it resolves to body colour plus the stronger border. */
.btn-ez-outline-secondary {
    --bs-btn-hover-color:         var(--bs-body-color);
    --bs-btn-hover-border-color:  var(--border-strong);
    --bs-btn-active-color:        var(--bs-body-color);
    --bs-btn-active-border-color: var(--border-strong);
}

.btn-ez-outline-primary { --ez-btn-tint: var(--bs-primary); }
.btn-ez-outline-success { --ez-btn-tint: var(--bs-success); }
.btn-ez-outline-danger  { --ez-btn-tint: var(--bs-danger); }
.btn-ez-outline-warning { --ez-btn-tint: var(--bs-warning); }
.btn-ez-outline-info    { --ez-btn-tint: var(--bs-info); }

.btn-ez-outline-primary,
.btn-ez-outline-success,
.btn-ez-outline-danger,
.btn-ez-outline-warning,
.btn-ez-outline-info {
    --bs-btn-hover-color:         var(--ez-btn-tint);
    --bs-btn-hover-border-color:  var(--ez-btn-tint);
    --bs-btn-active-color:        var(--ez-btn-tint);
    --bs-btn-active-border-color: var(--ez-btn-tint);
}


/* ── 3b. BORDERLESS - no border, no fill, glyph colour only ─────────────── */

/* The border is kept and made TRANSPARENT rather than removed. border:0 would
   take 2px off the height and desynchronise the button from the search input
   beside it, which is the exact problem this work exists to fix.

   Nothing but the glyph colour may change on hover: no border, no background,
   in any state. This is the shipped behaviour of the table action-menu trigger
   (ez-platform-nav.css), which currently needs 15 lines of !important to force
   it. Assigning --bs-btn-* instead means Bootstrap composes it and no
   !important is needed anywhere. */
.btn-ez-borderless {
    --bs-btn-border-color:          transparent;
    --bs-btn-bg:                    transparent;
    --bs-btn-hover-bg:              transparent;
    --bs-btn-hover-border-color:    transparent;
    --bs-btn-active-bg:             transparent;
    --bs-btn-active-border-color:   transparent;
    --bs-btn-disabled-bg:           transparent;
    --bs-btn-disabled-border-color: transparent;
}

.btn-ez-borderless-secondary {
    --bs-btn-hover-color:  var(--bs-body-color);
    --bs-btn-active-color: var(--bs-body-color);
}

.btn-ez-borderless-primary { --ez-btn-tint: var(--bs-primary); }
.btn-ez-borderless-success { --ez-btn-tint: var(--bs-success); }
.btn-ez-borderless-danger  { --ez-btn-tint: var(--bs-danger); }
.btn-ez-borderless-warning { --ez-btn-tint: var(--bs-warning); }
.btn-ez-borderless-info    { --ez-btn-tint: var(--bs-info); }

.btn-ez-borderless-primary,
.btn-ez-borderless-success,
.btn-ez-borderless-danger,
.btn-ez-borderless-warning,
.btn-ez-borderless-info {
    --bs-btn-hover-color:  var(--ez-btn-tint);
    --bs-btn-active-color: var(--ez-btn-tint);
}


/* ============================================================================
   4. TOGGLES - the open decisions, isolated one per attribute
   ========================================================================== */

/* 4a. RADIUS. Bootstrap already scales it with size (4 / 6 / 8px for sm /
   default / lg), which tracks the button's own proportions. Flattening to a
   single value is a real choice, not an obvious improvement: 8px on a 31px
   button reads much rounder than 8px on a 48px one. Inputs move with the
   buttons either way, since .form-control-* uses the identical scale. */
.btnlab[data-radius="flat6"] .btn,
.btnlab[data-radius="flat6"] .form-control,
.btnlab[data-radius="flat6"] .form-select { --bs-btn-border-radius: 0.375rem; border-radius: 0.375rem; }

.btnlab[data-radius="flat8"] .btn,
.btnlab[data-radius="flat8"] .form-control,
.btnlab[data-radius="flat8"] .form-select { --bs-btn-border-radius: var(--radius-sm); border-radius: var(--radius-sm); }

/* An input-group is one visual box, so its inner corners must stay square
   whatever the outer radius is. */
.btnlab[data-radius="flat6"] .input-group > :first-child,
.btnlab[data-radius="flat8"] .input-group > :first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.btnlab[data-radius="flat6"] .input-group > :last-child,
.btnlab[data-radius="flat8"] .input-group > :last-child { border-top-left-radius: 0; border-bottom-left-radius: 0; }

/* 4b. FONT SIZE. .btn renders 1rem (16px) while the app body is 14px, which is
   why a labelled button happens to match .form-control today. Dropping buttons
   to body size shortens them to ~35px UNLESS the height lock is on. Inputs must
   stay at 16px regardless: iOS Safari zooms the page on focus below that. */
.btnlab[data-fs="body"] .btn { --bs-btn-font-size: var(--bs-body-font-size); }

/* With body-size text the derived min-height would shrink too, so pin the lock
   to the INPUT's metrics instead: 14px button text, still 38px tall. */
.btnlab[data-fs="body"][data-lock="on"] .btn {
    min-height: calc(1.5 * 1rem + var(--bs-btn-padding-y) * 2 + var(--bs-btn-border-width) * 2);
}
.btnlab[data-fs="body"][data-lock="on"] .btn-ez-icon {
    min-width: calc(1.5 * 1rem + var(--bs-btn-padding-y) * 2 + var(--bs-btn-border-width) * 2);
}

/* 4c. FOCUS RING. Three treatments exist for one concept today: .ez-icon-btn
   uses an outline with offset, Bootstrap .btn uses a box-shadow ring, and the
   form fields use --ez-focus-ring. Borderless especially depends on this, since
   it has no border of its own to thicken. */
.btnlab[data-focus="outline"] .btn:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
    box-shadow: none;
}
.btnlab[data-focus="ring"] .btn:focus-visible {
    outline: 0;
    box-shadow: var(--ez-focus-ring);
}

/* 4d. PRESSED / TOGGLE STATE. Today an aria-pressed btn-outline-secondary gets a
   SOLID BRAND FILL, which will be very loud beside quiet neighbours. Segmented
   toggles are real (EzFileBrowser list/grid). */
.btnlab[data-pressed="subtle"] .btn-ez-outline[aria-pressed="true"],
.btnlab[data-pressed="subtle"] .btn-ez-borderless[aria-pressed="true"] {
    --bs-btn-bg:           var(--bs-secondary-bg);
    --bs-btn-color:        var(--bs-body-color);
    --bs-btn-border-color: var(--border-strong);
}
.btnlab[data-pressed="solid"] .btn-ez-outline[aria-pressed="true"],
.btnlab[data-pressed="solid"] .btn-ez-borderless[aria-pressed="true"] {
    --bs-btn-bg:           var(--bs-primary);
    --bs-btn-color:        #fff;
    --bs-btn-border-color: var(--bs-primary);
}


/* 4e. FORM FIELD HOVER. Fields currently hover to --ez-field-border-hover, a
   brand-tinted grey (color-mix of the primary at 42%), while their FOCUS border
   is the full brand colour. So hover and focus differ only by saturation, which
   is hard to tell apart while tabbing and mousing around a form. Matching the
   outline tier instead gives buttons and fields one shared three-step ladder -
   hairline at rest, --border-strong on hover, brand on focus - which matters
   most in the action bar, where a search input and a Filter button sit side by
   side and currently hover differently. */
.btnlab[data-fieldhover="neutral"] .form-control:hover:not(:focus):not(:disabled):not([readonly]),
.btnlab[data-fieldhover="neutral"] .form-select:hover:not(:focus):not(:disabled),
.btnlab[data-fieldhover="neutral"] .input-group:hover:not(:focus-within) .input-group-text,
.btnlab[data-fieldhover="neutral"] .input-group:hover:not(:focus-within) .form-control:not(:disabled):not([readonly]) {
    border-color: var(--border-strong);
}
.btnlab[data-fieldhover="brand"] .form-control:hover:not(:focus):not(:disabled):not([readonly]),
.btnlab[data-fieldhover="brand"] .form-select:hover:not(:focus):not(:disabled),
.btnlab[data-fieldhover="brand"] .input-group:hover:not(:focus-within) .input-group-text,
.btnlab[data-fieldhover="brand"] .input-group:hover:not(:focus-within) .form-control:not(:disabled):not([readonly]) {
    border-color: var(--bs-primary);
}


/* ============================================================================
   5. SEARCH INPUT - match the outline tier
   ----------------------------------------------------------------------------
   The search box border is ALREADY deliberately matched to btn-outline-secondary,
   but via var(--bs-secondary) - Bootstrap's mid-grey, far heavier than the
   --bs-border-color hairline the .ez-icon-btn family uses. Once the outline tier
   becomes the hairline, the search input is just a normal .form-control again,
   so promoting this means DELETING the overrides in ez-platform.css and
   ez-platform-forms.css rather than repointing them. Previewed here.
   ========================================================================== */
.btnlab[data-search="match"] .ez-search-input .input-group-text,
.btnlab[data-search="match"] .ez-search-input .form-control:not(:focus):not(:hover) {
    border-color: var(--bs-border-color);
}
/* The magnifying glass tracks the same muted token the button glyphs use,
   otherwise it sits darker than everything around it. */
.btnlab[data-search="match"] .ez-search-input .input-group-text > i {
    color: var(--bs-secondary-color) !important;
}


/* ============================================================================
   6. LAB PAGE CHROME - presentation of the lab itself, never promoted
   ========================================================================== */

.btnlab-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0.625rem 0;
    border-bottom: 1px dashed var(--bs-border-color);
}
.btnlab-row:last-child { border-bottom: 0; }

.btnlab-label {
    flex: 0 0 12rem;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
}

.btnlab-h {
    margin-left: auto;
    font-family: var(--bs-font-monospace);
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
    white-space: nowrap;
}
.btnlab-h[data-mismatch="true"]  { color: var(--bs-danger); font-weight: 700; }
.btnlab-h[data-mismatch="false"] { color: var(--bs-success); }

.btnlab-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
}
@media (min-width: 992px) {
    .btnlab-grid { grid-template-columns: 1fr 1fr; }
    .btnlab-grid--3 { grid-template-columns: repeat(3, 1fr); }
}

.btnlab-pane {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    padding: 1rem;
    min-width: 0;
}
/* ── Avatar inside a button ──────────────────────────────────────────────
   .user-card-ava ships as a FIXED 1.7rem (27.2px) circle with .small text, and that
   single fact is why the header controls are 42px today. A default .btn's content box
   is its line box, 1.5 x 16px = 24px - so a 27.2px circle is TALLER than the box and
   drags the button to ~41px, breaking parity with the 38px bell beside it. The button
   never chose 42px; the avatar did.

   Fixed by sizing the avatar in em, so it tracks sm / default / lg and always stays
   inside the line box. The initials then get real room because they are set as a
   fraction of the circle rather than of the button.

     initials  0.55 x the button's font-size
     circle    1.35 x the button's font-size

   Written as 2.45em because em on width resolves against the avatar's OWN reduced
   font-size: 0.55 x 2.45 = 1.35. Keep the two in step if either is retuned.

   Scoped to `.btn` so the "Today" bar - which uses the real .user-card - keeps shipping
   behaviour and stays an honest before/after. */
.btnlab .btn .user-card-ava {
    font-size: 0.55em;
    width: 2.45em;
    height: 2.45em;
    line-height: 1;
    letter-spacing: 0.02em;
}

/* The gap between avatar and name scales with the button too - a flat 0.5rem reads
   tight once the button is lg. */
.btnlab .btn:has(.user-card-ava) { gap: 0.625em; }

/* Bootstrap's .btn centres its text; a two-line user card has to read left-aligned like
   the real .user-card does, and the chevron has to sit hard right rather than hugging
   the name. */
.btnlab .btn .user-card-text { text-align: start; flex: 1 1 auto; min-width: 0; }
.btnlab .btn .user-card-chev { margin-left: auto; }

/* A two-line user card is the one control that cannot simply inherit the button's type.
   Left alone, the name takes .btn's 1rem/1.5 and the business line takes .small's 0.875em
   of that - about 41.6px of text stacked inside a 24px line box, which drags a default
   button from 38px to 55px. Today's .user-card avoids this only by pinning its own
   line-height and running at the 14px body size.

   Sized in em against the button so the block fits the line box exactly at every size:
       name  0.70em x 1.15    business  0.60em x 1.15
   which totals 1.495em - just inside the 1.5em line box .btn already reserves. That is
   what lets the user menu sit at the same height as the bell beside it instead of
   dictating a bespoke one, and it is why the business line pushes the header towards lg:
   at lg the name lands at 14px (exactly today's size), at default it drops to 11.2px.

   Two smaller things also had to go, both of which added height invisibly:
   .user-card-biz ships a 3px margin-top (fine against a fixed-height card, fatal against a
   line box it has to fit inside), and .user-card-ava-wrap is an inline-block, so it picks up
   descender space from the button's 1.5 line-height and measured 31.9px around a 27px circle
   - which would have become the tallest child the moment the text block was brought to size. */
.btnlab .btn .user-card-name { font-size: 0.70em; line-height: 1.15; }
.btnlab .btn .user-card-biz  { font-size: 0.60em; line-height: 1.15; margin-top: 0; }
.btnlab .btn .user-card-name-row { gap: 0.35em; }
.btnlab .btn .user-card-ava-wrap { display: flex; align-items: center; line-height: 1; }

/* Lab-only helpers. These exist so the page carries no inline style attributes - the platform
   rule is Bootstrap utility > Bootstrap CSS var > custom class > inline style (never). */
.btnlab-ref-input  { max-width: 12rem; }
.btnlab-search     { flex: 1 1 14rem; }
/* Static previews of interactive chrome: shown for shape, not meant to be clicked. */
.btnlab-inert      { pointer-events: none; }
/* .page-header at 0.4375rem padding: 48px lg controls then sit flush in a bar that keeps its
   current 62px height (48 + 14 = 62), instead of growing it to 68px. */
.btnlab-header--tight { padding: 0.4375rem; }
/* The dark-text alternative for the filled tier, for the WCAG comparison row. */
.btnlab-darktext   { --bs-btn-color: #1a1a1a; --bs-btn-hover-color: #1a1a1a; --bs-btn-active-color: #1a1a1a; }

.btnlab-pane-head {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    margin-bottom: 0.75rem;
}

/* No forced-theme island here on purpose: the dark tokens are scoped to
   [data-theme-brand][data-bs-theme="dark"] on <html> and there is no
   [data-bs-theme="light"] block to restore, so a nested data-bs-theme would
   inherit dark tokens and quietly show a lie. Judge light mode with the real
   theme toggle in the user menu. */

