/* 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.
   The one exception is the "Inter Fallback" block at the END of
   this file, which is hand-written and must survive a regenerate.

   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;
}

/* =============================================================
   Metric-matched fallbacks for Inter - HAND-WRITTEN, keep this
   block when the faces above are regenerated (see the README).

   font-display: swap paints text in the fallback first and swaps
   to Inter when it arrives. Inter is about 6.5% WIDER than Roboto
   or Arial, so every line, button and label grew at the swap and
   the page shifted under the reader - the jump on a first open.

   These faces are the system font at Inter's size, so the swap
   changes the letter shapes and nothing else. size-adjust matches
   the average advance width over typical copy (prose for the
   regular faces, UI labels for the bold ones); the ascent and
   descent overrides are Inter's own (1984 / 494 per 2048 em),
   divided by that size-adjust, so the baseline does not move
   either. Measured in Chromium against the shipped woff2 files,
   Roboto 5 and Liberation Sans (metric-identical to Arial).

   Two families because one face cannot carry two size-adjusts:
   Roboto is what Android (and the app's WebView) falls back to,
   Arial/Helvetica is what iOS, macOS and Windows have. A face
   whose local() fonts are all missing is skipped, so listing both
   in the stack costs nothing. Regular covers 100-549 and bold
   550-900, so 600/700 text is never synthesised from the regular.
   ============================================================= */
@font-face {
    font-family: 'Inter Fallback Roboto';
    font-style: normal;
    font-weight: 100 549;
    src: local('Roboto'), local('Roboto-Regular'), local('Roboto Regular');
    size-adjust: 107.4%;
    ascent-override: 90.20%;
    descent-override: 22.46%;
    line-gap-override: 0%;
}
@font-face {
    font-family: 'Inter Fallback Roboto';
    font-style: normal;
    font-weight: 550 900;
    src: local('Roboto Bold'), local('Roboto-Bold');
    size-adjust: 107.7%;
    ascent-override: 89.95%;
    descent-override: 22.40%;
    line-gap-override: 0%;
}
@font-face {
    font-family: 'Inter Fallback Arial';
    font-style: normal;
    font-weight: 100 549;
    src: local('Arial'), local('ArialMT'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 106.8%;
    ascent-override: 90.71%;
    descent-override: 22.59%;
    line-gap-override: 0%;
}
@font-face {
    font-family: 'Inter Fallback Arial';
    font-style: normal;
    font-weight: 550 900;
    src: local('Arial Bold'), local('Arial-BoldMT'), local('Helvetica Bold'), local('Helvetica-Bold'), local('Liberation Sans Bold');
    size-adjust: 101.2%;
    ascent-override: 95.73%;
    descent-override: 23.84%;
    line-gap-override: 0%;
}


/* ===== ez-platform-theme-fonts.css ===== */
/* =============================================================
   Self-hosted webfonts for the brand themes (themes/brands/*.css).

   GENERATED by tools/fonts/fetch-theme-fonts.mjs - do not hand-edit.
   Same rules as ez-platform-fonts.css: served from this app, never
   from fonts.googleapis.com, with Google's unicode-range split kept
   so a browser fetches only the subsets it renders. A family no
   theme is using is never downloaded at all.

   Licensed under the SIL Open Font License 1.1; the licence texts sit
   beside the files in wwwroot/lib/fonts/ (OFL-*.txt).
   ============================================================= */

/* ---- IBM Plex Sans (theme: Slate) ---- */
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/ibm-plex-sans-400-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;
}
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/ibm-plex-sans-400-700-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/ibm-plex-sans-400-700-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/ibm-plex-sans-400-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;
}
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/ibm-plex-sans-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/ibm-plex-sans-400-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;
}

/* ---- Source Sans 3 (theme: Eucalyptus) ---- */
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/source-sans-3-400-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;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/source-sans-3-400-700-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/source-sans-3-400-700-greek-ext.woff2') format('woff2');
    unicode-range: U+1F00-1FFF;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/source-sans-3-400-700-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/source-sans-3-400-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;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/source-sans-3-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/source-sans-3-400-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;
}

/* ---- Public Sans (theme: Ledger) ---- */
@font-face {
    font-family: 'Public Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/public-sans-400-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;
}
@font-face {
    font-family: 'Public Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/public-sans-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Public Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/public-sans-400-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;
}

/* ---- Work Sans (theme: Ironbark) ---- */
@font-face {
    font-family: 'Work Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/work-sans-400-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;
}
@font-face {
    font-family: 'Work Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/work-sans-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Work Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/work-sans-400-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;
}

/* ---- Figtree (theme: Coastline) ---- */
@font-face {
    font-family: 'Figtree';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/figtree-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Figtree';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/figtree-400-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;
}

/* ---- Manrope (theme: Indigo) ---- */
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/manrope-400-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;
}
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/manrope-400-700-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/manrope-400-700-greek.woff2') format('woff2');
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/manrope-400-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;
}
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/manrope-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/manrope-400-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;
}

/* ---- DM Sans (theme: Sandstone) ---- */
@font-face {
    font-family: 'DM Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/dm-sans-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'DM Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/dm-sans-400-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;
}

/* ---- Plus Jakarta Sans (theme: Graphite) ---- */
@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/plus-jakarta-sans-400-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;
}
@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/plus-jakarta-sans-400-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;
}
@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/plus-jakarta-sans-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/plus-jakarta-sans-400-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;
}

/* ---- Nunito Sans (theme: Clarity) ---- */
@font-face {
    font-family: 'Nunito Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/nunito-sans-400-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;
}
@font-face {
    font-family: 'Nunito Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/nunito-sans-400-700-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: 'Nunito Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/nunito-sans-400-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;
}
@font-face {
    font-family: 'Nunito Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/nunito-sans-400-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Nunito Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../lib/fonts/nunito-sans-400-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.css ===== */
/* =============================================================
   ez-platform.css - the platform's core component styling
   Brand-independent: every colour, font, corner and sidebar value here comes from the tokens a
   brand theme sets (themes/brands/{key}.css, chosen by BrandThemeProvider and linked before the
   bundle in App.razor). This file ships first in ez-platform-bundle.css on every page, whatever the
   theme, so a new theme never needs a copy of it. Never put a palette value here.
   ============================================================= */

/* ── Structure - brand-independent, set once ────────────────── */
/* Layout, spacing, and typography tokens that never change
   regardless of which brand theme is active. */
:root {
    --side-w:               17.5rem;

    /* Placeholder text, as ONE resolved colour rather than a colour plus an opacity.
       Two consumers depend on it and only one of them can read CSS: EzyPay's Stripe card fields
       live in a cross-origin iframe and are themed through an appearance object that accepts a
       colour and has nowhere to put an opacity. Keeping the fade inside the colour means the card
       fields can read this token directly and match by construction.
       Deliberately faint - a placeholder is a hint, not content. Anything that must be READ
       belongs in the label or the field Hint. */
    --ez-placeholder-color: color-mix(in srgb, var(--bs-secondary-color) 35%, transparent);

    --app-max-w:            90rem;
    --app-gutter:           1.5rem;
    --app-gutter-y:         1rem;

    /* Below xl: how far the rule under the header bar sits ABOVE the bar's bottom edge, reclaiming
       the dead space under the logo's ink. The one source of truth for where that line is drawn: the
       header draws its rule here (.page-title), and an app surface that replaces the rule with its own
       (VCR's studio step strip) pulls itself up by the same amount, so both land on the same pixel
       from the first paint with nothing measured at runtime. */
    --ez-header-rule-lift:  0.8125rem;

    --bs-body-font-size:    0.875rem;
}

/* ── Corner radius ──────────────────────────────────────────────
   The platform has NO radius scale of its own. Every corner resolves
   through Bootstrap's tokens, so a .card, a .btn, a .rounded-* utility
   and a hand-rolled ez- panel all agree by construction. Only the five
   VALUES below decide the look:

     OPTION B - Bootstrap stock, ACTIVE (nothing is declared, so Bootstrap's
     own :root values stand):
       sm 0.25rem / base 0.375rem / lg 0.5rem / xl 1rem / pill 50rem

     OPTION C - the brand values, kept below but commented out:
       sm 0.25rem / base 0.625rem / lg 0.875rem / xl 1rem / pill 999rem

   Caveat carried over from the migration: the old --radius-sm (0.5rem)
   and --radius (0.625rem) collapsed into one tier, so 0.5rem cannot be
   restored independently of 0.625rem.

   Measured side by side on 2026-08-10, so the trade is on record:
                        option B      option C
     .btn / .form-control    6px          10px
     VCR marketing surfaces  8px          14px
   Option C also re-fills Bootstrap's empty 8-16px gap, where 26 of the
   remaining hand-written radius values in the codebase happen to sit.

   >>> OPTION B IS CURRENTLY ACTIVE. Each brand theme now declares these five
   >>> values itself (themes/brands/*.css), so the Violet theme's file is where
   >>> option C goes; the block below is kept for the record.
   ──────────────────────────────────────────────────────────────

:root {
    --bs-border-radius-sm:      0.25rem;
    --bs-border-radius:         0.625rem;
    --bs-border-radius-lg:      0.875rem;
    --bs-border-radius-xl:      1rem;
    --bs-border-radius-pill:    999rem;
}
                                                                    */

/* The light and dark palettes that used to sit here are the Violet brand theme now:
   themes/brands/ez-platform.css. */

/* ── Progress bars follow the brand ───────────────────────────── */
/* Bootstrap declares --bs-progress-bar-bg on .progress itself (as its stock blue), so a token on the
   brand root above can never reach it - it has to be re-pointed on the component. Brand-agnostic on
   purpose: var(--bs-primary) resolves per theme and per brand, so light, dark and every app's own
   brand file all get their primary without a second rule. A bar that needs a status colour still
   takes a bg-* utility on .progress-bar, which wins over this. */
.progress,
.progress-stacked {
    --bs-progress-bar-bg: var(--bs-primary);
}

/* Bootstrap hard-codes white text on its primary fills. A theme whose primary is light (most dark
   palettes) sets a dark --ez-on-primary instead, so the two primary-fill utilities follow it. */
.badge.bg-primary { --bs-badge-color: var(--ez-on-primary); }
.text-bg-primary { color: var(--ez-on-primary) !important; }

body {
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
}

/* ── Base link styles ─────────────────────────────────────────── */
a {
    color: var(--bs-link-color);
    transition: color 0.2s ease;
}
a:hover {
    color: var(--bs-link-hover-color);
}

/* ── Logo sizing ──────────────────────────────────────────────── */
/* EzLogo renders two <img> elements (light + dark). These rules are global because
   child-component output gets no scoped-CSS attribute. The admin-configured height arrives
   as the --ez-logo-max-h custom property set by EzLogo on its anchor; here we keep the
   logos responsive and aspect-correct. */
.brand-logo {
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: var(--ez-logo-max-h, 2.5rem);
}

/* Show only the image matching the active colour mode (the [data-bs-theme] attribute is set
   on <html> by the theme switcher, and forced to "dark" on the auth hero). */
.brand-logo--light { display: block; }
.brand-logo--dark  { display: none; }
[data-bs-theme="dark"] .brand-logo--light { display: none; }
[data-bs-theme="dark"] .brand-logo--dark  { display: block; }

/* Bootstrap's .btn-close is a black SVG; invert it in dark mode so the modal X stays
   visible. Kept explicitly even though Bootstrap also inverts it under [data-bs-theme=dark]. */
[data-bs-theme="dark"] .btn-close {
    filter: invert(1) grayscale(100%) brightness(200%);
}

/* Sidebar brand row: push the mobile close button to the far right (replaces the old flex:1). */
.brand-wordmark {
    min-width: 0;
    margin-right: auto;
    object-fit: contain;
    object-position: left center;
}

/* Logo upload previews on the admin settings page - each on its matching backdrop. */
.logo-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 5.25rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius, 0.375rem);
}
.logo-preview img { max-height: 3.75rem; width: auto; max-width: 100%; }
.logo-preview--light { background: #ffffff; }
.logo-preview--dark  { background: #0c1a2e; }

/* ── App shell (flex-based sidebar layout) ──────────────────── */

/* App shell. Base = the full-bleed mobile surface: edge-to-edge with no card border/margin, and
   transparent so the page ground (--bs-tertiary-bg = #09090b, the marketing site's ground colour)
   shows through the transparent header and behind the #141416 content cards - exactly the site's
   surface. The boxed card (centred max-width, border, radius, outer gutter) is added only at >=xl
   in the min-width block below, where the persistent side-menu appears. display:flex and
   overflow:hidden are global: flex drives the sidebar+main row, and overflow:hidden guards against
   stray horizontal overflow on mobile and clips the rounded card corners on desktop. */
.layout {
    display: flex;
    min-height: 100vh;
    overflow: hidden;
}

/* Google's PlaceAutocompleteElement (address autocomplete) renders its suggestion list
   inside the component's shadow DOM, absolutely positioned - so any overflow-clipping
   ancestor cuts the list off. EzActionMenu escapes clipping via CSS anchor positioning,
   but that cannot reach inside a third-party shadow root; instead, un-clip the shell
   only while it actually hosts an address autocomplete. */
.layout:has(gmp-place-autocomplete) { overflow: visible; }

/* At >=xl the app becomes a boxed shell: a centred card (border, radius, outer gutter) sitting
   next to the persistent side-menu. Below xl none of this applies, so the layout is full-bleed. */
@media (min-width: 1200px) {
    .layout {
        min-height: calc(100vh - var(--app-gutter-y, 0) * 2);
        max-width: min(var(--app-max-w), calc(100vw - var(--app-gutter) * 2));
        margin-inline: auto;
        margin-block: var(--app-gutter-y, 0);
        margin-bottom: 2rem;
        background: var(--bs-body-bg);
        border: 1px solid var(--bs-border-color);
        border-radius: var(--bs-border-radius-xl, 0);
    }
}

/* Main content column.

   Bottom padding = the surface's own gutter PLUS a shared 1.5rem (Bootstrap spacer 4) of breathing
   room, so a page's last card never sits hard against the bottom edge of the screen. The extra rides
   a custom property rather than being baked into each breakpoint's value so a page can opt out in
   ONE place: add .ez-page-flush-bottom to the page root (see the rule under the media blocks below).
   Opting out is for full-bleed interactive surfaces whose own layout already reaches the bottom -
   VCR's styling studios are the only ones today. */
.main {
    flex: 1;
    min-width: 0;
    padding: 0.875rem 1.5rem calc(1.75rem + var(--ez-page-bottom-extra, 1.5rem));
}

/* Page content area */
.page { margin-top: 1rem; }

/* Page header */
.page-header {
    display: flex;
    align-items: center;
    background: var(--bs-body-bg);
    /* border: 1px solid var(--bs-border-color); */
    /* border-radius: var(--bs-border-radius-lg); */
    /* padding: 0.625rem; */
    /* min-height: 3.875rem; */
    margin-bottom: 0.875rem;
    /* Named so the phone notification banner can drop in just BELOW the header instead of over its
       bell and credit badge (see .notif-banner). */
    anchor-name: --ez-page-header;
}

/* Page title / breadcrumb row */
.page-title {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.3rem;
    flex: 1;
    min-width: 0;
    overflow: hidden;
    /* padding: 0 0.5rem; */
    margin-right: 0.5rem;
}
/* EzBreadcrumb renders h1 as a direct flex child of .page-title.
   Typography comes from .ez-title-1 on the h1; this rule keeps only
   the flex/truncation behavior. */
.page-title h1 {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1;
}
/* Underline only the label text, not the arrow icon or the gap between them
   (a default anchor underline spans every inline descendant, including whitespace). */
.breadcrumb-back { text-decoration: none; }
.breadcrumb-back span { text-decoration: underline; }

/* Brand logo shown in the header on mobile/tablet only (see LayoutHeader). No fixed cap: it uses
   the app's configured (sidebar) logo height via .brand-logo's --ez-logo-max-h, so the app's
   mobile header logo matches the marketing site's header logo. */
/* Allowed to shrink: `flex: none` pinned the logo at its intrinsic width, so at 320px there was
   no room left for the header actions and they wrapped onto a second line under a 120px bar.
   flex-shrink plus min-width: 0 lets the box narrow, and .brand-logo's max-width: 100% then
   scales the image with it (height: auto keeps the aspect ratio). */
.header-logo { display: inline-flex; align-items: center; flex: 0 1 auto; min-width: 0; }

/* Responsive: collapse sidebar and adjust layout - Bootstrap < xl */
@media (max-width: 1199.98px) {
    /* Content gutter: match the marketing site (its .vm-shell uses 1.25rem) - the single source
       of truth for the app's mobile side padding. No top padding so the header bar sits flush at
       the top like the site's sticky nav. */
    .main { padding: 0 1.25rem calc(1.5rem + var(--ez-page-bottom-extra, 1.5rem)); }

    /* Mobile/tablet header = an EXACT clone of the marketing site's nav bar (source of truth):
       a borderless, card-less row (hamburger + logo + actions) that is 7.5rem tall with its
       content vertically centred, a 1.5rem hamburger<->logo gap, and a divider flush at the bar's
       bottom. Only the actions differ from the site (credit badge / bell / avatar vs its CTA).
       - column-gap 1.5rem = the site's .vm-nav-inner gap; row-gap 0 keeps the divider flush.
       - padding 0: the row content aligns to .main's 1.25rem gutter, like the site's .vm-shell. */
    .page-header {
        flex-wrap: wrap;
        align-items: center;
        background: transparent;
        border: 0;
        border-radius: 0;
        padding: 0;
        min-height: 0;
        column-gap: 1.5rem;
        row-gap: 0;
    }
    /* The logo carries the bar's 7.5rem height (matching the site's .vm-nav-inner min-height): it
       makes the first flex line 7.5rem tall and, being inline-flex + centred, sits the 70px logo
       dead centre - exactly the site's bar. Doing it here (not on .page-header) keeps the wrapped
       breadcrumb line OUT of the bar height, so the bar is a true fixed-height row like the site. */
    .header-logo { min-height: 7.5rem; }
    /* Breadcrumb / page title drops onto its own line under the bar, with the header's rule along its
       top edge. The row (and so the rule) runs edge to edge: the negative inline margin cancels .main's
       1.25rem gutter and the matching padding puts the heading text back on it. */
    .page-title {
        flex-basis: 100%;
        order: 5;
        position: relative;
        /* The 7.5rem bar is taller than the logo's ink, which leaves dead space at its bottom.
           Pulling this row up by --ez-header-rule-lift puts the rule against the ink. VCR's studio
           pages pull their step strip up by the same token, so the two land on the same pixel. */
        margin: calc(var(--ez-header-rule-lift) * -1) -1.25rem 0;
        padding: 0.5rem 1.25rem 0;
    }
    /* ONE solid rule in the faded brand colour, drawn like one segment of the VCR studio's step strip
       (.st-progress-bar: 3px, primary at 0.22 alpha), so the line reads the same on every page and the
       studio's three segments look like this rule split into steps. Square ends: it meets the screen
       edges. A pseudo-element rather than a border so nothing in the row's box or spacing changes. */
    .page-title::before {
        content: "";
        position: absolute;
        inset: 0 0 auto;
        height: 3px;
        background: rgba(var(--bs-primary-rgb), 0.22);
        pointer-events: none;
    }
}

/* Bootstrap < md */
@media (max-width: 767.98px) {
    .page { margin-top: 1.125rem; }
}

/* Bootstrap < sm: the header row has to hold [hamburger] [logo] [credits "9,999"] [bell] [avatar] on
   one line down to the 320px floor in docs/standards/accessibility.md. The marketing site's 1.5rem
   hamburger<->logo gap is a desktop-width luxury here; half of it still separates the two. */
@media (max-width: 575.98px) {
    .page-header { column-gap: 0.75rem; }
}

/* Custom - not a Bootstrap breakpoint: the 320-359px phones (the accessibility floor) are the only
   widths where the full-height logo mark (~4rem wide) plus the four controls overflow the row. The
   mark steps down there, and only there, so every common phone (360px+) keeps the full-size brand.
   2.5rem is the measured ceiling: at 2.75rem the row is 2px too wide at 320px and the actions wrap
   onto a second line under the logo. */
@media (max-width: 359.98px) {
    .header-logo .brand-logo { --ez-logo-max-h: 2.5rem; }
}

/* Phone in landscape: wide but very short (a 410px-tall viewport is typical). The 7.5rem logo bar
   is sized to match the marketing site's nav, which is ~29% of the screen here - so the branding is
   scaled down for this orientation only, giving the page back roughly 64px of the little height it
   has. Keyed on height + orientation, NOT width, so a genuine tablet or desktop keeps the full bar. */
@media (orientation: landscape) and (max-height: 32rem) {
    /* 3rem: the wordmark read as a footnote at 2.25rem (36px - smaller than the controls beside it). */
    .header-logo { min-height: 4rem; }
    .header-logo .brand-logo { --ez-logo-max-h: 3rem; }
    /* The rule's lift is calibrated to the tall bar's dead space; the short bar has only 0.5rem under
       the 3rem logo, and the tall bar's 0.8125rem would draw the rule through the logo. */
    :root { --ez-header-rule-lift: 0.25rem; }
}

/* Bootstrap .btn is display:inline-block with no gap - override to flex so
   icon + label spacing is intentional, not dependent on whitespace text nodes.
   Value matches gap-2 (0.5rem) for consistency with the rest of the UI.
   Centring, height and the leading icon/spinner slot live in ez-platform-buttons.css
   alongside the .btn-ez-* tiers. */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    position: relative;
    overflow: visible;
}

/* .ez-btn-basic is gone. It existed to mark <button> elements that could not be an
   <EzButton> because they carry complex child content - the user-menu toggle rows and
   the user card. EzButton takes ChildContent now, so there is nothing left to mark and
   no second set of button metrics to keep in step with the first. */

/* Bootstrap < sm */
@media (max-width: 575.98px) {
    /* Edge-to-edge is already applied from the < xl block above; here only the bottom padding
       tightens on phones. The borderless header-bar + full-bleed divider rules carry through. */
    .main { padding: 0 1.25rem calc(1.25rem + var(--ez-page-bottom-extra, 1.5rem)); }
    .page-title { gap: 0.375rem; }
    .page-title h1 { font-size: 1.125rem; }
}

/* Opt out of the shared bottom breathing room (see .main above). One declaration covers every
   breakpoint because each one adds the same custom property. Declared after the media blocks so it
   is not tangled up in them - the property is read by .main's own padding at whatever width. */
.main:has(.ez-page-flush-bottom) { --ez-page-bottom-extra: 0rem; }

/* ── Component exceptions: things Bootstrap cannot express natively ──── */

/* EzDragHandle: the six-dot grip marking the draggable spot on a sortable row.
   Lifted from the form builder, which had the original treatment - deliberately understated, so
   the row reads as content with one grabbable corner rather than as a control. The grab/grabbing
   cursor is the whole point of the component, and it is also why the handle is a focusable div
   rather than a <button>: Bootstrap's reboot sets cursor:pointer on button at a specificity the
   grab cursor cannot beat. */
.ez-drag-handle {
    display: inline-flex;
    align-items: center;
    padding: 0;
    line-height: 1;
    font-size: 0.8125rem;
    /* --bs-secondary-color, not --bs-border-color: the border token is ~1.6:1 against the row in
       dark mode, so the grip was effectively invisible and the only thing that said a row could be
       reordered. A control's visual boundary needs 3:1 (WCAG 1.4.11). */
    color: var(--bs-secondary-color);
    cursor: grab;
}
/* While dragging: SortableJS marks the row .sortable-chosen, the form builder canvas marks it
   .fb-field--chosen. Both need the cursor to follow the pointer, not just the handle. */
.ez-drag-handle:active,
.sortable-chosen .ez-drag-handle,
.fb-field--chosen .ez-drag-handle { cursor: grabbing; }

/* EzButtonBadge: count badge overlay on icon buttons
   Uses Bootstrap badge classes (text-bg-*) for color variants.
   .btn prefix + !important to win over Bootstrap .badge layout. */
.btn .ez-btn-badge,
.ez-btn-badge {
    position: absolute !important;
    top: 0.125rem !important;
    right: 0.125rem !important;
    left: auto !important;
    bottom: auto !important;
    min-width: 0.875rem !important;
    height: 0.875rem !important;
    padding: 0 0.1875rem !important;
    /* Pill, not a tier: the badge grows wider than tall as the count goes 9 -> 99, and the
       old 0.4375rem was just half of the height above, restated by hand. */
    border-radius: var(--bs-border-radius-pill) !important;
    font-size: 0.5625rem !important;
    font-weight: 700;
    line-height: 0.875rem !important;
    text-align: center;
    pointer-events: none;
    z-index: 10;
}

/* EzCard: remove header/footer tinted background and internal dividers.
   Bootstrap defines --bs-card-cap-bg on .card itself, not :root, so the
   inherited token override above has no effect. Must override here. */
.card { --bs-card-cap-bg: transparent; --bs-card-border-color: var(--bs-border-color); }
/* No divider line between cap and body, so the header's bottom padding and the body's
   top padding (likewise body-bottom + footer-top) would stack into a doubled gap.
   Zero the inner cap padding and let the body's spacer provide a single, consistent
   1rem rhythm above and below the body content. */
.card-header { border-bottom: 0; padding-top: var(--bs-card-spacer-y); padding-bottom: 0; }
.card-footer { border-top: 0; padding-top: 0; padding-bottom: var(--bs-card-spacer-y); }

/* Three-level heading scale. Role-based (not tied to h1-h6), so a page always shows
   a clear title -> card/table title -> in-card sub-section hierarchy instead of every
   level rendering at the same size. Variant cards keep their text-* colour, which wins
   via Bootstrap's !important utilities. */

/* Level 1: page title (EzPageTop / EzBreadcrumb h1). One per page, the largest. */
.ez-title-1 {
    margin-bottom: 0;
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: -0.05em;
    text-transform: uppercase;
    color: var(--bs-emphasis-color);
}

/* Level 2: card / table panel title (EzCard, EzTable). One step down from the page
   title so a card never competes with it. */
.ez-title-2 {
    margin-bottom: 0;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    text-transform: uppercase;
    color: var(--bs-primary);
}

/* Level 3: in-card sub-group heading - a labelled cluster of fields nested inside a
   Level 1 section. Rendered by EzCardSection with Level="2" (see docs/agents/ui-razor.md).
   No uppercase - signals "still inside the same card/panel", not a new panel of its own. */
.ez-title-3 {
    margin-bottom: 0;
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--bs-emphasis-color);
}

