/* 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;
}

/* ── Image frames ─────────────────────────────────────────────
   Every stored-file image the platform shows sits in a FRAME: a box with a fixed
   aspect ratio and a muted ground, with the image fitted inside it. This is the
   treatment the VCR studios arrived at (.st-tile-img, .to-slot, .ss-adminthumb)
   and it is now the platform default, so a thumbnail in an admin table and a tile
   in a studio are the same object at different sizes.

   Why a frame rather than width/height on the <img>: the box exists before the
   bytes arrive and survives their absence. A run whose image is missing keeps its
   slot instead of collapsing the row, a slow image does not reflow the grid as it
   loads, and portrait and landscape sources line up in a column instead of each
   setting their own height.

   3/4 is the default because that is the shape of everything these apps generate -
   a person, head to knee. The other ratios are MODIFIERS, not a value passed in:
   a component that took a free-form ratio would have to write it into a style
   attribute, and inline styling is banned. A shape the app needs is a class here. */
.ez-img-frame {
    --ez-img-ar: 3 / 4;
    --ez-img-fit: contain;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    aspect-ratio: var(--ez-img-ar);
    overflow: hidden;
    /* The studios' own treatment (.to-slot, .ss-adminthumb, .sp-tile): a hairline
       border over a muted ground. The border is what makes a letterboxed image and
       an empty placeholder both read as a deliberate frame rather than as a gap. */
    background-color: var(--bs-secondary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-lg);
    /* Makes the frame a size container so the placeholder glyph can be sized against
       the BOX. A percentage font-size cannot do this - it resolves against the parent's
       font-size, not the element's dimensions, which is why the glyph was previously
       pinned to its floor at every size. */
    container-type: size;
}

/* Sits above the placeholder glyph and hides it whenever there are bytes to show. */
.ez-img-frame > img {
    position: relative;
    z-index: 1;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: var(--ez-img-fit);
}

/* ── Letterbox backdrop ───────────────────────────────────────
   A blown-up, blurred, darkened copy of the same photo filling the bars either side
   of a contained image. Straight from the VCR studios' .to-slot::before, and the
   reason it belongs to every framed image rather than to them: contain is the
   default here, so ALMOST EVERY image on the platform is letterboxed, and a portrait
   marooned on a flat grey slab reads as a picture of a picture. The blur makes the
   frame read as one object at any aspect ratio.

   The studios drove this from `style="--fill:url(...)"`, which is the banned inline
   style wearing a custom property - and it was only ever a way to get a runtime URL
   into a background-image. EzImage renders a second <img> at the same src instead:
   the browser serves it from cache, and the whole effect becomes class-driven, so it
   needs nothing at the call site. Those studio sites remain as legacy.

   Sits between the placeholder glyph (z-index 0) and the real image (z-index 1). */
.ez-img-frame > img.ez-img-fill {
    --ez-img-blur: 24px;
    position: absolute;
    inset: 0;
    z-index: 0;
    object-fit: cover;
    transform: scale(1.2);
    filter: blur(var(--ez-img-blur)) brightness(0.55) saturate(1.25);
}

/* Nothing to fill: cover already reaches every edge, so the backdrop is pure cost -
   a second decode and a blur on pixels no one can see. */
.ez-img-frame--cover > img.ez-img-fill { display: none; }

/* A 24px blur on a 40px thumbnail is a smear that swallows the picture. The blur has
   to scale with the box, not with the design it was drawn for. */
.ez-img-frame--sm > img.ez-img-fill { --ez-img-blur: 6px; }

/* contain is the default because these are photographs of people and cropping a
   face is worse than letterboxing. Opt into cover for a decorative tile where
   filling the box matters more than showing all of it. */
.ez-img-frame--cover { --ez-img-fit: cover; }

/* The shapes the apps actually use. Add one here rather than passing a value in. */
.ez-img-frame--square { --ez-img-ar: 1; }
.ez-img-frame--wide   { --ez-img-ar: 16 / 9; }

/* Table-row thumbnail. Matches VCR .ss-adminthumb so admin lists agree across apps,
   including its tighter radius - the large radius is for tiles, not for 40px rows. */
.ez-img-frame--sm {
    width: 2.6rem;
    border-radius: var(--bs-border-radius);
}

/* ── Missing image ────────────────────────────────────────────
   The placeholder glyph. Always rendered, sitting UNDER the image, so it is already
   in place for the case the server cannot predict: a key that exists on the record
   but 404s, which only the browser discovers. js/missing-image.js then hides the
   failed <img> and this shows through - no script has to inject markup, and the
   known-null case (EzImage renders no <img> at all) lands on the identical visual.

   A real Font Awesome element rather than a background image, because an <img> is a
   replaced element whose pseudo-elements never render - which was what forced an
   earlier data-URI SVG, and with it a hardcoded grey that had to work on every
   surface. An icon inherits the theme instead.

   Deliberately NOT the app's brand mark: the brand is per-app and admin-configurable
   (and is itself a stored file that can go missing), and stamping a logo on every
   failure ties the brand to breakage.

   Lives in the BUNDLE, not in ez-platform.css. That file is the BRAND theme, loaded
   as themes/{BrandTheme.Current}.css - so a rule placed there reaches only
   deployments still on the default brand and silently does nothing on an app with
   its own theme. This is structural, not brand styling. */