/* EzCardSection spacing - the whitespace that separates one form section from the
   previous one. Owned here rather than by padding/margin utilities in the component so a
   single rhythm governs every form. Fields inside a section are already 1rem apart
   (each field's own mb-3); when a section break was worth roughly the same, a long
   settings form read as one undifferentiated stack of inputs with headings dropped in.

   A Level 1 section stacks its gap ABOVE the divider rule (on top of the preceding
   field's mb-3, so ~2.5rem of clear space before the line) and repeats it below, so the
   heading is never crowded by the rule. Level 2 is a sub-group inside a section, so it
   takes the smaller gap - still comfortably past 1rem once the preceding field's mb-3 is
   counted, but visibly tighter than a full section break, which is what keeps the two
   levels readable as a hierarchy. The panel variant has no rule and takes the gap as a
   plain top margin. A section that opens its card has nothing above it to separate from,
   so it drops the leading space entirely. */
:root {
    --ez-card-section-gap: 1.5rem;
    --ez-card-section-gap-sub: 1rem;
}

.ez-card-section { margin-top: var(--ez-card-section-gap); }
.ez-card-section.border-top { padding-top: var(--ez-card-section-gap); }
.ez-card-section-sub { margin-top: var(--ez-card-section-gap-sub); }
.ez-card-section:first-child { margin-top: 0; }

/* btn-outline-secondary uses Bootstrap's defaults - secondary
   border and text. Do not re-add a card-border override; the search box border and icon
   are matched to this via var(--bs-secondary). */

/* The pressed/expanded state moved to .btn-ez-outline[aria-pressed] in
   ez-platform-buttons.css, and became a subtle fill rather than a solid brand one - a
   loud fill shouted beside the quiet neighbours it now sits with. Nothing renders bare
   .btn-outline-secondary any more, so the old rule is gone rather than left to rot. */

/* btn-primary / btn-outline-primary follow the brand token. Bootstrap bakes a literal
   #0d6efd into these classes, so without this override solid primary buttons stay blue
   even though --bs-primary is the lavender brand. Solid fill uses white text (consistent
   with the other solid variants); hover/active darken the fill via color-mix. Outline
   text matches its border (the brand colour) and fills to the brand with white text on
   hover/active. The other solid variants (secondary/success/warning/danger/info) need no
   override - their tokens already equal Bootstrap's defaults. */