.ez-img-ph {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Overrides the line-height the glyph would otherwise inherit from the page, which
       is what shifts an icon off the optical centre of its box even when the flexbox
       centring is correct. */
    line-height: 1;
    color: var(--bs-secondary-color);
    opacity: .55;
    /* Sized against the FRAME via container query units, so one rule covers a 2.6rem
       table thumbnail and a full-width result tile with no per-call-site modifier.
       cqmin is 1% of the container's smaller side, so the glyph keeps the same
       proportion of the box at every size. A percentage would NOT work here: font-size
       percentages resolve against the inherited font-size, not the box. */
    font-size: clamp(1rem, 42cqmin, 3.5rem);
}

/* A failed load, tagged by js/missing-image.js. Hiding the image (rather than the
   whole frame) keeps the box and reveals the glyph already behind it. */
.ez-img-frame > img.ez-img-missing { display: none; }

/* An unframed <img> that failed - a page not yet moved onto EzImage. No glyph is
   possible here, for the replaced-element reason above, so this only suppresses the
   browser's broken icon and the leaked alt text, leaving a quiet muted box. */
img.ez-img-missing {
    color: transparent;
    background-color: var(--bs-secondary-bg);
    border-radius: var(--bs-border-radius);
}


/* ===== 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);

    /* Disabled fields. Declared here rather than aliased off Bootstrap, because
       --bs-btn-disabled-opacity is declared INSIDE Bootstrap's .btn rule block, so it does not
       exist on a .form-control - var(--bs-btn-disabled-opacity) there resolves to nothing,
       which makes the whole opacity declaration invalid and silently leaves the field at full
       strength. Deliberately below Bootstrap's 0.65 default: a field is a large, mostly-empty
       box, so it needs a harder fade than a button to read as dead.
       KEEP IN STEP with the .btn override in ez-platform-buttons.css - the two are one
       decision, and a disabled field beside a disabled button must fade by the same amount. */
    --ez-field-disabled-opacity: 0.4;
}

/* ── 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(--bs-border-radius);
    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, and Bootstrap's own
   disabled opacity. The field keeps its normal border and fill so it still reads as an
   INPUT - the state is carried by the fade alone, exactly as it is on a disabled button. */
[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: var(--ez-field-disabled-opacity);
}

/* ── 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(--bs-border-radius);
    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(--bs-border-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: var(--bs-border-radius-lg);
    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: var(--bs-border-radius-lg);
    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(--bs-border-radius-lg) var(--bs-border-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(--bs-border-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;
}
/* The admin user page header: one avatar spanning the name, facts and flags rows. */
.user-card-ava-lg { width: 4.5rem; height: 4.5rem; font-size: 1.5rem; flex-shrink: 0; }
.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(--bs-border-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(--bs-border-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(--bs-border-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: var(--bs-border-radius-lg);
    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: var(--bs-border-radius-lg); 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(--bs-border-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: var(--bs-border-radius-lg);
    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: var(--bs-border-radius);
    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(--bs-border-radius);
    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(--bs-border-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(--bs-border-radius) var(--bs-border-radius) 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(--bs-border-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(--bs-border-radius);
    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;
}

/* Header (EzActionMenuHeader) - a caption naming the group of items under it, for a panel that mixes
   two kinds of action (a staff-only cluster under the user's own, a "danger zone", an export format
   list). Sized and coloured as chrome, NOT as an item: uppercase, smaller, and on the same muted
   --bs-secondary-color the resting items use, so it can never be mistaken for a disabled entry.
   Every token here is a Bootstrap theme variable, so light and dark both follow with no second rule. */
.ez-action-menu .dropdown-header {
    padding: 0.4375rem 0.625rem 0.1875rem;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    white-space: nowrap;
}
/* A header at the top of the panel, or straight after a divider, sits against space something else
   already provided - the panel's own padding, or the rule's margin - so it drops its top inset and
   the break reads as one gap instead of two. */
.ez-action-menu .dropdown-header:first-child,
.ez-action-menu .dropdown-divider + .dropdown-header { padding-top: 0; }

/* ============ 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(--bs-border-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(--bs-border-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(--bs-border-radius-lg) - 1px) calc(var(--bs-border-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(--bs-border-radius);
    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(--bs-border-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(--bs-border-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(--bs-border-radius);
    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(--bs-border-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(--bs-border-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; }

/* Disabled buttons fade harder than Bootstrap's 0.65 default. Same specificity as
   Bootstrap's own .btn block, so this wins purely on cascade order - the bundle loads
   after bootstrap.min.css in App.razor. Overriding the variable rather than the opacity
   property keeps every tier and colour on one knob, since all three read it through
   Bootstrap's shared .btn:disabled rule.
   KEEP IN STEP with --ez-field-disabled-opacity in ez-platform-forms.css. */
.btn { --bs-btn-disabled-opacity: 0.4; }


/* ============================================================================
   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;
}