.btn-primary {
    --bs-btn-bg:                 var(--bs-primary);
    --bs-btn-border-color:       var(--bs-primary);
    --bs-btn-color:              var(--ez-on-primary);
    --bs-btn-hover-bg:           color-mix(in srgb, var(--bs-primary) 88%, #000);
    --bs-btn-hover-border-color: color-mix(in srgb, var(--bs-primary) 85%, #000);
    --bs-btn-hover-color:        var(--ez-on-primary);
    --bs-btn-active-bg:          color-mix(in srgb, var(--bs-primary) 80%, #000);
    --bs-btn-active-border-color:color-mix(in srgb, var(--bs-primary) 78%, #000);
    --bs-btn-active-color:       var(--ez-on-primary);
    --bs-btn-disabled-bg:        var(--bs-primary);
    --bs-btn-disabled-border-color: var(--bs-primary);
    --bs-btn-disabled-color:     var(--ez-on-primary);
}
.btn-outline-primary {
    --bs-btn-color:              var(--bs-primary);
    --bs-btn-border-color:       var(--bs-primary);
    --bs-btn-hover-bg:           var(--bs-primary);
    --bs-btn-hover-border-color: var(--bs-primary);
    --bs-btn-hover-color:        var(--ez-on-primary);
    --bs-btn-active-bg:          var(--bs-primary);
    --bs-btn-active-border-color:var(--bs-primary);
    --bs-btn-active-color:       var(--ez-on-primary);
}

/* Bootstrap compiles its stock blue (#0d6efd) into these component tokens rather than reading
   --bs-primary, so every theme - the original included - showed blue filter pills and menu highlights.
   Re-point them at the theme's primary and its ink. */
.nav-pills {
    --bs-nav-pills-link-active-bg:    var(--bs-primary);
    --bs-nav-pills-link-active-color: var(--ez-on-primary);
}
.dropdown-menu {
    --bs-dropdown-link-active-bg:    var(--bs-primary);
    --bs-dropdown-link-active-color: var(--ez-on-primary);
}
.list-group {
    --bs-list-group-active-bg:           var(--bs-primary);
    --bs-list-group-active-border-color: var(--bs-primary);
    --bs-list-group-active-color:        var(--ez-on-primary);
}

/* Bootstrap 5.3 compiles fixed light fills into its table variants and .text-bg-light, so they glare in
   dark mode and ignore the theme. Rebuild them from the theme's surfaces and Bootstrap's own mode-aware
   subtle tokens (--bs-{status}-bg-subtle / -text-emphasis), which do follow dark mode. */
.table-light {
    --bs-table-color: var(--bs-body-color);
    --bs-table-bg: var(--bs-secondary-bg);
    --bs-table-border-color: var(--bs-border-color);
    --bs-table-striped-bg: color-mix(in srgb, var(--bs-body-color) 5%, var(--bs-secondary-bg));
    --bs-table-striped-color: var(--bs-body-color);
    --bs-table-active-bg: color-mix(in srgb, var(--bs-body-color) 10%, var(--bs-secondary-bg));
    --bs-table-active-color: var(--bs-body-color);
    --bs-table-hover-bg: color-mix(in srgb, var(--bs-body-color) 7.5%, var(--bs-secondary-bg));
    --bs-table-hover-color: var(--bs-body-color);
    color: var(--bs-table-color);
    border-color: var(--bs-table-border-color);
}
.table-primary   { --ez-table-bg: var(--bs-primary-bg-subtle);   --ez-table-fg: var(--bs-primary-text-emphasis);   --ez-table-line: var(--bs-primary-border-subtle); }
.table-secondary { --ez-table-bg: var(--bs-secondary-bg-subtle); --ez-table-fg: var(--bs-secondary-text-emphasis); --ez-table-line: var(--bs-secondary-border-subtle); }
.table-success   { --ez-table-bg: var(--bs-success-bg-subtle);   --ez-table-fg: var(--bs-success-text-emphasis);   --ez-table-line: var(--bs-success-border-subtle); }
.table-danger    { --ez-table-bg: var(--bs-danger-bg-subtle);    --ez-table-fg: var(--bs-danger-text-emphasis);    --ez-table-line: var(--bs-danger-border-subtle); }
.table-warning   { --ez-table-bg: var(--bs-warning-bg-subtle);   --ez-table-fg: var(--bs-warning-text-emphasis);   --ez-table-line: var(--bs-warning-border-subtle); }
.table-info      { --ez-table-bg: var(--bs-info-bg-subtle);      --ez-table-fg: var(--bs-info-text-emphasis);      --ez-table-line: var(--bs-info-border-subtle); }
:is(.table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info) {
    --bs-table-color: var(--ez-table-fg);
    --bs-table-bg: var(--ez-table-bg);
    --bs-table-border-color: var(--ez-table-line);
    --bs-table-striped-bg: color-mix(in srgb, var(--ez-table-fg) 5%, var(--ez-table-bg));
    --bs-table-striped-color: var(--ez-table-fg);
    --bs-table-active-bg: color-mix(in srgb, var(--ez-table-fg) 10%, var(--ez-table-bg));
    --bs-table-active-color: var(--ez-table-fg);
    --bs-table-hover-bg: color-mix(in srgb, var(--ez-table-fg) 7.5%, var(--ez-table-bg));
    --bs-table-hover-color: var(--ez-table-fg);
    color: var(--bs-table-color);
    border-color: var(--bs-table-border-color);
}
.text-bg-light {
    color: var(--bs-body-color) !important;
    background-color: var(--bs-secondary-bg) !important;
}

/* The remaining stock-blue states: Bootstrap writes #0d6efd / #86b7fe / rgba(13,110,253,.25) as
   literals for these, which no token reaches. The reversible form theme ([data-theme-forms="on"])
   restyles the form controls more fully and wins over these; these keep them on-brand when it is off. */
.form-check-input:checked,
.form-check-input:indeterminate {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
}
.form-control:focus,
.form-select:focus,
.form-check-input:focus {
    border-color: color-mix(in srgb, var(--bs-primary) 50%, var(--bs-body-bg));
    box-shadow: var(--ez-focus-ring);
}
.form-switch .form-check-input:focus:not(:checked) {
    /* The knob turns stock light blue on focus; keep it the resting grey (the ring shows focus). */
    --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
}
[data-bs-theme="dark"] .form-switch .form-check-input:focus:not(:checked) {
    --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e");
}
.btn-close { --bs-btn-close-focus-shadow: var(--ez-focus-ring); }
.nav-link:focus-visible { box-shadow: var(--ez-focus-ring); }

/* Form hint text: italic to visually distinguish from field labels */
.form-text { font-style: italic; }

/* EzPagination: map Bootstrap pagination tokens to design tokens */
.pagination {
    --bs-pagination-color:                 var(--bs-tertiary-color);
    --bs-pagination-bg:                    var(--bs-body-bg);
    --bs-pagination-border-color:          var(--bs-border-color);
    --bs-pagination-hover-color:           var(--bs-primary);
    --bs-pagination-hover-bg:              rgba(var(--bs-primary-rgb), .08);
    --bs-pagination-hover-border-color:    var(--bs-border-color);
    --bs-pagination-focus-color:           var(--bs-primary);
    --bs-pagination-focus-bg:              rgba(var(--bs-primary-rgb), .10);
    --bs-pagination-focus-box-shadow:      var(--ez-focus-ring);
    --bs-pagination-active-color:          var(--bs-primary);
    --bs-pagination-active-bg:             rgba(var(--bs-primary-rgb), .12);
    --bs-pagination-active-border-color:   var(--bs-primary);
    --bs-pagination-disabled-color:        var(--bs-secondary-color);
    --bs-pagination-disabled-bg:           var(--bs-body-bg);
    --bs-pagination-disabled-border-color: var(--bs-border-color);
}

/* EzPagination: hide browser spinners on the jump-to-page number input */
#ez-jump-page::-webkit-inner-spin-button,
#ez-jump-page::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
#ez-jump-page[type=number] { -moz-appearance: textfield; }

/* ── Accordion (Bootstrap .accordion) ─────────────────────────────────────
   Bootstrap's accordion is the one common component that is still hard-wired to
   ITS defaults in three places, so a rebranded theme leaves it looking foreign:
   the focus ring is a literal blue rgba(13,110,253,.25), and BOTH chevrons are
   SVG data URIs with baked-in hex fills (#212529 / #052c65 in light, #6ea8fe in
   the [data-bs-theme=dark] override). Everything else is already token-driven -
   including the expanded header, which rides --bs-primary-bg-subtle /
   --bs-primary-text-emphasis and therefore follows the brand automatically
   because those two are derived from --bs-primary at the top of this file.
   The mapping below finishes the job: brand focus ring and the compact padding
   the 0.875rem body font asks for. */
.accordion {
    --bs-accordion-color:               var(--bs-body-color);
    --bs-accordion-bg:                  var(--bs-body-bg);
    --bs-accordion-border-color:        var(--bs-border-color);
    /* Radius is deliberately not restated here: Bootstrap already derives
       --bs-accordion-border-radius (and the inner one) from --bs-border-radius,
       so the accordion tracks the platform corner for free. */
    --bs-accordion-btn-color:           var(--bs-body-color);
    /* Header and body share the card's 1rem horizontal rhythm; the header is a touch
       tighter vertically so a closed accordion reads as a control, not a panel. */
    --bs-accordion-btn-padding-x:        1rem;
    --bs-accordion-btn-padding-y:        0.75rem;
    --bs-accordion-body-padding-x:       1rem;
    --bs-accordion-body-padding-y:       1rem;
    --bs-accordion-btn-focus-box-shadow: var(--ez-focus-ring);
    --bs-accordion-active-color:         var(--bs-primary-text-emphasis);
    --bs-accordion-active-bg:            var(--bs-primary-bg-subtle);
}

/* The header is a heading, not body copy - Bootstrap leaves it at the inherited weight. */
.accordion-button { font-weight: 600; }

/* Hover affordance (Bootstrap gives the button no hover state at all). Closed lifts off
   the card with the same faint brand wash used by ghost buttons and pagination; open
   deepens its existing subtle tint rather than fighting it. */
.accordion-button.collapsed:hover { background-color: rgba(var(--bs-primary-rgb), .06); }
.accordion-button:not(.collapsed):hover {
    background-color: color-mix(in srgb, var(--bs-primary) 20%, var(--bs-accordion-bg));
}

/* Chevron: drop Bootstrap's two hard-coded SVG fills and paint the glyph with
   currentColor through a mask instead. One declaration then covers every case -
   light, dark, closed (body colour) and open (brand emphasis colour) - and a
   rebrand needs no new icon. background-image is zeroed on BOTH selectors because
   .accordion-button:not(.collapsed)::after sets it at higher specificity, and the
   dark-mode override re-points the icon variables on the pseudo-element itself.
   The rotate-on-open transform is Bootstrap's and still applies. */
.accordion-button::after,
.accordion-button:not(.collapsed)::after {
    background-image: none;
    background-color: currentColor;
    -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M3 6L8 11L13 6'/%3e%3c/svg%3e");
            mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M3 6L8 11L13 6'/%3e%3c/svg%3e");
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: center;
            mask-position: center;
    -webkit-mask-size: var(--bs-accordion-btn-icon-width);
            mask-size: var(--bs-accordion-btn-icon-width);
}

/* ── EzAnnouncementBanner ───────────────────────────────────── */
/* z-index above all content; Bootstrap utilities only reach z-3 */
.annc-group { position: relative; z-index: 1045; isolation: isolate; }
/* Clickable strip: pointer cursor and hover darkening (no Bootstrap utility) */
.annc--linked { cursor: pointer; }
.annc--linked:hover { filter: brightness(1.06); }
/* Fixed-colour themes with no Bootstrap text-bg-* equivalent (beta rides the brand primary) */
.annc--celebrate   { background-color: #be185d; }
.annc--new-feature { background-color: #059669; }
.annc--beta        { background-color: var(--bs-primary); color: var(--ez-on-primary); }
/* Maintenance: muted panel that adapts to light and dark themes */
.annc--maintenance { background-color: var(--bs-secondary-bg); color: var(--bs-body-color); border-bottom-color: var(--bs-border-color); }
/* Body: wide enough for one sentence on one line on a desktop; min-width 0 lets it shrink
   beside the icon and arrow in the one-row phone layout instead of pushing them off the edge.
   Alignment comes from the component's utilities (left on a phone, centred from md up). */
.annc-body { max-width: 90ch; min-width: 0; font-size: 0.9375rem; line-height: 1.4; }
@media (max-width: 767.98px) { .annc-body { font-size: 0.875rem; } }
.annc-body :is(p, ul, ol) { margin: 0; }
/* Linked strip's arrow: a round tint of the strip's own text colour, so it reads on every theme,
   light or dark, and slides forward while the strip is hovered. */
.annc-go {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    color: inherit;
    font-size: 0.8125rem;
    background: color-mix(in srgb, currentColor 18%, transparent);
    transition: transform 0.15s ease, background-color 0.15s ease;
}
.annc--linked:hover .annc-go { transform: translateX(3px); background: color-mix(in srgb, currentColor 28%, transparent); }
.annc-go:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .annc-go { transition: none; } .annc--linked:hover .annc-go { transform: none; } }
.annc-body a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.annc-body strong, .annc-body b { font-weight: 700; }
.annc-body code { padding: 0.1em 0.35em; background: rgba(255,255,255,.22); border-radius: var(--bs-border-radius-sm); }
/* Announcement dismiss uses btn-close btn-close-white (Bootstrap default close button). */

/* Strip is the positioning context for the stretched link, the hazard edge and the progress
   rail; the dismiss control sits above the stretched link so it stays clickable. */
.annc { position: relative; }
.annc-dismiss { position: relative; z-index: 2; }

/* ── Announcement designs (themes 10+) ──────────────────────
   Graduated from /admin/ui/custom/announcement-banner-mockups. Gradient
   fills and self-dark stages fix their hues on purpose and carry a dark
   override. Ink on the primary fill is the theme's --ez-on-primary (it was a
   fixed near-black violet, unreadable once a theme's primary is dark). The strip's bottom hairline is
   recoloured through --bs-border-color, since .border-bottom is !important. */

/* Shared parts */
.annc-chip {
    flex-shrink: 0;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    border-radius: var(--bs-border-radius-pill);
    padding: 0.25rem 0.5rem;
}
.annc-label { font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
.annc-eyebrow {
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--bs-primary-text-emphasis);
}
.annc-cta { flex-shrink: 0; color: inherit; font-weight: 600; white-space: nowrap; text-underline-offset: 2px; }
.annc-cta--pill {
    text-decoration: none;
    border: 1px solid currentColor;
    border-radius: var(--bs-border-radius-pill);
    padding: 0.25rem 1rem;
}
.annc-medallion,
.annc-tile {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.annc-medallion {
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 50%;
    background: var(--bs-primary-bg-subtle);
    border: 1px solid var(--bs-primary-border-subtle);
    color: var(--bs-primary-text-emphasis);
    font-size: 1.125rem;
}
.annc-tile {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--bs-border-radius);
    background: var(--bs-primary);
    color: var(--ez-on-primary);
    font-size: 1rem;
}
.annc-count {
    font-family: var(--bs-font-monospace);
    font-size: 0.8125rem;
    font-weight: 600;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: var(--bs-border-radius-sm);
    padding: 0.25rem 0.5rem;
    white-space: nowrap;
}
.annc-count small {
    font-family: var(--bs-body-font-family);
    font-size: 0.625rem;
    letter-spacing: 0.08em;
    opacity: 0.6;
}
.annc-progress {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 0.25rem;
    border: 0;
    -webkit-appearance: none;
            appearance: none;
    background: rgba(var(--bs-warning-rgb), 0.2);
}
.annc-progress::-webkit-progress-bar { background: transparent; }
.annc-progress::-webkit-progress-value { background: var(--bs-warning); }
.annc-progress::-moz-progress-bar { background: var(--bs-warning); }
.annc-pulse {
    position: relative;
    width: 0.5rem;
    height: 0.5rem;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--bs-success);
}
.annc-pulse::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: rgba(var(--bs-success-rgb), 0.5);
    animation: annc-pulse 2s ease-out infinite;
}
@keyframes annc-pulse {
    from { transform: scale(1); opacity: 0.9; }
    to   { transform: scale(3); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .annc-pulse::after { animation: none; }
}

/* 10 Brand Ribbon: solid brand strip, dark violet ink. Stays light in dark
   mode, so undo the dark-mode btn-close inversion. */
.annc--brand-ribbon { background: var(--bs-primary); color: var(--ez-on-primary); }
[data-bs-theme="dark"] .annc--brand-ribbon .btn-close { filter: none; }

/* 11 Soft Tint: brand-subtle surface, fully token-driven. */
.annc--soft-tint {
    --bs-border-color: var(--bs-primary-border-subtle);
    background: var(--bs-primary-bg-subtle);
    color: var(--bs-body-color);
}
.annc--soft-tint :is(.annc-icon, .annc-body a) { color: var(--bs-primary-text-emphasis); }

/* 12 Violet Gradient: purple sweep with a pill CTA. */
.annc--violet-gradient { background: linear-gradient(90deg, #6d28d9, #a855f7); color: #fff; }
.annc--violet-gradient .annc-cta--pill { color: #fff; border-color: rgba(255, 255, 255, 0.45); }
.annc--violet-gradient .annc-cta--pill:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
[data-bs-theme="dark"] .annc--violet-gradient { background: linear-gradient(90deg, #5b21b6, #9333ea); }

/* 13 Aurora Wash: alpha glows over the body surface (pink has no token). */
.annc--aurora-wash {
    color: var(--bs-body-color);
    background:
        radial-gradient(60rem 9rem at 12% 0%, rgba(var(--bs-primary-rgb), 0.20), transparent 60%),
        radial-gradient(50rem 9rem at 55% 100%, rgba(var(--bs-info-rgb), 0.12), transparent 60%),
        radial-gradient(40rem 9rem at 92% 0%, rgba(236, 72, 153, 0.14), transparent 60%),
        var(--bs-body-bg);
}
.annc--aurora-wash .annc-body a { color: var(--bs-link-color); }
.annc--aurora-wash .annc-chip,
.annc--neon-outline .annc-chip { color: var(--bs-primary-text-emphasis); }
.annc--aurora-wash .annc-chip { background: rgba(var(--bs-primary-rgb), 0.18); }

/* 14 Hairline Editorial: newsprint small caps, no fill. */
.annc--hairline-editorial {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    border-top: 1px solid var(--bs-border-color);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.annc--hairline-editorial .annc-body a { color: var(--bs-primary-text-emphasis); }

/* 15 Label Split: solid brand label fused to a quiet message. */
.annc--label-split { background: var(--bs-body-bg); color: var(--bs-body-color); }
.annc--label-split .annc-label { background: var(--bs-primary); color: var(--ez-on-primary); }
.annc--label-split .annc-body a { color: var(--bs-link-color); }

/* 16 Icon Medallion: circular brand medallion, pill CTA. */
.annc--icon-medallion { background: var(--bs-body-bg); color: var(--bs-body-color); }
.annc--icon-medallion .annc-cta--pill { color: var(--bs-primary-text-emphasis); border-color: var(--bs-primary-border-subtle); }
.annc--icon-medallion .annc-cta--pill:hover { background: var(--bs-primary-bg-subtle); }

/* 17 Countdown Event: self-dark violet night stage with time chips. */
.annc--countdown-event {
    --bs-border-color: rgba(255, 255, 255, 0.08);
    background: linear-gradient(100deg, #17102b, #241640);
    color: #fff;
}
.annc--countdown-event :is(.annc-body a, .annc-cta) { color: var(--bs-primary); }

/* 18 Progress Maintenance: amber status strip with a progress rail. */
.annc--progress-maintenance,
.annc--amber-hazard {
    --bs-border-color: var(--bs-warning-border-subtle);
    background: var(--bs-warning-bg-subtle);
    color: var(--bs-warning-text-emphasis);
}

/* 19 Split CTA: quiet upsell, message left, button right. */
.annc--split-cta,
.annc--action-bar { background: var(--bs-body-bg); color: var(--bs-body-color); }
.annc--split-cta .annc-icon,
.annc--action-bar .annc-icon { color: var(--bs-primary-text-emphasis); }

/* 20 Frosted Glass: translucent strip that blurs what sits beneath. */
.annc--frosted-glass {
    background: rgba(var(--bs-body-bg-rgb), 0.55);
    -webkit-backdrop-filter: blur(12px) saturate(160%);
            backdrop-filter: blur(12px) saturate(160%);
    color: var(--bs-body-color);
}

/* 21 Neon Outline: brand keyline and a whisper of brand fill. */
.annc--neon-outline {
    --bs-border-color: color-mix(in srgb, var(--bs-primary) 55%, var(--bs-body-bg));
    border-top: 1px solid var(--bs-border-color);
    background: rgba(var(--bs-primary-rgb), 0.05);
    color: var(--bs-body-color);
}
.annc--neon-outline .annc-chip { border: 1px solid var(--bs-border-color); }
.annc--neon-outline .annc-body a { color: var(--bs-primary-text-emphasis); }

/* 22 Emerald Launch: green gradient for shipped features. */
.annc--emerald-launch { background: linear-gradient(90deg, #047857, #10b981); color: #fff; }
.annc--emerald-launch .annc-chip { background: rgba(255, 255, 255, 0.2); }
[data-bs-theme="dark"] .annc--emerald-launch { background: linear-gradient(90deg, #065f46, #059669); }

/* 23 Amber Hazard: warning strip with a chevron-striped safety edge
   (surface shared with 18 above). */
.annc--amber-hazard::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 0.5rem;
    background: repeating-linear-gradient(
        135deg,
        rgba(var(--bs-warning-rgb), 0.9) 0 0.5rem,
        rgba(var(--bs-warning-rgb), 0.35) 0.5rem 1rem);
}

/* 24 Midnight Promo: near-black stage; <strong> in the body is the
   spectrum offer and <code> the dashed coupon chip. */
.annc--midnight-promo {
    --bs-border-color: rgba(255, 255, 255, 0.1);
    background: #0b0b10;
    color: #e4e4e7;
}
.annc--midnight-promo .annc-body :is(strong, b) {
    font-weight: 800;
    background: linear-gradient(90deg, #f472b6, #af85ff, #22d3ee);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
}
.annc--midnight-promo .annc-body code {
    font-size: 0.8125rem;
    color: #fff;
    background: transparent;
    border: 1px dashed rgba(255, 255, 255, 0.35);
    padding: 0.125rem 0.5rem;
}

/* 25 Pill Float: centred status capsule with a live heartbeat dot. */
.annc--pill-float { background: transparent; color: var(--bs-body-color); }
.annc--pill-float .annc-content {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-pill);
    background: var(--bs-body-bg);
    padding: 0.5rem 1rem;
    font-size: 0.8125rem;
}
.annc--pill-float .annc-body a { color: var(--bs-link-color); }

/* 26 Card Inset: muted surface led by a square brand tile. */
.annc--card-inset { background: var(--bs-secondary-bg); color: var(--bs-body-color); }
.annc--card-inset .annc-body a { color: var(--bs-link-color); }

/* 27 Hero Announce: taller centrepiece under a crown of brand light. */
.annc--hero-announce {
    background:
        radial-gradient(40rem 10rem at 50% -4rem, rgba(var(--bs-primary-rgb), 0.22), transparent 70%),
        var(--bs-body-bg);
    color: var(--bs-body-color);
}
.annc--hero-announce .annc-body { font-size: 1.25rem; font-weight: 600; }
.annc--hero-announce .annc-cta { color: var(--bs-primary-text-emphasis); }
.annc--hero-announce .annc-dismiss { position: absolute; top: 0.5rem; right: 0; }

/* 29 Confetti Celebrate: raspberry sweep dusted with confetti. */
.annc--confetti-celebrate {
    --annc-confetti:
        radial-gradient(circle 0.1875rem at 8% 30%, rgba(255, 255, 255, 0.5) 100%, transparent),
        radial-gradient(circle 0.125rem at 22% 75%, rgba(255, 214, 102, 0.7) 100%, transparent),
        radial-gradient(circle 0.15625rem at 37% 20%, rgba(103, 232, 249, 0.6) 100%, transparent),
        radial-gradient(circle 0.125rem at 52% 80%, rgba(255, 255, 255, 0.45) 100%, transparent),
        radial-gradient(circle 0.1875rem at 66% 25%, rgba(255, 214, 102, 0.6) 100%, transparent),
        radial-gradient(circle 0.125rem at 78% 70%, rgba(103, 232, 249, 0.55) 100%, transparent),
        radial-gradient(circle 0.15625rem at 91% 35%, rgba(255, 255, 255, 0.5) 100%, transparent);
    background-image: var(--annc-confetti), linear-gradient(90deg, #be185d, #db2777);
    color: #fff;
}
[data-bs-theme="dark"] .annc--confetti-celebrate {
    background-image: var(--annc-confetti), linear-gradient(90deg, #9d174d, #be185d);
}

@media (max-width: 767.98px) {
    .annc--hero-announce .annc-body { font-size: 1.0625rem; }
}

/* ── KPI tile (rendered inside EzCard) ─────────────────────── */
/* Value-forward metric tile. Use a standalone tile as:
     <EzCard Variant="success" Class="ez-kpi-card"><div class="ez-kpi">...</div></EzCard>
   or drop the <div class="ez-kpi"> straight into an existing card body.
   The icon colour is driven by the EzCard variant via its border-* class. */
.ez-kpi-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.5rem;
}
.ez-kpi-label { font-size: 0.8125rem; color: var(--bs-secondary-color); }
.ez-kpi-icon  { font-size: 1.125rem; color: var(--bs-secondary-color); flex-shrink: 0; }
.ez-kpi-value {
    margin-top: 0.25rem;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.15;
    color: var(--bs-emphasis-color);
}
.ez-kpi-trend { font-size: 0.75rem; margin-left: 0.35rem; }
.ez-kpi-trend--up   { color: var(--bs-success); }
.ez-kpi-trend--down { color: var(--bs-warning); }
/* Icon tint follows the EzCard variant (border-* class it already emits). */
.ez-kpi-card.border-primary   .ez-kpi-icon { color: var(--bs-primary); }
.ez-kpi-card.border-secondary .ez-kpi-icon { color: var(--bs-secondary); }
.ez-kpi-card.border-success   .ez-kpi-icon { color: var(--bs-success); }
.ez-kpi-card.border-warning   .ez-kpi-icon { color: var(--bs-warning); }
.ez-kpi-card.border-danger    .ez-kpi-icon { color: var(--bs-danger); }
.ez-kpi-card.border-info      .ez-kpi-icon { color: var(--bs-info); }

/* ── Color palette demo (UI Examples > Colors) ─────────────── */
/* Swatch chip beside each hex value in the comparison table. The per-swatch
   background colour is set inline because it is dynamic demo data. */
.color-swatch {
    display: inline-block;
    width: 1.25rem;
    height: 1.25rem;
    border-radius: var(--bs-border-radius-sm);
    border: 1px solid var(--bs-border-color);
    vertical-align: -0.25rem;
    margin-right: 0.5rem;
}
/* Native colour picker used in the Tweak column - sized to match the swatch row. */
.color-tweak {
    width: 2.5rem;
    height: 1.75rem;
    padding: 0;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-sm);
    background: var(--bs-body-bg);
    cursor: pointer;
}

/* ── EzImpersonationBanner ─────────────────────────────────── */

.ez-impersonation-banner {
    position: sticky;
    top: env(safe-area-inset-top); /* sticks below the status bar in the edge-to-edge mobile app */
    z-index: 1050;
    background: #fbbf24;
    border-bottom: 2px solid color-mix(in srgb, #fbbf24 72%, #000);
}

.ez-imp-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.4375rem 1.25rem;
}

.ez-imp-left {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    min-width: 0;
}

.ez-imp-icon {
    color: rgba(0, 0, 0, 0.75);
    flex-shrink: 0;
}

.ez-imp-text {
    font-weight: 500;
    color: rgba(0, 0, 0, 0.85);
    display: flex;
    align-items: center;
    gap: 0.375rem;
    min-width: 0;
}

.ez-imp-user {
    font-weight: 700;
    background: rgba(0, 0, 0, 0.12);
    color: rgba(0, 0, 0, 0.9);
    padding: 0.125rem 0.5rem;
    border-radius: var(--bs-border-radius);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 20rem;
}

.ez-imp-end-btn {
    flex-shrink: 0;
    background: #dc2626 !important;
    border-color: transparent !important;
    color: #fff !important;
}

.ez-imp-end-btn:hover {
    background: color-mix(in srgb, #dc2626 85%, #fff) !important;
}

/* ── Table utilities ───────────────────────────────────────── */

/* Header cells never wrap: keep the label and any sort icon on one line.
   Body cells keep their default wrapping behaviour. */
.table thead th { white-space: nowrap; }

/* Body cells are vertically centered so mixed-height content (icons, badges,
   action menus, avatars) lines up with adjacent single-line text. Bootstrap's
   default is vertical-align: inherit -> baseline, which leaves rows looking
   top-heavy. Tables that deliberately want top alignment (the Error Log detail
   row, the permission matrix) override this with a more specific selector. */
.table > tbody > tr > td { vertical-align: middle; }

/* Sortable headers (EzSortHeader): the th holds a real <button>, so the control is keyboard
   operable and the state is announced through aria-sort on the th. The button carries the
   cell's own padding and the th drops to zero, so the WHOLE header cell stays the hit target -
   exactly as it was when the click handler sat on the th itself. */
.table thead th.sortable { padding: 0; }
.table thead th.sortable .sort-header {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 0.5rem; /* Bootstrap's own table cell padding - it is a literal there, not a token */
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: inherit;
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
}
.table-sm thead th.sortable .sort-header { padding: 0.25rem; } /* matches .table-sm's own cells */
.table thead th.sortable .sort-icon { margin-left: 0.35em; opacity: 0.5; }
.table thead th.sortable.is-sorted .sort-icon,
.table thead th.sortable:hover .sort-icon,
.table thead th.sortable .sort-header:focus-visible .sort-icon { opacity: 1; }

/* Actions column: shrink to content width, no wrap, no trailing padding */
.col-actions { width: 1px; white-space: nowrap; padding-right: 0 !important; }

/* ... and when it is the row's FIRST cell it stays pinned to the left edge while a wide table
   scrolls sideways, so a row's actions are always reachable without scrolling back. Scoped to
   :first-child on purpose: in a Selectable table the checkbox column comes first, and a second
   cell pinned to left:0 would simply sit on top of it. The background is the card surface the
   table sits on; Bootstrap paints stripe/hover as an inset box-shadow over it, so both survive. */
.table tr > .col-actions:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background-color: var(--bs-card-bg, var(--bs-body-bg));
}
.table thead tr > .col-actions:first-child { z-index: 2; }

/* The sticky cell is a stacking context, so the open panel's own z-index is trapped inside it at
   z-index 1 - and every LATER row's pinned cell (and anything else at 1) paints over the panel.
   Lift the cell whose menu is open above the rest of the table, header included. */
.table tr > .col-actions:first-child:has(.ez-action-menu__panel.show) { z-index: 3; }

/* Selection column (EzTable Selectable): shrink to the checkbox, centered. */
.col-select { width: 1px; white-space: nowrap; text-align: center; }
.col-select .form-check-input { margin: 0; cursor: pointer; }

/* Channel columns (In-App / Push / Email / SMS on messaging and automation lists):
   one fixed, equal width so the channel checks line up across every table. */
.col-channel { width: 5.5rem; text-align: center; }

/* Archived rows: dim and strike through all cells except the actions column */
.row-archived td:not(.col-actions) { opacity: 0.5; text-decoration: line-through; }
.row-archived td:not(.col-actions) .badge { text-decoration: none; }

/* Single-line cells that clip overflow to an ellipsis. Pairs with a
   fixed-layout table (or a cell max-width) so the column width is honoured
   and long, unbroken content (URLs, stack frames) can't stretch the row. */
.cell-truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Auto-layout variant of .cell-truncate: carries its own width cap, so a long
   unbroken value (an email address, a business name, preview text) cannot
   stretch its column and push the whole table into a wide horizontal scroll
   on phones. Pair with title="..." so the full value stays reachable. */
.td-clamp {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 20rem;
}
@media (max-width: 767.98px) {
    .td-clamp { max-width: 12rem; }
}
/* The phone cap at every size, for a secondary column in a wide table (the runs log's User column)
   where even 20rem lets one relay email address push the figures beside it off screen. */
.td-clamp.td-clamp--narrow { max-width: 12rem; }

/* ── Error log table ─────────────────────────────────────────────────────
   No fixed layout and no declared column widths: the seven of them added up to about 1032px, so
   the table could never fit a phone and the first column was always partly off screen. The two
   columns that actually needed containing (Exception, Path) carry .td-clamp, which brings its
   own width cap, so a long JS exception URL still cannot stretch a column or balloon a row. */

/* Bulk action bar shown above the error table when rows are selected. */
.bulk-action-bar {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin: 0 0 0.75rem;
    padding: 0.5rem 0.875rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-tertiary-bg);
}
.bulk-action-count { font-weight: 600; color: var(--bs-body-color); }

/* Summary-row cells stay on a single line; only the message row below wraps. */
.errors-table-container thead th,
.errors-table-container tbody tr:not(.ez-row-detail) > td { white-space: nowrap; }

/* ── Error log triage summary chips ─────────────────────────────────────────── */
.error-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 0.625rem;
    margin: 0 0 1rem;
}
.error-summary-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    cursor: pointer;
    transition: border-color 0.12s ease, background-color 0.12s ease;
}
.error-summary-chip:hover { background: var(--bs-tertiary-bg); }
.error-summary-chip .error-summary-count { font-size: 1.05rem; font-weight: 700; line-height: 1; }
.error-summary-chip .error-summary-label {
    font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}
.error-summary-chip.is-critical { border-color: var(--bs-danger); }
.error-summary-chip.is-critical .error-summary-count { color: var(--bs-danger); }
.error-summary-chip.is-error { border-color: var(--bs-warning); }
.error-summary-chip.is-error .error-summary-count { color: var(--bs-warning); }
.error-summary-chip.is-warning { border-color: var(--bs-info); }
.error-summary-chip.is-warning .error-summary-count { color: var(--bs-info); }

/* Full-width message row sitting directly beneath each error's summary row.
   The summary row drops its bottom border so the pair reads as one unit. */
.errors-table-container tbody tr:has(+ .ez-row-detail) > td { border-bottom: 0; }
.errors-table-container .ez-row-detail > td {
    border-top: 0;
    padding-top: 0.25rem;
    padding-bottom: 0.875rem;
    vertical-align: top;
}
.errors-table-container .ez-row-detail .error-message-spacer { padding: 0; }

/* Keep the summary row and its message row highlighted together on hover. */
.errors-table-container tbody tr:hover + .ez-row-detail > td,
.errors-table-container tbody tr:has(+ .ez-row-detail:hover) > td {
    background-color: var(--bs-table-hover-bg);
}

/* ============================================================
   .ez-code-block - reusable inline code/output panel
   Renders monospace text (messages, stack traces, payloads, IDs)
   in a bordered, tinted panel that wraps long unbroken strings.
   Use on a <pre> to preserve original line breaks.
   ============================================================ */
.ez-code-block {
    margin: 0;
    padding: 0.5rem 0.75rem;
    font-family: var(--bs-font-monospace);
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--bs-body-color);
    background-color: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    white-space: pre-wrap;
    overflow-wrap: anywhere;   /* wrap long unbroken URLs/stack frames */
}

/* ============================================================
   Audit / error detail modal - metadata grid + before/after diff
   Shared by the Audit Log and Error Log "View details" modals.
   ============================================================ */

/* Two-column definition grid for entry metadata. */
.audit-detail-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem 1.5rem;
    margin: 0 0 1.25rem;
}
.audit-detail-grid > div { min-width: 0; }
.audit-detail-grid .audit-detail-full { grid-column: 1 / -1; }
.audit-detail-grid dt {
    margin: 0 0 0.125rem;
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}
.audit-detail-grid dd {
    margin: 0;
    color: var(--bs-body-color);
    overflow-wrap: anywhere;
}
.audit-detail-reason { display: block; margin-top: 0.25rem; color: var(--bs-danger); }

/* Section heading above a diff or stack trace. */
.audit-diff-heading {
    margin: 1.25rem 0 0.5rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}

/* ── Unified before/after field diff (GitHub-style) ──────────────────────── */
.audit-diff-block {
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    font-family: var(--bs-font-monospace);
    font-size: 0.8125rem;
    line-height: 1.6;
}
.diff-line {
    display: flex;
    gap: 0.5rem;
    padding: 0.0625rem 0.75rem;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.diff-gutter {
    flex: none;
    width: 1ch;
    text-align: center;
    color: var(--bs-secondary-color);
    -webkit-user-select: none;
    user-select: none;
}
.diff-body { min-width: 0; }
.diff-field-name { color: var(--bs-secondary-color); }

.diff-line--removed { background-color: rgba(var(--bs-danger-rgb), 0.12); }
.diff-line--removed .diff-gutter { color: var(--bs-danger); }
.diff-line--added { background-color: rgba(var(--bs-success-rgb), 0.12); }
.diff-line--added .diff-gutter { color: var(--bs-success); }
.diff-line--context { color: var(--bs-secondary-color); }

/* Revertable hint icon shown beside the result badge in the table. */
.audit-revert-hint { color: var(--bs-warning); opacity: 0.85; }

/* Inline link-styled button (e.g. the navigable correlation id in the detail modal). */
.btn-link-inline {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--bs-link-color);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.btn-link-inline:hover { color: var(--bs-link-hover-color); }

/* "Changed only" toggle above the field diff. */
.audit-diff-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--bs-secondary-color);
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
}
.audit-diff-toggle input { cursor: pointer; }

/* ============================================================
   EzActionBar
   ============================================================ */

/* Reusable 1px vertical rule for the .page-actions flex row.
   Place <span class="page-actions-divider"> between any two flex children.
   Shown automatically when the immediately preceding sibling has content. */
.page-actions-divider {
    display: none;
    width: 1px;
    align-self: stretch;
    background: var(--bs-border-color);
    flex-shrink: 0;
    margin: 0 0.25rem;
}
.page-actions-search:has(*) + .page-actions-divider {
    display: block;
}
/* Divider placed directly inside the buttons zone */
.page-actions-buttons .page-actions-divider {
    display: block;
}

/* The row's top spacing, and the ONLY source of it - EzActionBar.razor deliberately carries no mt-*
   utility, because a utility class cannot be conditional and the spacing has to disappear along with
   the bar. Both zones collapse to display:none when their SectionOutlet is empty, so on a page with
   no search and no actions (every form / edit / detail page) the row is zero-height; an
   unconditional margin there is 1rem of dead space above the content with nothing to separate it
   from. 1rem matches the mt-3 this replaces, so a page that HAS a toolbar is unchanged.

   `> *` and NOT a nested `:has(*)`: :has() may not contain another :has(). Such a selector is a
   syntax error that browsers drop silently, taking the whole rule with it - which is exactly what
   happened to the previous version of this rule, so it never applied on any page. */
.page-actions:has(.page-actions-search > *, .page-actions-buttons > *) {
    margin-top: 1rem; /* Bootstrap spacer 3 */
}

/* Left zone: flex-fill utility on element handles flex growth.
   min-width: 0 prevents overflow in flex containers - no Bootstrap equivalent.
   display:none default is overridden by :has(*). */
.page-actions-search {
    display: none;
    min-width: 0;
}
.page-actions-search:has(*) { display: flex; }

/* Bootstrap < md: search, Filters, the primary action and the overflow trigger share ONE line.
   Every button in the search zone drops to an icon-only square (the same treatment the primary
   gets below), and the search box is the only thing that shrinks to make room. The label is
   clipped out of sight, not removed, so the accessible name is unchanged; a filter-count badge is
   not a label and stays visible.

   The exception is a zone holding a select or a switch (a status picker, "Show ended sessions"):
   neither can collapse to a glyph, so that zone wraps rather than squeezing the search box to
   nothing; row-gap matches the gap-2 already on the zone. */
@media (max-width: 767.98px) {
    .page-actions-search > .ez-search-input { min-width: 0; }
    .page-actions-search > .btn {
        flex: none;
        --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);
    }
    .page-actions-search > .btn > span:not(.ez-btn-badge) {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
    .page-actions-search:has(.form-select, .form-check) { flex-wrap: wrap; row-gap: 0.5rem; } /* Bootstrap spacer 2 */
}

/* Right zone: hidden until content arrives via SectionOutlet. */
.page-actions-buttons { display: none; }
.page-actions-buttons:has(*) { display: flex; }

/* Active filter chips row: gap-2 and layout utilities on element.
   display:none cannot use d-none - Bootstrap d-none is !important and blocks :has() override. */
.page-current-filters { display: none; }
.page-current-filters:has(*) { display: flex; }

/* Overflow trigger zone: the icon-only button that collapses the page's SECONDARY action buttons on
   small screens. Hidden outright at >= md and whenever the page supplied nothing beyond its primary
   action. */
.page-actions-toggle-zone { display: none; }

/* Bootstrap < md: the action buttons stop wrapping onto their own line. The page's primary action -
   the one filled button, .btn-ez-primary - stays in the row as an icon-only square: a list page
   exists to Create, and that must never be a tap away behind an unlabelled menu. Everything else
   hides behind the trigger, which sits immediately beside it, and the buttons zone becomes a panel
   dropping from the row's right edge when the trigger is tapped.

   One outlet serves both states, so the zone changes SHAPE rather than being duplicated: while
   closed it is display:contents, which lifts its children into this flex row (the primary shows,
   the rest are hidden); while open it is a box again - the panel - holding every action. The primary
   moves into the panel for as long as it is open, which is fine: it is still visible there, as the
   first row, and the trigger does not move because the search zone (flex-fill) takes up the width. */
@media (max-width: 767.98px) {
    /* Only worth a trigger when the page pushed something OTHER than its primary action into the
       outlet - a page whose only action is Create has nothing to overflow.
       `> *` and NOT a nested `:has(*)`: :has() may not contain another :has(), and a selector that
       does is a syntax error the browser drops silently - taking the whole rule with it. */
    .page-actions:has(.page-actions-buttons > :not(.btn-ez-primary)) .page-actions-toggle-zone {
        display: flex;
        align-items: center;
        flex: none;
        order: 1; /* after the primary square, so the row reads [search] [primary] [more] */
    }

    /* Anchor for the absolutely positioned panel below. */
    .page-actions { position: relative; }

    /* Closed: no box of its own - the children sit in the row. Beats the base
       `.page-actions-buttons:has(*) { display: flex }` (equal specificity, declared later); .is-open
       then beats this one on specificity. */
    .page-actions-buttons:has(*) { display: contents; }

    /* Closed: everything but the primary is out of the row, the DOM and the tab order - display:none,
       not a visual hide, or a phone would carry a row of invisible tab stops. */
    .page-actions-buttons:not(.is-open) > :not(.btn-ez-primary) { display: none; }

    /* Closed: the primary becomes the same square an icon-only EzButton renders (.btn-ez-icon) - the
       label is clipped out of sight but left in the DOM, so the accessible name is unchanged and a
       screen reader still hears "Create". */
    .page-actions-buttons:not(.is-open) > .btn-ez-primary {
        --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);
        margin-left: auto; /* the zone's ms-auto, which display:contents no longer applies */
    }
    .page-actions-buttons:not(.is-open) > .btn-ez-primary > span {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }

    /* Open: a box again, floating under the trigger. */
    .page-actions-buttons.is-open:has(*) {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        position: absolute;
        top: 100%;
        right: 0;
        z-index: 1050; /* above .user-menu-backdrop (1049) */
        min-width: 12rem;
        max-width: min(20rem, calc(100vw - 2.5rem));
        margin-top: 0.375rem;
        padding: 0.375rem;
        background: var(--bs-body-bg);
        border: 1px solid var(--border-strong);
        border-radius: var(--bs-border-radius-lg);
    }
    /* Stacked full-width rows so the panel reads as one list of actions. Variant colours are kept:
       the primary action should still stand out here exactly as it does in the header. */
    .page-actions-buttons.is-open .btn {
        width: 100%;
        justify-content: flex-start;
    }
}

/* Bootstrap < sm */
@media (max-width: 575.98px) {
    .page-actions-divider { display: none !important; }
}

/* Placeholder text: dim it below Bootstrap's default so it reads clearly as a hint/mask
   rather than as entered content. Applies to every .form-control (inputs + textareas).

   Built on --bs-secondary-color, NOT --bs-tertiary-color. This theme's dark block inverts
   Bootstrap's convention - tertiary (#d4d4d8) is BRIGHTER than secondary (#a1a1aa) there -
   so the old pairing started from the brightest muted token and then spent alpha winning
   that back. Starting one step lower gets the same faintness with less of the washing-out
   that alpha causes over a coloured card surface. */
.form-control::placeholder {
    /* One token, no separate opacity - see --ez-placeholder-color at the top of this file for why
       the fade has to live inside the colour. Change it there, not here: EzyPay's Stripe card
       fields read that same token so they stay in step automatically. */
    color: var(--ez-placeholder-color);
    opacity: 1;
}

/* Google PlaceAutocompleteElement (address autocomplete): its input + suggestion list live
   in a closed shadow DOM styled with Google's Material defaults. The --gmp-* custom properties
   theme the surfaces, text and dropdown panel, but the input's *border* is NOT one of them: it
   is a hard-coded `border: 1px solid black` on the shadow's `:host` rule (confirmed by dumping
   the shadow stylesheet). An outer-document rule on the host element outranks the shadow's
   `:host` rule, so we restyle the resting + hover border here to track .form-control exactly
   (grey at rest, brand-mix on hover). The FOCUS treatment cannot live here: Chrome does not
   apply a `:focus-within` rule to this host because focus is inside a CLOSED shadow tree (tested
   - it never paints, even with !important), so the brand focus border + halo are injected onto
   Google's own `.focus-ring` element in addressSearch.js (EzyPlatform.Components) instead. */
gmp-place-autocomplete {
    --gmp-mat-color-surface: var(--bs-body-bg);
    --gmp-mat-color-surface-container: var(--bs-body-bg);
    --gmp-mat-color-on-surface: var(--bs-body-color);
    --gmp-mat-color-on-surface-variant: var(--bs-secondary-color);
    --gmp-mat-color-primary: var(--bs-primary);
    --gmp-mat-font-family: var(--bs-body-font-family);
    --gmp-card-border-radius: var(--bs-border-radius);

    background-color: var(--bs-body-bg);
    border: var(--bs-border-width, 1px) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
gmp-place-autocomplete:hover:not(:focus-within) {
    border-color: var(--ez-field-border-hover, var(--border-strong));
}

/* EzSearchInput: Bootstrap input-group with prepended icon */
/* Hide native browser 'x' on search inputs */
.ez-search-input input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
/* No border override here any more: the search box is matched to the OUTLINE TIER, whose
   border is the --bs-border-color hairline that .form-control already carries. The old
   --bs-secondary pinning existed to match the heavier btn-outline-secondary and now would
   make the search box louder than the Filter button beside it. */
/* The [data-theme-forms="on"] resting/hover overrides for this component live in
   ez-platform-forms.css, next to the rest of that theme's rules - see the "Grouped
   fields" section there for why they're needed. */
/* Clear button: Bootstrap resets cursor on input-group-text buttons */
.ez-search-input__clear { cursor: pointer; }
.ez-search-input__clear:hover { color: var(--bs-body-color) !important; }

/* Focus ring on ALL input groups: match a standalone field's focus (the brand ring via
   --bs-focus-ring-*), but draw ONE ring around the whole .input-group instead of each
   element, so a grouped field (icon + input) reads identically to a standalone field. */
.input-group:focus-within .form-control:focus,
.input-group:focus-within .form-select:focus { box-shadow: none; }
.input-group:focus-within {
    border-radius: var(--bs-border-radius);
    box-shadow: 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}
.input-group:focus-within .input-group-text,
.input-group:focus-within .form-control,
.input-group:focus-within .form-select { border-color: var(--bs-primary); } /* brand focused-input border (matches standalone fields) */

/* EzSearchSelect dropdown: mirror the .ez-action-menu panel + primary-tint hover
   (same design tokens) so result items stand out and match the app's other menus. */
.ez-search-select__menu {
    margin-top: 0.375rem;
    padding: 0.375rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--bs-border-radius-lg);
    box-shadow: var(--bs-box-shadow);
    max-height: 18.75rem; /* keep long result lists inside short viewports */
    overflow-y: auto;
}
.ez-search-select__menu .dropdown-item {
    padding: 0.4375rem 0.625rem;
    border-radius: var(--bs-border-radius);
    white-space: normal;
}
.ez-search-select__menu .dropdown-item > span { font-weight: 500; }
.ez-search-select__menu .dropdown-item:hover,
.ez-search-select__menu .dropdown-item:focus {
    background: rgba(var(--bs-primary-rgb), .08);
    color: var(--bs-primary);
}

/* EzPagination jump-to-page input: fixed compact width (was an inline style). */
.ez-pagination-jump { width: 4.5rem; }

/* ============================================================
   Blazor form validation
   ============================================================ */

/* Blazor adds .valid/.invalid directly to <input> - cannot use Bootstrap utilities. */
.valid.modified:not([type=checkbox]) { outline: 1px solid var(--bs-success); }
.invalid { outline: 1px solid var(--bs-danger); }

/* <ValidationMessage> output.

   NO ICON, deliberately. A field error used to carry a fa-triangle-exclamation, which broke down on
   the payment forms: the card fields are rendered by Stripe inside a cross-origin iframe, and their
   appearance API silently discards both `content` and `backgroundImage` on its error text (verified in
   a browser - it accepts the rules without complaint and then ignores them, exactly as it does for any
   CSS function it does not recognise). So a card error could never carry the icon, and Quick Pay showed
   two kinds of validation message a few pixels apart.

   The icon was decorative in any case: the text states the problem, it is already --bs-danger, and the
   field itself takes a red border. Nothing is lost by dropping it, and the money forms now read as one
   thing. The dividing line is now: INLINE field errors are plain text; block-level EzAlert keeps its
   icon, because a standalone panel needs an affordance that a line under a field does not.

   align-items:baseline has no Bootstrap utility equivalent, so this rule stays. */
.validation-message {
    color: var(--bs-danger);
    font-size: 0.875em;
    font-weight: 500;
    margin-top: 0.25rem;
    display: flex;
    align-items: baseline;
    gap: 0.3em;
}

/* ============================================================
   Shared overlay backdrop
   All overlays that dim and blur the page behind them.
   Transparent click-capture backdrops (action-menu, user-menu)
   carry no background or blur - they are not overlays.
   ============================================================ */

.ez-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .5);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    animation: ez-backdrop-in .2s ease both;
}
@keyframes ez-backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* ============================================================
   EzFilterDrawer
   Slide-out right panel for page-level filters.
   ============================================================ */

/* Backdrop: extends .ez-backdrop with z-index. The drawer pair sits ABOVE the bottom tab bar (1046),
   the sidebar (1047) and the header menus (1050) and BELOW Bootstrap modals (1055): at 1030/1031 the
   drawer footer - Clear all / Apply - was painted over by the tab bar on every app-surface phone. */
.ez-filter-backdrop { z-index: 1052; }

/* Drawer panel */
.ez-filter-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(22rem, 94vw);
    /* Clear of the status bar and home indicator in the edge-to-edge mobile app (0 elsewhere). */
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--bs-body-bg);
    border-left: 1px solid var(--border-strong);
    box-shadow: var(--bs-box-shadow-lg);
    z-index: 1053;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: ez-filter-slide-in .28s cubic-bezier(0.32, 0.72, 0, 1) both;
}
@keyframes ez-filter-slide-in {
    from { transform: translateX(100%); }
    to   { transform: translateX(0); }
}

/* Body: thin scrollbar - no Bootstrap equivalent */
.ez-filter-body {
    overflow-y: auto;
    scrollbar-color: var(--border-strong) transparent;
    scrollbar-width: thin;
}

/* Filter section: each filter control group in the drawer */
.ez-filter-section {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* Active filter count indicator inside the drawer header */
.ez-filter-count {
    font-weight: 500;
    color: var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.1);
    /* Pill: the old 0.75rem already exceeded half this chip's height, so it rendered fully
       rounded anyway. The token says so, and keeps saying it if the padding changes. */
    border-radius: var(--bs-border-radius-pill);
    padding: 0.125rem 0.5rem;
}

/* Section heading label */
.ez-filter-label {
    font-size: 0.875em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}

.ez-filter-hint {
    font-size: 0.875em;
    color: var(--bs-secondary-color);
    margin: 0;
}

/* ── EzFilterChip ─────────────────────────────────────────────────────────── */

.ez-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.625rem;
    border-radius: var(--bs-border-radius);
    border: 1px solid var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.08);
    color: var(--bs-primary);
    font-weight: 500;
    cursor: pointer;
    line-height: 1.4;
    text-decoration: none;
    transition: background 0.15s, transform 0.15s;
}

.ez-chip:hover {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: var(--ez-on-primary);
    transform: translateY(-1px);
}

/* ============================================================
   Bootstrap modal backdrop override
   EzModal renders Bootstrap's .modal-backdrop; override it to
   match the shared .ez-backdrop (dim + blur).
   ============================================================ */

.modal-backdrop {
    --bs-backdrop-bg: rgba(0, 0, 0, .88);
    --bs-backdrop-opacity: 1;
    background: rgba(0, 0, 0, .88);
    backdrop-filter: blur(10px) saturate(.6);
    -webkit-backdrop-filter: blur(10px) saturate(.6);
    animation: ez-backdrop-in .2s ease both;
}

/* The dialog itself has to separate from the page behind it. On a dark theme the modal surface and the
   page are BOTH pure black, so a faint edge let the dialog read as part of the page it covered. Flat
   design: a full-strength brand outline does the separating, never a shadow. */
.modal-content {
    border: 2px solid var(--bs-primary);
}
/* ezModal.js parks focus on the dialog container when it opens with no form field to focus (tabindex
   -1, so it is never a Tab stop). It is not a control, so it gets no ring - the brand border already
   says where the dialog is. */
.modal-content:focus { outline: 0; }

/* Body scroll lock while an EzModal is open. Bootstrap does this from its own modal JS, which we
   do not load (EzModal is Blazor/ezModal.js driven), and Bootstrap 5 ships no .modal-open rule -
   so the class is ours. Set by ezModal.js lockScroll(), which reference-counts so an inner modal
   closing cannot unlock the page under an outer one. --ez-scrollbar-gap is the width the vanished
   scrollbar leaves behind: padding it back keeps the page from jumping sideways on open (0 on
   overlay-scrollbar platforms, and on a modal opened over an already unscrollable page). */
body[data-ez-scroll-lock] {
    overflow: hidden;
    padding-right: var(--ez-scrollbar-gap, 0);
}

/* Blazor error UI (#blazor-error-ui): all rules live in ez-platform-error-ui.css
   (single source - do not redeclare them here). */

/* ============================================================
   Blazor reconnect modal (#components-reconnect-modal)
   Shown by Blazor JS when the SignalR circuit is lost. Two stages, set by
   ReconnectModal.razor.js:
     .ez-reconnect--pill  first 10s of a drop - a small chip, light dim.
                          Top centre at >= xl; above the bottom tab bar below.
     .ez-reconnect--card  after 10s, or any state the user must act on.
                          Bottom sheet on phones, centred card at >= sm.
   The page is inert in both (showModal), because every tap on a dropped
   circuit goes nowhere - the dim just says so without hiding the page.
   ============================================================ */

/* Visibility helpers - Blazor (and ReconnectModal.razor.js) toggle classes on the dialog */
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.ez-reconnect-offline-visible,
.components-rejoining-animation { display: none; }

#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { display: block; }

/* Offline: the retry messages give way to the offline one (the spinner stays - it is still trying). */
#components-reconnect-modal.ez-reconnect-offline:is(.components-reconnect-show, .components-reconnect-retrying) .ez-reconnect-offline-visible { display: block; }
#components-reconnect-modal.ez-reconnect-offline .components-reconnect-first-attempt-visible,
#components-reconnect-modal.ez-reconnect-offline .components-reconnect-repeated-attempt-visible { display: none; }

#components-reconnect-modal {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--border-strong);
    max-width: calc(100vw - 2rem);
    padding: 0;
    overflow: visible;
}
#components-reconnect-modal[open] { animation: ez-reconnect-in .2s ease-out both; }

#components-reconnect-modal::backdrop {
    background: rgba(0, 0, 0, .15);
    animation: ez-backdrop-in .2s ease both;
}

#components-reconnect-modal p { margin: 0; }

/* Stage 1: pill. Spinner and message on one line. */
#components-reconnect-modal.ez-reconnect--pill {
    width: max-content;
    margin: calc(1rem + env(safe-area-inset-top)) auto auto;
    border-radius: var(--bs-border-radius-pill);
}
#components-reconnect-modal.ez-reconnect--pill .components-reconnect-container {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
}

/* Stage 2: card. Stacked and centred, with room for the action button. */
#components-reconnect-modal.ez-reconnect--card {
    width: 22rem;
    margin: auto;
    border-radius: var(--bs-border-radius-lg);
    animation-name: ez-reconnect-card-in;
}
#components-reconnect-modal.ez-reconnect--card .components-reconnect-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    padding: 1.5rem;
    text-align: center;
}
#components-reconnect-modal.ez-reconnect--card::backdrop { background: rgba(0, 0, 0, .5); }

/* Bootstrap < xl: the bottom tab bar is the thumb zone on phones and tablets, so the pill sits just
   above it (4.25rem is the bar's own height) rather than over the page header. */
@media (max-width: 1199.98px) {
    #components-reconnect-modal.ez-reconnect--pill {
        margin: auto auto calc(1rem + env(safe-area-inset-bottom));
    }
    :root:has(.bottom-tabs) #components-reconnect-modal.ez-reconnect--pill {
        margin-bottom: calc(4.25rem + 1rem + env(safe-area-inset-bottom));
    }
}

/* Bootstrap < sm: the card becomes a full-width bottom sheet, growing out of where the pill was. */
@media (max-width: 575.98px) {
    #components-reconnect-modal.ez-reconnect--card {
        width: calc(100vw - 2rem);
        margin: auto auto calc(1rem + env(safe-area-inset-bottom));
    }
    :root:has(.bottom-tabs) #components-reconnect-modal.ez-reconnect--card {
        margin-bottom: calc(4.25rem + 1rem + env(safe-area-inset-bottom));
    }
    #components-reconnect-modal.ez-reconnect--card .btn { width: 100%; }
}

@keyframes ez-reconnect-in {
    from { opacity: 0; transform: translateY(0.5rem); }
    to   { opacity: 1; transform: none; }
}
@keyframes ez-reconnect-card-in {
    from { opacity: 0; transform: scale(0.97); }
    to   { opacity: 1; transform: none; }
}

/* ── EzToastHost - fixed toast notification stack ───────────────
   Restored after the host-shell migration (was lost when the old
   consolidated app.css was dropped). Mapped onto the current --bs-*
   theme tokens. */
.ez-toast-host {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.ez-toast {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.6875rem 0.875rem;
    border-radius: var(--bs-border-radius);
    border: 1px solid var(--border-strong);
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    font-size: 0.8125rem;
    min-width: 16rem;
    max-width: 22rem;
    box-shadow: var(--bs-box-shadow);
    animation: ez-toast-in .18s ease;
}
@keyframes ez-toast-in {
    from { opacity: 0; transform: translateY(0.375rem); }
    to   { opacity: 1; transform: translateY(0); }
}
.ez-toast__icon { flex-shrink: 0; }
.ez-toast__msg  { flex: 1; line-height: 1.4; }
.ez-toast--success { border-color: rgba(var(--bs-success-rgb), .3); }
.ez-toast--success .ez-toast__icon { color: var(--bs-success); }
/* Errors look like every other toast - panel background, tinted border and icon - and auto-dismiss
   like them too (see ToastService). Filled-red plus sticky made routine, recoverable errors ("that
   name is taken") read as an incident and left the stack to be cleared by hand. A message that must
   not be missed belongs in a page-level EzAlert. */
.ez-toast--error { border-color: rgba(var(--bs-danger-rgb), .3); }
.ez-toast--error .ez-toast__icon { color: var(--bs-danger); }
/* Warnings stay FILLED: the ones we raise (out of credits, action blocked) stop the user doing what
   they came for, and on a dark phone screen the panel style camouflaged them. */
.ez-toast--warning {
    background: var(--bs-warning);
    border-color: var(--bs-warning);
    color: #1a1a1a;
}
.ez-toast--warning .ez-toast__icon { color: #1a1a1a; }
.ez-toast--info    { border-color: rgba(var(--bs-primary-rgb), .3); }
.ez-toast--info    .ez-toast__icon { color: var(--bs-link-hover-color); }

/* Toasts live at the BOTTOM at every size, and notifications at the TOP (the arrival banner, see
   .notif-banner). Owner decision 2026-09-28, after phones had tried toasts at the top: a toast is the
   reply to something the user just did, so it belongs by the thumb that did it - Undo and View are one
   short reach from the button just pressed - while news arriving from elsewhere drops from the top,
   the way the operating system's own notifications do. Two kinds of message, two fixed places, so
   neither ever has to stack around the other.

   Below xl the fixed bottom tab bar is showing, and a toast sat on top of its last tab. Worse than
   hiding it: when the toast timed out, a tap aimed at its action (Undo) landed on the tab underneath
   and navigated away. Lift the stack clear of the bar - the same 4.25rem clearance .surface-app .main
   reserves for it. */
@media (max-width: 1199.98px) {
    body:has(.bottom-tabs) .ez-toast-host {
        bottom: calc(4.25rem + env(safe-area-inset-bottom) + 0.5rem);
    }
}

/* Phones (Bootstrap < sm): full width, like an Android snackbar - a 22rem corner card would leave a
   ragged strip beside it - and slightly larger type. The stack still grows upward from the bottom, so
   the newest toast is always the one nearest the thumb. */
@media (max-width: 575.98px) {
    .ez-toast-host {
        left: 0.75rem;
        right: 0.75rem;
    }
    .ez-toast {
        width: 100%;
        min-width: 0;
        max-width: none;
        font-size: 0.875rem;
    }
}

/* ============================================================
   Support ticket views (TicketPageBody / TicketThread / reply toggle)
   Restored after the host-shell migration (was lost when the old
   consolidated app.css was dropped, same as EzToastHost above).
   Originally per-component scoped CSS; mapped onto the current
   --bs-* theme tokens (--panel→--bs-body-bg, --panel-sub→--bs-secondary-bg,
   --border→--bs-border-color, --text→--bs-body-color,
   --text-muted→--bs-secondary-color, --text-subtle→--bs-tertiary-color).
   ============================================================ */

/* ── Ticket page header ─────────────────────────────────────────────── */
.ticket-head {
    margin-bottom: 1.25rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--bs-border-color);
}
.ticket-head-label {
    margin: 0 0 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--bs-tertiary-color);
}
.ticket-head-title {
    margin: 0 0 0.5rem;
    font-size: 1.625rem;
    font-weight: 700;
    line-height: 1.25;
    color: var(--bs-primary);
    text-transform: uppercase;
    word-wrap: break-word;
}
/* ── Sidebar details list (compact two-column) ──────────────────────── */
.ticket-details {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: 0.875rem;
    row-gap: 0.4375rem;
    font-size: 0.8125rem;
    margin: 0;
}
.ticket-details dt {
    color: var(--bs-secondary-color);
    font-weight: 500;
}
.ticket-details dd {
    margin: 0;
    color: var(--bs-body-color);
    overflow-wrap: anywhere;
}

/* ─── Ticket Thread: GitHub/Linear-style vertical timeline ───────────────── */
.thread {
    list-style: none;
    margin: 0;
    padding: 0;
    position: relative;
}

/* Vertical rail running the full length of the thread */
.thread::before {
    content: '';
    position: absolute;
    left: 1.1875rem;          /* center of the 2.375rem avatar column */
    top: 0.5rem;
    bottom: 0.5rem;
    width: 2px;
    background: var(--bs-border-color);
    border-radius: 1px;
}

.thread-row {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 0.75rem 0;
}

/* ── Message rows ────────────────────────────────────────────────────────── */
.thread-row--msg + .thread-row--msg,
.thread-row--event + .thread-row--msg,
.thread-row--msg + .thread-row--event {
    margin-top: 0.25rem;
}

/* ── Card colour tokens ───────────────────────────────────────────────────── */
/* All card colours live here as CSS custom properties on the row element.    */
/* .thread-card and its children inherit and consume the tokens, so colour    */
/* variants (user, staff, private, archived) only need to override the vars.  */
.thread-row--msg {
    --tc-bg:           var(--bs-body-bg);
    --tc-head-bg:      var(--bs-secondary-bg);
    --tc-border:       var(--bs-border-color);
    --tc-arrow-outer:  var(--bs-border-color);
    --tc-arrow-inner:  var(--bs-secondary-bg);
    --tc-author-color: var(--bs-body-color);
}

.thread-avatar {
    flex: 0 0 2.375rem;
    width: 2.375rem;
    height: 2.375rem;
    border-radius: 50%;
    background: var(--bs-primary);
    color: var(--ez-on-primary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    border: 3px solid var(--bs-body-bg);
    box-shadow: 0 0 0 1px var(--bs-border-color);
    z-index: 1;
}

.thread-row.is-staff .thread-avatar {
    background: var(--bs-info);
}

.thread-card {
    flex: 1 1 auto;
    min-width: 0;
    position: relative;
    background: var(--tc-bg);
    border: 1px solid var(--tc-border);
    border-radius: var(--bs-border-radius);
    overflow: hidden;
}

/* Speech-bubble arrow pointing at the avatar */
.thread-card::before,
.thread-card::after {
    content: '';
    position: absolute;
    top: 0.875rem;
    width: 0;
    height: 0;
    border-style: solid;
}
.thread-card::before {
    left: -9px;
    border-width: 8px 9px 8px 0;
    border-color: transparent var(--tc-arrow-outer) transparent transparent;
}
.thread-card::after {
    left: -8px;
    border-width: 8px 9px 8px 0;
    border-color: transparent var(--tc-arrow-inner) transparent transparent;
}

.thread-card-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.875rem;
    background: var(--tc-head-bg);
    border-bottom: 1px solid var(--tc-border);
    font-size: 0.8125rem;
}

.thread-card-author {
    font-weight: 600;
    color: var(--tc-author-color);
}

.thread-card-role {
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 0.0625rem 0.4375rem;
    border-radius: var(--bs-border-radius-pill);
    background: rgba(var(--bs-secondary-rgb), .14);
    color: var(--bs-info);
}

.thread-card-time {
    margin-left: auto;
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
}

.thread-card-body {
    padding: 0.875rem;
    color: var(--bs-body-color);
    line-height: 1.55;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
}

.thread-card-body-text {
    width: 100%;
    white-space: pre-wrap;
    word-wrap: break-word;
}

.thread-card-files {
    list-style: none;
    margin: 0;
    padding: 0 0.875rem 0.875rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
}

.thread-card-files a {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.625rem;
    background: var(--bs-secondary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-pill);
    font-size: 0.75rem;
    color: var(--bs-body-color);
    text-decoration: none;
    transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.thread-card-files a:hover {
    background: rgba(var(--bs-primary-rgb), .08);
    border-color: var(--bs-primary);
    color: var(--bs-primary);
}
.thread-card-files i { font-size: 0.6875rem; color: var(--bs-secondary-color); }
.thread-card-files a:hover i { color: var(--bs-primary); }

/* ── Event rows ──────────────────────────────────────────────────────────── */
.thread-row--event {
    align-items: flex-start;
}

/* Column-spacer that keeps the compact circle on the rail */
.thread-node {
    flex: 0 0 2.375rem;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    z-index: 1;
}

/* The visual circle sits on the inner <i> so it can be compact */
.thread-node i {
    flex-shrink: 0;
    width: 1.375rem;
    height: 1.375rem;
    border-radius: 50%;
    background: var(--bs-body-bg);
    border: 2px solid var(--bs-border-color);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--bs-secondary-color);
    font-size: 0.5rem;
}

.thread-event {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
    line-height: 1.5;
}

.thread-event-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.thread-event-head strong {
    color: var(--bs-body-color);
    font-weight: 600;
}

.thread-event-change {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    flex-wrap: wrap;
}

.thread-event-change em {
    font-style: normal;
    font-weight: 500;
    color: var(--bs-body-color);
}

.thread-event-arrow {
    font-size: 0.625rem;
    color: var(--bs-tertiary-color);
    opacity: 0.7;
}

.thread-event-reason {
    margin: 0;
    color: var(--bs-secondary-color);
    font-style: italic;
    font-size: 0.8125rem;
}

.thread-event-time {
    flex-shrink: 0;
    font-size: 0.75rem;
    color: var(--bs-tertiary-color);
    white-space: nowrap;
}

/* ── Message colour variants ─────────────────────────────────────────────── */
/* Override the CSS tokens defined on .thread-row--msg for each message type. */
/* Values cascade into .thread-card and all descendants automatically.        */

/* User messages - primary tint */
.thread-row--msg.is-user {
    --tc-bg:          rgba(var(--bs-primary-rgb), 0.04);
    --tc-head-bg:     rgba(var(--bs-primary-rgb), 0.08);
    --tc-border:      rgba(var(--bs-primary-rgb), 0.2);
    --tc-arrow-outer: rgba(var(--bs-primary-rgb), 0.25);
    --tc-arrow-inner: rgba(var(--bs-primary-rgb), 0.08);
}

/* Staff messages (public) - info/blue tint */
.thread-row--msg.is-staff {
    --tc-bg:          rgba(var(--bs-info-rgb), 0.04);
    --tc-head-bg:     rgba(var(--bs-info-rgb), 0.10);
    --tc-border:      rgba(var(--bs-info-rgb), 0.2);
    --tc-arrow-outer: rgba(var(--bs-info-rgb), 0.25);
    --tc-arrow-inner: rgba(var(--bs-info-rgb), 0.04);
}

/* -- Private staff notes - amber tint ----------------------------------------- */
/* Specificity (0,3,0) beats .is-staff (0,2,0) so amber always wins.         */
.thread-row--msg.is-staff.thread-row--private {
    --tc-bg:           rgba(var(--bs-warning-rgb), 0.06);
    --tc-head-bg:      rgba(var(--bs-warning-rgb), 0.14);
    --tc-border:       rgba(var(--bs-warning-rgb), 0.25);
    --tc-arrow-outer:  rgba(var(--bs-warning-rgb), 0.35);
    --tc-arrow-inner:  rgba(var(--bs-warning-rgb), 0.14);
    --tc-author-color: var(--bs-warning);
}

.thread-card-private-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--bs-danger);
    background: rgba(var(--bs-danger-rgb), 0.1);
    border: 1px solid rgba(var(--bs-danger-rgb), 0.35);
    border-radius: var(--bs-border-radius);
    padding: 0.1rem 0.375rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.thread-row--private.is-blurred .thread-card-body,
.thread-row--private.is-blurred .thread-card-files {
    filter: blur(4px);
    -webkit-user-select: none;
    user-select: none;
}

/* ── Archived colour tokens ───────────────────────────────────────────────── */
/* Set on the row so they beat the type-specific token declarations above.    */
/* The action menu button lives inside the card but is not targeted here.     */

/* Archived non-private: specificity (0,3,0) beats user/staff (0,2,0) */
.thread-row--msg.has-archived:not(.thread-row--private) {
    --tc-bg:          color-mix(in srgb, var(--bs-body-bg) 50%, transparent);
    --tc-head-bg:     color-mix(in srgb, var(--bs-secondary-bg) 50%, transparent);
    --tc-border:      color-mix(in srgb, var(--bs-border-color) 50%, transparent);
    --tc-arrow-outer: color-mix(in srgb, var(--bs-border-color) 50%, transparent);
    --tc-arrow-inner: color-mix(in srgb, var(--bs-secondary-bg) 50%, transparent);
}

/* Archived private: specificity (0,4,0) beats private rule (0,3,0).         */
/* Keep the amber tint but halve each alpha so the scheme stays recognisable. */
.thread-row--msg.has-archived.is-staff.thread-row--private {
    --tc-bg:          rgba(var(--bs-warning-rgb), 0.03);
    --tc-head-bg:     rgba(var(--bs-warning-rgb), 0.07);
    --tc-border:      rgba(var(--bs-warning-rgb), 0.125);
    --tc-arrow-outer: rgba(var(--bs-warning-rgb), 0.175);
    --tc-arrow-inner: rgba(var(--bs-warning-rgb), 0.07);
}

/* ── Archived messages ───────────────────────────────────────── */
/* Elements targeted individually - opacity cannot be restored on a child    */
/* once set on a parent (it composites), so the action menu stays at 100%.   */
/* Matches the same opacity + strikethrough pattern as .row-archived.        */

.thread-card--archived .thread-card-role,
.thread-card--archived .thread-card-private-badge,
.thread-card--archived .thread-card-archived-label { opacity: 0.5; }

.thread-card--archived .thread-card-author,
.thread-card--archived .thread-card-time,
.thread-card--archived .thread-card-body-text { opacity: 0.5; text-decoration: line-through; }

.thread-card-footer {
    display: flex;
    justify-content: flex-end;
    padding: 0.375rem 0.75rem 0.5rem;
}

.thread-card-archived-label {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
    background: rgba(var(--bs-secondary-rgb), 0.12);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    padding: 0.1rem 0.375rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Toggle bar shown above the thread when private notes exist */
.thread-private-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
}

.thread-private-toggle button {
    background: none;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    padding: 0.15rem 0.5rem;
    font-size: 0.75rem;
    cursor: pointer;
    color: var(--bs-secondary-color);
    transition: color 0.15s, border-color 0.15s;
}

.thread-private-toggle button:hover {
    color: var(--bs-body-color);
    border-color: var(--bs-secondary-color);
}

/* ── Reply mode toggle (admin: public reply vs private note) ─────────────── */
/* Same pill-toggle design as .user-menu-toggle in ez-platform-nav.css - keep the
   two in sync if the toggle look ever changes. */
.reply-mode-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;
}

.reply-mode-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);
}

.reply-mode-toggle.is-on { background: var(--bs-warning); }
.reply-mode-toggle.is-on::after { transform: translateX(1rem); }

/* Hide private notes when printing - Print media (not a width breakpoint) */
@media print {
    .thread-row--private {
        display: none !important;
    }

    .thread-private-toggle {
        display: none !important;
    }
}


/* =============================================================
   Permission template matrix + member permission overrides
   (permission-templates editor and the member Permissions page)
   ============================================================= */

/* ── Roles x permissions matrix (template editor) ───────────── */
.perm-matrix-table { margin-bottom: 0; }

.perm-matrix-table th,
.perm-matrix-table td {
    vertical-align: middle;
    border-color: var(--bs-border-color);
}

/* Header rows hold the role definitions - tint to group them. */
.perm-matrix-table thead th { background: var(--bs-tertiary-bg); }

/* First column (permission name / "Role") stays pinned during horizontal scroll. */
.perm-matrix-table th.perm-matrix-rowhead,
.perm-matrix-table td.perm-matrix-rowhead {
    position: sticky;
    left: 0;
    z-index: 1;
    text-align: left;
    min-width: 14rem;
}
.perm-matrix-table th.perm-matrix-rowhead { background: var(--bs-tertiary-bg); }
.perm-matrix-table td.perm-matrix-rowhead { background: var(--bs-body-bg); }

/* Role-definition inputs in the header cells. */
.perm-matrix-table .role-name-select,
.perm-matrix-table .role-display-input,
.perm-matrix-table .role-scope-select {
    min-width: 8.5rem;
    max-width: 12rem;
    margin-inline: auto;
}

/* Role columns: even width with a faint divider for cross-row scanning. */
.perm-matrix-table thead th:not(.perm-matrix-rowhead) { min-width: 9rem; }
.perm-matrix-table thead th:not(.perm-matrix-rowhead),
.perm-matrix-table tbody tr.perm-row td:not(.perm-matrix-rowhead) {
    border-left: 1px solid var(--bs-border-color);
}

/* Larger, easier-to-hit checkboxes. */
.perm-matrix-table tbody .form-check-input {
    width: 1.15rem;
    height: 1.15rem;
    margin: 0;
    cursor: pointer;
}

/* Category divider rows. */
.perm-matrix-table .perm-category-row td {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}

/* Row hover (not the pinned first column, which must stay opaque). */
.perm-matrix-table tbody tr.perm-row:hover td:not(.perm-matrix-rowhead) {
    background: rgba(var(--bs-primary-rgb), 0.04);
}

/* ── Member permissions page ────────────────────────────────── */
/* Resolution summary: a brand accent so the "answer" reads first. */
.resolution-summary { border-left: 3px solid var(--bs-primary); }

/* "Browse all permissions" table column widths (classes, not inline styles). */
.member-perm-table th.perm-col-narrow { width: 7.5rem; }
.member-perm-table th.perm-col-actions { width: 13rem; }

/* ── Modals ─────────────────────────────────────────────────── */
/* Bootstrap's default dialog margin is 0.5rem, which on a phone reads as edge-to-edge - a
   confirmation ends up touching both screen edges. Raise it below sm so every modal keeps a
   visible gutter. Overriding the VARIABLE (rather than .modal-dialog's margin) is deliberate:
   Bootstrap also feeds it into the centred dialog's min-height calc, so the two stay in step.
   From sm up Bootstrap already uses 1.75rem.
   0.75rem rather than the 1.25rem this started at: still a clear gutter, but 1rem more width for the
   content, which on a 360px screen is what a photo row or a form field needs. */
/* Every modal (EzModal renders Bootstrap's .modal) keeps its dialog clear of the status bar and home
   indicator in the edge-to-edge mobile app: the inset goes on the full-screen .modal layer, so a tall or
   scrollable dialog stops below the clock and above the home bar. env() is 0 everywhere else, so this
   changes nothing in a browser. One rule for the whole app, rather than a fix per dialog. */
.modal {
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
}
@media (max-width: 575.98px) {
    .modal {
        --bs-modal-margin: 0.75rem;
        --bs-modal-header-padding: 0.75rem 1rem;
    }
    /* The modal heading uses the page-heading scale (1.5rem bold capitals), so on a phone a three-word
       title ("HOW IT WAS MADE") filled the width and ran into the close button. One step down, with
       the X lined up with the title's FIRST line if it still wraps (Bootstrap centres it vertically,
       which floats it between two lines). 1.25rem x 1.2 line-height = 1.5rem, the btn-close's own
       height, so on one line the two still sit level. */
    .modal-header { align-items: flex-start; }
    .modal-header .modal-title.ez-title-2 {
        font-size: 1.25rem;
        line-height: 1.2;
        letter-spacing: -0.02em;
    }
    .modal-header .btn-close { margin-top: 0; margin-bottom: 0; }
}

/* ── Reduced motion (WCAG 2.3.3 / prefers-reduced-motion) ───────────────
   Users who ask their OS for reduced motion get a near-instant, non-animated
   experience: reconnect ripple, toast/drawer slide-ins, notification-banner
   bounce, skeleton shimmer and every transition collapse to imperceptible.
   The standard defensive reset - scoped to the media query, so it only ever
   applies when the user opted in. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ── EzTable sticky header ──────────────────────────────────────────────
   Applied to the .table-responsive wrapper by EzTable's Sticky parameter.
   Capping the height turns that wrapper into the scrollport, which is what
   gives the header something to pin to.

   The header cells carry their own background because EzTable leaves <thead>
   unclassed unless a Variant is set - a transparent pinned row would let the
   body rows scroll through it. Reading --bs-table-bg rather than --bs-body-bg
   keeps the tint when a Variant IS set, since table-{variant} sets that var
   and .table defaults it to the body background.

   The divider is an inset line, not a border: Reboot collapses table borders,
   so a th's own border belongs to the table and scrolls out from under the
   pinned row. 1px, no blur, no offset - a hairline rule, not a depth shadow. */
.ez-table-scroll--sticky {
    max-height: 70vh;
    overflow-y: auto;
}

.ez-table-scroll--sticky thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background-color: var(--bs-table-bg);
    box-shadow: inset 0 -1px 0 var(--bs-border-color);
}

/* ── EzTable loading skeleton ───────────────────────────────────────────
   Placeholder rows shown while a table loads (EzTable IsLoading + SkeletonRows).
   The bar shimmers left-to-right; each row starts fractionally later via --i,
   set as a custom property on the element so no rule is written inline. */
.ez-table-skeleton-bar {
    display: block;
    height: 0.85rem;
    border-radius: var(--bs-border-radius-sm);
    /* The highlight is --bs-border-color, NOT a surface token. This used to sweep
       --bs-secondary-bg into --bs-tertiary-bg, which worked only while those two differed;
       the pure-black dark surfaces made them identical and the shimmer vanished on every
       list page in the app. Border colour is the one muted token guaranteed to stay visible
       against any surface in both themes, and it is the same recipe as the card-fields
       skeleton (ezPayments.css), so loading looks like ONE language everywhere. */
    background: linear-gradient(90deg,
        var(--bs-body-bg) 25%,
        var(--bs-border-color) 37%,
        var(--bs-body-bg) 63%);
    background-size: 400% 100%;
    animation: ez-table-skeleton 1.4s ease-in-out infinite;
    animation-delay: calc(var(--i, 0) * 0.08s);
}

@keyframes ez-table-skeleton {
    from { background-position: 100% 50%; }
    to   { background-position: 0 50%; }
}

/* ============================================================
   Bar fill steps - .ez-bar-step--{n}
   ============================================================
   A progress or meter fill is a WIDTH, and a width only known at runtime used
   to reach the element as a style attribute - which this platform forbids
   outright. The way out is the one the rule itself names: the value comes from
   a small fixed set, so it is a MODIFIER CLASS rather than a value. Every bar
   here shows a percentage, so the set is 0-100 and the only decision left is
   the granularity.

   Five points is that granularity. A thin bar is a few hundred pixels wide at
   most, so a 5% step is a handful of pixels - narrower than the rounded cap on
   either end - and 21 classes is a block anyone can read at a glance.

   EzBarStep.Class(percent) in EzyPlatform.Components snaps a percentage to one
   of these, and EzBarStep.Class(completed, total) does the division first. Never
   add a bespoke width rule for one bar; widen the scale (here AND in
   EzBarStep.StepSize) if a surface ever genuinely needs finer steps. */

.ez-bar-step--0   { width: 0; }
.ez-bar-step--5   { width: 5%; }
.ez-bar-step--10  { width: 10%; }
.ez-bar-step--15  { width: 15%; }
.ez-bar-step--20  { width: 20%; }
.ez-bar-step--25  { width: 25%; }
.ez-bar-step--30  { width: 30%; }
.ez-bar-step--35  { width: 35%; }
.ez-bar-step--40  { width: 40%; }
.ez-bar-step--45  { width: 45%; }
.ez-bar-step--50  { width: 50%; }
.ez-bar-step--55  { width: 55%; }
.ez-bar-step--60  { width: 60%; }
.ez-bar-step--65  { width: 65%; }
.ez-bar-step--70  { width: 70%; }
.ez-bar-step--75  { width: 75%; }
.ez-bar-step--80  { width: 80%; }
.ez-bar-step--85  { width: 85%; }
.ez-bar-step--90  { width: 90%; }
.ez-bar-step--95  { width: 95%; }
.ez-bar-step--100 { width: 100%; }

/* ============================================================
   .ez-media-actions - the icon-action row laid over media
   ============================================================
   The cluster of sibling icon buttons that sits on top of a photo or a video:
   EzImageViewer's lightbox close, VCR's compare overlay and result rating.
   Faint at rest so it never competes with the image, solid the moment the
   pointer, the keyboard or a touch screen asks for it.

   This base rule was missing entirely. Three sheets already carried a
   ":hover .ez-media-actions { opacity: 1 }" reveal (ez-platform-upload.css,
   ez-platform-image-viewer.css, VCR's tryon.css) and all three were dead - there
   was nothing faint to bring up, so the buttons simply sat at full strength over
   the picture. POSITIONING is deliberately not here: each host places its own
   cluster (.ez-imgviewer-close, .to-cmp-close, .st-rate), and this
   class only owns the reveal. */

.ez-media-actions {
    opacity: 0.55;
    transition: opacity 0.15s ease-in-out;
}

/* Hovering the cluster itself, and the keyboard, which never hovers at all.
   The per-host rules that reveal it from the surrounding media live with those
   hosts and win on specificity. */
.ez-media-actions:hover,
.ez-media-actions:focus-within,
.ez-media-actions:focus-visible {
    opacity: 1;
}

/* A touch screen has no hover to give, so a faint control there is simply a
   control the user cannot find. */
@media (hover: none) {
    .ez-media-actions { opacity: 1; }
}

/* ── Live password rules (PasswordRules.razor + layout.js) ─────────────
   The configured policy as pills under a new-password field. Neutral until
   the person types; then each rule still missing is red and each one met
   is green with a tick, so what is left is what stands out. Colour is never
   the only signal: the tick and the weight change carry it too. */
.ez-password-rules { display: flex; flex-wrap: wrap; gap: .375rem; margin-top: .25rem; font-style: normal; }
.ez-password-rules [data-rule] {
    font-size: .75rem;
    line-height: 1.5;
    padding: .0625rem .625rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-pill);
    color: var(--bs-secondary-color);
    transition: color .15s, border-color .15s, background-color .15s;
}
.ez-password-rules.typing [data-rule] {
    border-color: var(--bs-danger-border-subtle);
    color: var(--bs-danger-text-emphasis);
    font-weight: 600;
}
.ez-password-rules.typing [data-rule].met {
    border-color: var(--bs-success-border-subtle);
    background-color: var(--bs-success-bg-subtle);
    color: var(--bs-success-text-emphasis);
    font-weight: 400;
}
/* The tick is decoration for sighted users; the alt text after "/" keeps it out of the accessible name.
   The plain declaration first is the fallback for a browser without the alt-text syntax. */
.ez-password-rules.typing [data-rule].met::before { content: "\2713\00a0"; content: "\2713\00a0" / ""; }


/* ===== 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;
}

/* ── 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;
    /* Keeps the layers below (glyph 0, backdrop 0, image 1) inside the frame. Without it the
       image's z-index 1 competes with the page's own z-index 1 layers - in a table, the pinned
       actions cell - and a later row's photo paints over an open action menu. */
    isolation: isolate;
}

/* 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);
    /* The studio cards' darker backdrop: at 0.55 a grey studio wall blurred into a band the same
       value as the photo, and the subject stopped standing out. Matches the filled upload tile. */
    filter: blur(var(--ez-img-blur)) brightness(0.32) saturate(1.2);
}

/* 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; }

/* Viewable (EzImage Viewable): the frame is a <button> that opens the full-screen viewer. Reset
   the button's own padding and colour so it renders exactly like the plain frame. */
.ez-img-frame--viewable {
    padding: 0;
    color: inherit;
    cursor: zoom-in;
}

/* 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);
}

/* ── Password show/hide toggle ─────────────────────────────── */
/* The eye beside a password (EzTextInput and the static login page, both marked
   data-ez-password-toggle) is the right-hand half of the FIELD, not a loose button:
   it takes the input's own fill and hairline border, follows the group's hover and
   focus border, and answers the pointer itself - a lifted fill and a brighter glyph on
   hover, a deeper fill and the brand glyph while pressed or focused. It rides on the
   Bootstrap --bs-btn-* variables EzButton already composes from, so nothing fights
   the borderless tier it is rendered as. */
[data-theme-forms="on"] .input-group > [data-ez-password-toggle] {
    --bs-btn-bg:                  var(--bs-body-bg);
    --bs-btn-border-color:        var(--bs-border-color);
    --bs-btn-color:               var(--bs-secondary-color);
    --bs-btn-hover-bg:            var(--bs-tertiary-bg);
    --bs-btn-hover-border-color:  var(--ez-field-border-hover);
    --bs-btn-hover-color:         var(--bs-body-color);
    --bs-btn-active-bg:           var(--bs-secondary-bg);
    --bs-btn-active-border-color: var(--ez-field-border-focus);
    --bs-btn-active-color:        var(--bs-primary);
    transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out, color 0.15s ease-in-out;
}
[data-theme-forms="on"] .input-group:hover:not(:focus-within) > [data-ez-password-toggle] {
    border-color: var(--ez-field-border-hover);
}
[data-theme-forms="on"] .input-group:focus-within > [data-ez-password-toggle] {
    border-color: var(--ez-field-border-focus);
}
/* Keyboard focus: the whole group already rings as one (focus-within above), so the
   button's own offset outline would draw a second box inside the first. The brand
   glyph and fill mark which half has focus instead. */
[data-theme-forms="on"] .input-group > [data-ez-password-toggle]:focus-visible {
    outline: none;
    background-color: var(--bs-secondary-bg);
    color: var(--bs-primary);
}

/* 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: the
   bulk-selection cells and the permission matrix have no .form-check wrapper, so none of this
   reaches them). Radios keep the smaller box - this is the checkbox rule. */
[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; }

/* A form checkbox is a 24px box (it was 18px, under the 24px floor in WCAG 2.5.8) inside a 44px
   tappable area. The area is a transparent pseudo-element rather than padding on the input:
   padding would grow the BORDER box, and the tick, the border and the focus ring are all painted
   on it, so the drawn box would have grown with it. Nothing about the control's appearance
   changes - only where a finger may land. The row grows to 44px too, so two stacked checkboxes
   cannot eat into each other's target.
   :not(.form-switch) because a switch is a checkbox as well, and it keeps its own track. */
[data-theme-forms="on"] .form-check:not(.form-switch):has(> .form-check-input[type="checkbox"]) {
    min-height: 2.75em;   /* 44px */
    padding-left: 2.25em; /* the 1.5em box plus a 0.75em gap before the label */
}
[data-theme-forms="on"] .form-check:not(.form-switch) > .form-check-input[type="checkbox"] {
    width: 1.5em;         /* 24px */
    height: 1.5em;
    margin-left: -2.25em;
    margin-top: 0.625em;  /* centres the box on the 44px row */
    position: relative;
}
[data-theme-forms="on"] .form-check:not(.form-switch) > .form-check-input[type="checkbox"]::before {
    content: "";
    position: absolute;
    inset: -0.625em;      /* 1.5em + 2 x 0.625em = 2.75em square */
}
/* First line of the label sits level with the middle of the box, not the top of the row. */
[data-theme-forms="on"] .form-check:not(.form-switch):has(> .form-check-input[type="checkbox"]) > .form-check-label {
    padding-top: 0.625em;
}

/* 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; }

/* ── EzDateOfBirthInput: Day / Month / Year ─────────────────── */
/* Not theme-gated: this is layout, not look. One row on a phone at normal text size; the basis is
   in em, so at a large SYSTEM font size (which the Android WebView follows) the parts wrap onto a
   second line rather than clipping the digits. Each basis is the part's content plus its padding
   and, for the month, the select arrow. */
.ez-dob-parts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.ez-dob-part { min-width: 0; }
.ez-dob-part--day   { flex: 3 1 3.25em; }
.ez-dob-part--month { flex: 5 1 5.75em; }
.ez-dob-part--year  { flex: 4 1 4.75em; }


/* ===== 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: var(--ez-on-primary); }
.toolbar-btn.btn[aria-expanded="true"]:hover,
.toolbar-btn.btn[aria-pressed="true"]:hover { background: var(--bs-primary); border-color: var(--bs-primary); color: var(--ez-on-primary); }

/* 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);
}
/* Bootstrap < sm: drop the banner just below the header instead of hanging it over the bell and the
   credit badge it is usually about - a floating card now, so all four corners and a top border.
   max() keeps it on screen once the header has scrolled away; without anchor positioning it stays
   a card at the top of the screen. Toasts live at the bottom (see .ez-toast-host), so the two never meet. */
@media (max-width: 575.98px) {
    .notif-banner {
        top: calc(0.75rem + env(safe-area-inset-top));
        border-top: 1px solid rgba(56, 189, 248, 0.3);
        border-radius: var(--bs-border-radius-lg);
    }
    @supports (anchor-name: --test) {
        .notif-banner {
            top: max(calc(0.75rem + env(safe-area-inset-top)), calc(anchor(--ez-page-header bottom, 0px) + 0.5rem));
        }
    }
}
@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: var(--ez-on-primary);
}
.user-card-wrap button.user-card[aria-expanded="true"]:hover {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: var(--ez-on-primary);
}
.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: var(--ez-on-primary);
    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: var(--ez-on-primary);
    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: var(--ez-on-primary); border-color: var(--bs-primary); }
.user-menu-switch.is-open { background: var(--bs-primary); color: var(--ez-on-primary); 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: var(--ez-on-primary); }

.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); }

/* Header actions below md (Bootstrap < md): the user card is avatar-only (the name/business text and
   the chevron are hidden by the d-none/d-md-* utilities on the markup). With the page title hidden
   the header row has room to spare, so the bell, avatar and credit pill grow to a 44px target (the
   WCAG 2.2 touch size). Inside that box the CONTENT is what grows (glyph, initials, number), not the
   padding: a big box around a small icon read as empty frames.

   The ceiling is the WIDTH of the row, not taste: logo + credit pill + bell + avatar must fit one line
   on a ~360px phone with a four-digit balance ("9,999"), or the actions wrap under the logo and push
   the whole studio down. A 48px box with a 1.625rem glyph wrapped at "2,971" on a phone with enlarged
   system text; 44px with 1.125rem leaves room for "9,999" there.

   Sized through each control's own FONT-SIZE, because that is what the button system scales from:
   ez-platform-buttons.css gives a button's glyph `font-size: inherit` at 1em, and sizes the avatar
   disc at 2.45em of 0.55em of the button. Setting the glyph or the disc directly lost to those rules,
   which is why an earlier pass here changed nothing on screen. One shared value (--ez-header-glyph)
   gives the bell glyph and the credit badge's coin + number the SAME size; the avatar disc then fills
   its box, its initials at the button system's own ratio. */
@media (max-width: 767.98px) {
    .header-actions { --ez-header-glyph: 1.125rem; }
    /* Bootstrap < sm: 1rem, so the credit pill showing "9,999" (~5.5rem) still fits the one-line header
       row at the 320px floor (see .page-header column-gap in ez-platform.css for the rest of the budget). */
    @media (max-width: 575.98px) {
        .header-actions { --ez-header-glyph: 1rem; }
    }
    .header-actions .user-card.btn {
        justify-content: center; padding: 0; width: 2.75rem; height: 2.75rem; max-width: none;
        /* Disc = 1.35 x this = 2.36rem (0.2rem inside the box each side); initials 0.55 x = 0.96rem. */
        font-size: 1.75rem;
        /* ez-platform-buttons.css gives every .btn a min-height of 1.5em + padding, in em - so the big
           font-size above that sizes the disc also raised the button's floor to ~3.4rem, and the avatar
           box stood taller than the bell and credit pill beside it. The box is pinned, so drop the floor. */
        min-height: 0;
    }
    .header-actions .toolbar-btn.btn { width: 2.75rem; height: 2.75rem; min-height: 0; font-size: var(--ez-header-glyph); }
}

/* 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(--ez-side-bg);
    color: var(--ez-side-fg);
    box-shadow: var(--bs-box-shadow);
    transition: left .25s ease;
    overflow-y: auto;
    padding: 0.875rem;
    border-right: 1px solid var(--ez-side-border);
    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(--ez-side-muted);
    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(--ez-side-fg); }

.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(--ez-side-border); margin: 0.625rem 2px; }

.side-menu-pad { height: 3rem; }

/* Menu items inside the sidebar take the theme's sidebar tokens (--ez-side-*), so a theme can give the
   sidebar its own ground - Harbour's navy, Clarity's blue - without touching .menu-link elsewhere (the
   user menu keeps the page surface). With the original theme these resolve to the same values as the
   plain .menu-link rules above. */
.side-menu .menu-link { color: var(--ez-side-fg); }
.side-menu .menu-link i { color: var(--ez-side-muted); }
.side-menu .menu-link:hover { background: var(--ez-side-hover-bg); color: var(--ez-side-active-fg); }
.side-menu .menu-link.active { background: var(--ez-side-active-bg); color: var(--ez-side-active-fg); }
.side-menu .menu-link:hover i,
.side-menu .menu-link.active i { color: var(--ez-side-active-fg); }

/* A theme whose sidebar stays dark in light mode (BrandTheme.DarkSidebar) gets .side-menu--dark from
   NavMenu: show the dark logo there and keep the close button visible, as dark mode does page-wide. */
.side-menu--dark .brand-logo--light { display: none; }
.side-menu--dark .brand-logo--dark  { display: block; }
.side-menu--dark .btn-close { filter: invert(1) grayscale(100%) brightness(200%); }

/* 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); }

/* Bootstrap < md: a strip with more tabs than fit scrolls sideways instead of wrapping. Wrapped, a
   six-tab account strip became two ragged rows at 320-375px with the active underline floating in
   the middle of the block; scrolled, it stays one row under one rule and the active tab is
   scrolled into view by ezTabs.js when it changes. The scrollbar is hidden (the strip is short and
   the overflow is discoverable by the cut-off tab at the edge), and the tabs must not shrink or
   wrap their labels, or the row simply squeezes instead of overflowing. */
@media (max-width: 767.98px) {
    .nav-tabs {
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }
    .nav-tabs::-webkit-scrollbar { display: none; }
    .nav-tabs .nav-item { flex: none; }
    .nav-tabs .nav-link { white-space: nowrap; }
}

/* ============ 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);
    /* Transparent, not removed: the 1px stays in the bar's height, so nothing that budgets for the
       bar (the content clearance, studio.js's band) moves. The visible line is the ::before below. */
    border-top: 1px solid transparent;
    /* Reserve the gesture-nav inset so the row sits above the system nav bar. */
    padding-bottom: env(safe-area-inset-bottom);
}
/* The same rule as the one under the header (.page-title::before in ez-platform.css) and the VCR
   studio's step strip: 3px in the faded brand colour, running edge to edge with square ends, so the
   screen is framed top and bottom by one matching line. */
.bottom-tabs::before {
    content: "";
    position: absolute;
    top: -1px;
    left: 0;
    right: 0;
    height: 3px;
    background: rgba(var(--bs-primary-rgb), 0.22);
    pointer-events: none;
}
.bottom-tab {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Icon and label sit 2px lower in the bar: 2px more above the icon, 2px less below the label, so
       the bar's height is unchanged. */
    gap: 0.1875rem;
    padding: 0.625rem 0.25rem 0.3125rem;
    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;
}
/* EzCreditBalance adds this once when a live spend carries the balance into the low (amber) range:
   two soft pulses to catch the eye, since that badge now carries the low-credit notice on its own
   (the notice's arrival banner is off). Flat ring, no blur, per the no-shadows design. */
.ez-credit-pulse { animation: ez-credit-pulse 0.9s ease-out 2; }
@keyframes ez-credit-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(var(--bs-warning-rgb), 0.7); transform: scale(1); }
    40%  { transform: scale(1.08); }
    100% { box-shadow: 0 0 0 0.5rem rgba(var(--bs-warning-rgb), 0); transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .ez-credit-pulse { animation: none; }
}
/* Bootstrap < md: the same 44px target as the bell and avatar beside it (see .user-card above). */
@media (max-width: 767.98px) {
    /* Same glyph size as the bell beside it (--ez-header-glyph on .header-actions), with only a hair
       of padding: the coin and the number are the content, the pill is just their frame. */
    .header-actions .billing-badge { min-height: 2.75rem; padding-inline: 0.375rem; font-size: var(--ez-header-glyph, 1.125rem); }
}

@media (min-width: 1200px) {
    .bottom-tabs { display: none; }
}

/* Safe-area + bottom-bar clearance (< xl only). The top inset keeps everything clear of the status
   bar; bottom padding keeps the last content above the fixed tab bar and the gesture-nav inset
   (app surface only - the admin console has no tab bar). */
@media (max-width: 1199.98px) {
    /* 4.25rem is the bar's own height; the extra 1.5rem is breathing room, so the last row of a page
       ends above the bar instead of sitting flush on its top border. */
    .surface-app .main {
        padding-bottom: calc(4.25rem + 1.5rem + env(safe-area-inset-bottom));
    }

    /* The status-bar inset sits on the page itself, above the strips ChromeLayout stacks over the layout
       (impersonation, announcements, confirm-your-email), not on .main below them: on the iOS app a
       banner there rendered under the clock. Covers the admin console too, which had no inset at all. */
    body:has(.layout) { padding-top: env(safe-area-inset-top); }

    /* 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)); }
    /* ...and runs to the true bottom of the screen with its last link above the home indicator. Pinned
       top AND bottom rather than 100vh tall: in the iOS app's WebView 100vh stops short of the bottom. */
    .side-menu {
        bottom: 0;
        height: auto;
        padding-bottom: calc(0.875rem + env(safe-area-inset-bottom));
    }

    /* 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;
    /* Its content stays above the home indicator in the edge-to-edge mobile app (0 elsewhere). */
    padding-bottom: env(safe-area-inset-bottom);
    z-index: 9998;
    background: #1a0f0f;
    border-top: 2px solid #ff6868;
    /* The shared elevation token, not a hand-rolled value. The red glow this replaces was the
       only shadow in the platform mixed from a literal colour, so it never followed the theme
       and never followed a decision to flatten the app (set the three tokens to none). The
       2px top border is what separates the bar from the page; the shadow only ever softened it. */
    box-shadow: var(--bs-box-shadow-lg);
    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: var(--ez-on-primary, #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.
   ============================================================= */

/* Ink on the coloured chips: white suits the strong status fills, but the primary fill takes the
   theme's own ink, and warning and info are too light for white (the dark amber is the one the
   filled warning and info buttons use). The demo dashboard's chips share these rules. */
:is(.dash-stat-icon, .dash-cmd-icon, .dash-rank-pos).bg-primary { color: var(--ez-on-primary); }
:is(.dash-stat-icon, .dash-cmd-icon, .dash-rank-pos):is(.bg-warning, .bg-info) { color: #431407; }

.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 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 {
    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;
}

/* The primary fill takes its ink from the theme: white on the original violet, a dark ink on a theme
   whose primary is light (most dark palettes, and Slate's near-white). The status colours keep white
   or the dark amber below, which their fixed Bootstrap hues were measured against. */
.btn-ez-filled.btn-ez-primary {
    --bs-btn-color:          var(--ez-on-primary);
    --bs-btn-hover-color:    var(--ez-on-primary);
    --bs-btn-active-color:   var(--ez-on-primary);
    --bs-btn-disabled-color: var(--ez-on-primary);
}

/* #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: the platform's over-image button (an outline-secondary EzButton, which carries its
   own opaque background), positioned top right. This wrapper only places it. */
.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);
}

/* EzUpload Requirements: what makes a good file ("Full length, one person, good light"). Reads as
   guidance under the heading (secondary body colour via text-body-secondary on the element, one size
   down) - the hint below it carries the small print. Balanced so a two-line requirement does not
   leave one word on its own. A "\n" in the text is a deliberate line break ("Full body shot\n(1 person
   only)"), and the block sits apart from the heading above and the hint below. */
.ez-upload-req {
    display: block;
    max-width: 18rem;
    margin: 0.5rem auto;
    font-size: 0.875rem;
    text-wrap: balance;
    white-space: pre-line;
}

/* 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;
}

/* Filled, the tile IS the platform image frame (.ez-img-frame in ez-platform-utilities.css): the
   same hairline border, ground, backdrop and edge-to-edge contain fit, so an uploaded photo and a
   displayed one are the same object. Keep the two in step. */
.ez-upload-zone--tile.ez-upload-zone--filled {
    padding: 0;
    overflow: hidden;
    cursor: default;
    border: 1px solid var(--bs-border-color);
    background: var(--bs-secondary-bg);
}

/* Viewable: 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.32) saturate(1.2);
}

/* Edge to edge, like the studio cards: a 3:4 photo fills the frame and the backdrop only shows
   where a photo of another shape leaves space. The frame clips the corners, so no radius here. */
.ez-upload-tile-link {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}

.ez-upload-tile-img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
}

/* The filled tile's one remove, top right, always at full strength. Hidden while the upload is
   disabled (e.g. locked during generation). */
.ez-upload-tile-close {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 3;
}
.ez-upload--disabled .ez-upload-tile-close {
    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;
    /* Safe-area insets: clear of the status bar and home indicator in the edge-to-edge mobile app. */
    padding: calc(2rem + env(safe-area-inset-top)) 2rem calc(2rem + env(safe-area-inset-bottom));
    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: calc(1rem + env(safe-area-inset-top));
    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-compare.css ===== */
/* ez-platform-compare.css - the before/after compare overlay (EzCompareOverlay).
   A full-screen takeover in the same family as the EzImageViewer lightbox. Always dark - it hosts
   photos edge to edge - so the captions and labels use fixed light inks rather than theme tokens.
   The zoom/pan and wipe behaviour lives in EzyPlatform.Components/wwwroot/ezCompare.js. */

/* The safe-area insets keep the toolbar and captions clear of the status bar and home indicator in the
   edge-to-edge mobile app; env() is 0 everywhere else. */
.ez-cmp { position: fixed; inset: 0; z-index: 1090; display: flex; flex-direction: column; align-items: center;
    gap: 1rem; padding: calc(1.25rem + env(safe-area-inset-top)) 1.25rem calc(1.25rem + env(safe-area-inset-bottom));
    background: rgba(0, 0, 0, 0.9); cursor: zoom-out;
    animation: ez-cmp-fade 0.15s ease-out; }
@keyframes ez-cmp-fade { from { opacity: 0; } to { opacity: 1; } }
.ez-cmp-close { position: fixed; top: calc(1rem + env(safe-area-inset-top)); right: 1.25rem; z-index: 2; }
.ez-cmp:hover .ez-media-actions { opacity: 1; }
.ez-cmp-toggle { display: flex; align-items: center; gap: 0.5rem; cursor: default; }
/* zoom cluster: separated from the mode buttons by a thin divider (flat, matching the dark overlay) */
.ez-cmp-zoomctl { display: inline-flex; align-items: center; gap: 0.35rem;
    margin-left: 0.5rem; padding-left: 0.85rem; border-left: 1px solid rgba(255, 255, 255, 0.25); }
/* one body box for both modes: fills the remaining height and CLIPS the zoomed stage. The stage
   inside carries the zoom/pan transform; the labels sit on the body so they never scale. */
.ez-cmp-body { position: relative; flex: 1 1 0; min-height: 0; width: 100%; overflow: hidden;
    display: flex; align-items: center; justify-content: center; cursor: default; touch-action: none; }
/* ezCompare.js publishes the zoom/pan as --cmp-z / --cmp-x / --cmp-y; the transform is declared here. */
.ez-cmp-stage { display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;
    will-change: transform;
    transform: translate(var(--cmp-x, 0px), var(--cmp-y, 0px)) scale(var(--cmp-z, 1)); }
.ez-cmp-zoomed, .ez-cmp-zoomed .ez-cmp-wipe { cursor: grab; }
/* side by side: the two contain-fitted images sit FLUSH against each other (no gap) - each element
   hugs its painted image (height-driven width), so the seam is real edge-to-edge contact. Only the
   outer corners are rounded, reading as one combined before/after frame. */
.ez-cmp-img { height: 100%; max-width: 44vw; object-fit: contain; }
.ez-cmp-img--left { border-radius: var(--bs-border-radius) 0 0 var(--bs-border-radius); }
.ez-cmp-img--right { border-radius: 0 var(--bs-border-radius) var(--bs-border-radius) 0; }
/* wipe slider: both images stacked in one contain-fitted box; the image on top is clipped from the
   right by --cmp (before left, after right) and the divider tracks the same var. Pointer drags are
   handled in ezCompare.js (wipe vs pan); the invisible range input remains for keyboard arrows only.

   The box must HUG the painted image, never be a wider frame the image is letterboxed inside: the
   clip, the divider and the drag maths are all expressed against the box, so any empty band beside
   the image is a band the divider can be dragged into. ezCompare.js measures both images and
   publishes the narrower aspect ratio as --cmp-ar, which sizes the box: height-driven normally, and
   width-driven via max-height once the width cap binds - an exact fit with no letterbox either way. */
.ez-cmp-wipe { position: relative; flex: none; --cmp: 50%; --cmp-ar: 0.75; cursor: ew-resize;
    height: 100%; width: auto; aspect-ratio: var(--cmp-ar);
    max-width: min(92vw, 60rem); max-height: calc(min(92vw, 60rem) / var(--cmp-ar)); }
.ez-cmp-wipe img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.ez-cmp-over { clip-path: inset(0 calc(100% - var(--cmp)) 0 0); }
.ez-cmp-range { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0;
    pointer-events: none; }
/* Flat design: solid 2px divider + a solid brand grip chip, no glow/shadow. pointer-events off so the
   range input beneath receives every drag. */
.ez-cmp-bar { position: absolute; top: 0; bottom: 0; left: var(--cmp); width: 2px; margin-left: -1px;
    background: var(--bs-primary); pointer-events: none; }
.ez-cmp-grip { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 2.2rem; height: 2.2rem; border-radius: 50%; background: var(--bs-primary); color: var(--ez-on-primary);
    display: flex; align-items: center; justify-content: center; font-size: 0.85rem; }
/* keyboard focus on the invisible range shows as a ring on the grip chip (flat 0-blur ring) */
.ez-cmp-range:focus-visible ~ .ez-cmp-bar .ez-cmp-grip { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.85); }
/* labels: pinned to the body either side of the centre seam (outside the stage, so zooming never
   scales them) - the before label always left, the after label always right in both modes */
.ez-cmp-tag { position: absolute; bottom: 0.6rem; z-index: 1; padding: 0.2rem 0.55rem; border-radius: var(--bs-border-radius);
    font-size: 0.72rem; font-weight: 600; color: #fff; background: rgba(0, 0, 0, 0.7); pointer-events: none; }
.ez-cmp-tag--left { right: calc(50% + 0.6rem); }
.ez-cmp-tag--right { left: calc(50% + 0.6rem); }

@media (prefers-reduced-motion: reduce) {
    .ez-cmp { 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;
}

