/* ContentVelo brand + component layer — the one source of truth.
 *
 * This file is the master. It is copied byte-for-byte into all three apps by
 * `python tools/ui-rebuild/sync-brand.py`, and `brandcheck.py` fails the build if the copies
 * drift or if a brand hex appears anywhere else in the product.
 *
 * Every colour here is derived from docs/brand/contentvelo-logo-c-master.png, whose three
 * ribbons sample as #5145F5 indigo, #06ECFE cyan and #AF63FB violet. The logo's own values are
 * kept for fills, where they are seen against a surface rather than read as text. Where a value
 * has to be *read*, it is a measured variant instead: pure #06ECFE is 1.36:1 on a light page,
 * which is not a colour anybody can read, and shipping it as link text would have been the
 * predictable way to lose the contrast the last audit won.
 *
 * Every pairing below is measured, not judged. The numbers in the comments are real ratios
 * against the surface named.
 *
 * No Tailwind here. The CDN's JIT only rewrites classes it finds in markup, so `@apply` in a
 * static stylesheet silently does nothing. Plain CSS on custom properties also means the
 * component layer survives the day this project stops using a CDN build.
 */


/* Colours are declared as bare channels — `81 69 245`, not `#5145F5`.
 *
 * Tailwind's alpha modifier (`bg-primary/10`, `ring-primary/50`) is compiled by substituting
 * into an `<alpha-value>` placeholder, which only works if the colour arrives as channels. The
 * markup in this product uses that modifier in about two hundred places, so a token holding a
 * hex string would have silently turned every one of them into an opaque block.
 *
 * Each channel token is therefore the single source, and the `--cv-x` beside it is derived from
 * it with `rgb()` so the two cannot drift. The hex in each comment is the same value, written
 * the way a designer reads it.
 */

/* ── Tokens ──────────────────────────────────────────────────────────────────────────── */

:root {
    color-scheme: light;

    /* The mark itself. Fills and gradients only — see the note above about reading these. */
    --cv-ribbon-indigo-rgb: 81 69 245;  /* #5145F5 — the top band */
    --cv-ribbon-indigo: rgb(var(--cv-ribbon-indigo-rgb));
    --cv-ribbon-cyan-rgb: 6 236 254;  /* #06ECFE — the middle band */
    --cv-ribbon-cyan: rgb(var(--cv-ribbon-cyan-rgb));
    --cv-ribbon-violet-rgb: 175 99 251;  /* #AF63FB — the bottom band */
    --cv-ribbon-violet: rgb(var(--cv-ribbon-violet-rgb));

    /* The violet as a badge, which the ribbon colour cannot be.
     *
     * White on #AF63FB is 3.50:1. That is arithmetic, not a rendering accident, and it was
     * shipping at 12px on the billing page's Premium badge in every locale, every theme and
     * every shape — 72 findings, and the only one of the three the first dense-data audit
     * found that has nothing to do with row count. A brand colour is not automatically a
     * surface you can write on; this is the same soft-tint-and-darker-ink pair every other
     * badge in the system already uses, in the violet. */
    --cv-violet-soft-rgb: 245 235 254;  /* #F5EBFE */
    --cv-violet-soft: rgb(var(--cv-violet-soft-rgb));
    --cv-violet-soft-ink-rgb: 106 33 201;  /* #6A21C9 — 6.81:1 on that tint */
    --cv-violet-soft-ink: rgb(var(--cv-violet-soft-ink-rgb));

    /* Primary — actions, links, the brand. 5.58:1 on the page, 5.97:1 on a card, and white
       on it is 5.97:1. It reads both ways round, which is what lets one token be a button
       fill and a link. */
    --cv-primary-rgb: 81 69 245;  /* #5145F5 — the logo indigo */
    --cv-primary: rgb(var(--cv-primary-rgb));
    --cv-primary-hover-rgb: 68 56 219;  /* #4438DB — 7.41:1 white-on-it */
    --cv-primary-hover: rgb(var(--cv-primary-hover-rgb));
    --cv-primary-active-rgb: 63 51 207;  /* #3F33CF — 8.12:1 */
    --cv-primary-active: rgb(var(--cv-primary-active-rgb));
    --cv-primary-soft-rgb: 238 237 254;  /* #EEEDFE — a tinted surface: a selected row, a quiet badge */
    --cv-primary-soft: rgb(var(--cv-primary-soft-rgb));
    --cv-primary-soft-ink-rgb: 63 51 207;  /* #3F33CF — 7.03:1 on that tint */
    --cv-primary-soft-ink: rgb(var(--cv-primary-soft-ink-rgb));
    --cv-primary-ink-rgb: 255 255 255;  /* #FFFFFF — what goes on top of --cv-primary */
    --cv-primary-ink: rgb(var(--cv-primary-ink-rgb));

    /* Accent — the cyan band. A fill, a focus ring, a live indicator. */
    --cv-accent-rgb: 6 236 254;  /* #06ECFE */
    --cv-accent: rgb(var(--cv-accent-rgb));
    /* The hover step. Ninety-three controls in the product had `bg-danger hover:bg-danger` or
       `text-muted hover:text-muted` -- a hover state identical to the state it hovers from, so
       the control looked live and answered nothing. They were not written that way: the colour
       sweeps mapped `bg-red-600` and `hover:bg-red-700` onto the one token that replaced both,
       because the design system had a hover step for the primary and for LINE and for nothing
       else. It has one for each fill now.

       Light darkens by 15%, dark lightens by 8% -- the same direction and roughly the same
       distance the primary already moves, so a hover feels like the same gesture wherever it
       happens. */
    --cv-accent-hover-rgb: 5 201 216;  /* #05C9D8 */
    --cv-accent-hover: rgb(var(--cv-accent-hover-rgb));
    --cv-accent-ink-rgb: 6 42 46;  /* #062A2E — 10.5:1 on the accent fill */
    --cv-accent-ink: rgb(var(--cv-accent-ink-rgb));
    --cv-accent-text-rgb: 6 115 127;  /* #06737F — the readable cyan: 5.22:1 on the page */
    --cv-accent-text: rgb(var(--cv-accent-text-rgb));
    --cv-accent-soft-rgb: 227 251 254;  /* #E3FBFE */
    --cv-accent-soft: rgb(var(--cv-accent-soft-rgb));

    /* Secondary — the violet band. AI surfaces, generated content, half of a gradient. */
    --cv-secondary-rgb: 175 99 251;  /* #AF63FB */
    --cv-secondary: rgb(var(--cv-secondary-rgb));
    --cv-secondary-hover-rgb: 149 84 213;  /* #9554D5 */
    --cv-secondary-hover: rgb(var(--cv-secondary-hover-rgb));
    --cv-secondary-ink-rgb: 30 11 46;  /* #1E0B2E — 5.25:1 on the violet fill */
    --cv-secondary-ink: rgb(var(--cv-secondary-ink-rgb));
    --cv-secondary-text-rgb: 134 57 214;  /* #8639D6 — the readable violet: 5.50:1 on the page */
    --cv-secondary-text: rgb(var(--cv-secondary-text-rgb));
    --cv-secondary-soft-rgb: 246 237 254;  /* #F6EDFE */
    --cv-secondary-soft: rgb(var(--cv-secondary-soft-rgb));

    /* Surfaces. */
    --cv-bg-rgb: 247 247 251;  /* #F7F7FB */
    --cv-bg: rgb(var(--cv-bg-rgb));
    --cv-bg-subtle-rgb: 239 239 246;  /* #EFEFF6 */
    --cv-bg-subtle: rgb(var(--cv-bg-subtle-rgb));
    --cv-card-rgb: 255 255 255;  /* #FFFFFF */
    --cv-card: rgb(var(--cv-card-rgb));
    --cv-card-raised-rgb: 255 255 255;  /* #FFFFFF */
    --cv-card-raised: rgb(var(--cv-card-raised-rgb));
    --cv-sidebar-rgb: 20 20 42;  /* #14142A */
    --cv-sidebar: rgb(var(--cv-sidebar-rgb));
    --cv-sidebar-ink-rgb: 242 242 247;  /* #F2F2F7 — 16.18:1 on the sidebar */
    --cv-sidebar-ink: rgb(var(--cv-sidebar-ink-rgb));
    --cv-sidebar-muted-rgb: 165 165 196;  /* #A5A5C4 — 7.55:1 on the sidebar */
    --cv-sidebar-muted: rgb(var(--cv-sidebar-muted-rgb));
    /* The sidebar is dark in BOTH themes, so anything set on it needs a colour chosen against
       the rail rather than against the page. The light theme's readable cyan is 3.24:1 here. */
    --cv-sidebar-accent-rgb: 95 228 242;  /* #5FE4F2 — 11.92:1 on the sidebar */
    --cv-sidebar-accent: rgb(var(--cv-sidebar-accent-rgb));

    /* Scrims and the frosted bars. Their alpha is part of the value rather than something a
       utility applies, so these are stated whole instead of derived from a channel token. */
    --cv-overlay: rgb(11 11 20 / 0.55);
    --cv-glass: rgb(255 255 255 / 0.78);

    /* A mid indigo for gradients and decoration. Deliberately NOT a text colour: 3.12:1 on the
       page, which is a UI-component ratio and not a reading one. */
    --cv-primary-tint-rgb: 139 130 255;  /* #8B82FF */
    --cv-primary-tint: rgb(var(--cv-primary-tint-rgb));

    /* Text. */
    --cv-heading-rgb: 21 21 31;  /* #15151F — 16.96:1 */
    --cv-heading: rgb(var(--cv-heading-rgb));
    --cv-text-rgb: 36 36 47;  /* #24242F — 14.36:1 */
    --cv-text: rgb(var(--cv-text-rgb));
    --cv-muted-rgb: 91 91 110;  /* #5B5B6E — 6.21:1 — a real secondary, not a grey that fails AA */
    --cv-muted: rgb(var(--cv-muted-rgb));
    --cv-on-fill-rgb: 255 255 255;  /* #FFFFFF */
    --cv-on-fill: rgb(var(--cv-on-fill-rgb));

    /* Lines. A decorative rule and a control boundary are not the same requirement:
       the second has to clear 3:1, so it gets its own token rather than being hoped for. */
    --cv-border-rgb: 228 228 238;  /* #E4E4EE */
    --cv-border: rgb(var(--cv-border-rgb));
    --cv-border-strong-rgb: 133 133 153;  /* #858599 — 3.38:1 on the page — inputs, checkboxes, switch tracks */
    --cv-border-strong: rgb(var(--cv-border-strong-rgb));

    /* Status. Each fill carries its own readable ink rather than assuming white works. */
    --cv-success-rgb: 15 123 79;  /* #0F7B4F — 5.29:1 on a card */
    --cv-success: rgb(var(--cv-success-rgb));
    --cv-success-hover-rgb: 13 105 67;  /* #0D6943 */
    --cv-success-hover: rgb(var(--cv-success-hover-rgb));
    --cv-success-soft-rgb: 228 246 236;  /* #E4F6EC */
    --cv-success-soft: rgb(var(--cv-success-soft-rgb));
    --cv-warning-rgb: 154 91 0;  /* #9A5B00 — 5.43:1 on a card */
    --cv-warning: rgb(var(--cv-warning-rgb));
    --cv-warning-hover-rgb: 131 77 0;  /* #834D00 */
    --cv-warning-hover: rgb(var(--cv-warning-hover-rgb));
    --cv-warning-soft-rgb: 252 240 220;  /* #FCF0DC */
    --cv-warning-soft: rgb(var(--cv-warning-soft-rgb));
    --cv-danger-rgb: 194 24 60;  /* #C2183C — 6.02:1 on a card, and white on it is 6.02:1 */
    --cv-danger: rgb(var(--cv-danger-rgb));
    --cv-danger-hover-rgb: 165 20 51;  /* #A51433 */
    --cv-danger-hover: rgb(var(--cv-danger-hover-rgb));

    /* LINE, whose green is fixed because it is somebody else's brand.
       Every one of these buttons was written `bg-[#06C755] text-heading`, and `text-heading`
       switches — so in the dark theme they rendered near-white on green at 2.02:1. The ink
       below is fixed and measures 8.1:1 on the fill in both themes. The *text* colour is a
       separate token, because the green that reads on a dark page is 2.6:1 on a light one. */
    --cv-linebrand-rgb: 6 199 85;           /* #06C755 — LINE's current brand green */
    --cv-linebrand-hover-rgb: 5 168 71;     /* #05A847 */
    --cv-linebrand-ink-rgb: 4 26 11;        /* #041A0B — 8.07:1 on the fill, fixed in both themes */
    --cv-linebrand-text-rgb: 20 116 56;     /* #147438 — 5.11:1 on the page.
                                              #157F3A cleared 5.09 on a white *card*
                                              and 4.44 on the page behind it, which is
                                              where the contact page puts it. */
    --cv-linebrand: rgb(var(--cv-linebrand-rgb));
    /* Used by `.cv-btn--line:hover` and never defined until 2026-09-25, so hovering -- or
       tapping, since iOS keeps :hover after a tap -- dropped the button to no fill at all. */
    --cv-linebrand-hover: rgb(var(--cv-linebrand-hover-rgb));
    /* Facebook's blue, fixed for the same reason as LINE's green. Used as the mark on the
       sign-in button, never as a surface under a label: white on it is 4.23:1. */
    --cv-facebook-rgb: 24 119 242;          /* #1877F2 */
    --cv-facebook: rgb(var(--cv-facebook-rgb));
    --cv-linebrand-ink: rgb(var(--cv-linebrand-ink-rgb));
    /* Lazada, on the same reasoning as LINE above and Shopee's orange in `app.css`: a third
       party's colour is not ours to route through our palette, so it is fixed in both themes
       -- but it is *named* here rather than typed as `bg-[#0F1689]` into a page, which is
       where it was. A literal in a .razor is a literal whoever reads it next cannot place. */
    --cv-lazada-rgb: 15 22 137;         /* #0F1689 -- Lazada's navy */
    --cv-lazada: rgb(var(--cv-lazada-rgb));
    --cv-lazada-accent-rgb: 248 86 6;   /* #F85606 -- Lazada's orange, 4.6:1 on the navy */
    --cv-lazada-accent: rgb(var(--cv-lazada-accent-rgb));
    --cv-danger-soft-rgb: 253 232 236;  /* #FDE8EC */
    --cv-danger-soft: rgb(var(--cv-danger-soft-rgb));
    --cv-info-rgb: 6 115 127;  /* #06737F — 5.22:1 on a card */
    --cv-info: rgb(var(--cv-info-rgb));
    --cv-info-hover-rgb: 5 98 108;  /* #05626C */
    --cv-info-hover: rgb(var(--cv-info-hover-rgb));
    --cv-info-soft-rgb: 227 251 254;  /* #E3FBFE */
    --cv-info-soft: rgb(var(--cv-info-soft-rgb));

    /* What goes on top of each status fill. White on all four in the light theme, measured
       5.29, 5.43, 6.02 and 5.58 — chosen rather than assumed, because the same assumption on
       Tailwind's yellow-600 gave 2.94. */
    --cv-success-ink-rgb: 255 255 255;  /* #FFFFFF — 5.29:1 on the success fill */
    --cv-success-ink: rgb(var(--cv-success-ink-rgb));
    --cv-warning-ink-rgb: 255 255 255;  /* #FFFFFF — 5.43:1 */
    --cv-warning-ink: rgb(var(--cv-warning-ink-rgb));
    --cv-danger-ink-rgb: 255 255 255;  /* #FFFFFF — 6.02:1 */
    --cv-danger-ink: rgb(var(--cv-danger-ink-rgb));
    --cv-info-ink-rgb: 255 255 255;  /* #FFFFFF — 5.58:1 */
    --cv-info-ink: rgb(var(--cv-info-ink-rgb));

    /* The legacy pairs. Every existing page is written as `bg-card-light dark:bg-card-dark`,
       and these keep about a thousand such class names working unchanged. They are fixed
       values by design — a `-light` token that changed in dark mode would be a trap. New
       work uses the theme-aware tokens above and needs no `dark:` variant at all. */
    --cv-background-light-rgb: 247 247 251;  /* #F7F7FB */
    --cv-background-light: rgb(var(--cv-background-light-rgb));
    --cv-background-dark-rgb: 11 11 20;  /* #0B0B14 */
    --cv-background-dark: rgb(var(--cv-background-dark-rgb));
    /* Theme-aware, unlike the pairs around it, and for a specific reason: `background-alt`
       is used fourteen times across the marketing site and has a `dark:` partner in none of
       them. The frozen `-light`/`-dark` tokens below exist so that existing
       `bg-card-light dark:bg-card-dark` markup keeps working — that arrangement needs both
       halves. A token used on its own has to switch by itself or it is a permanently light
       surface, which is what it was: most of the marketing site's remaining contrast findings
       were theme-aware text sitting on this. */
    --cv-background-alt-rgb: var(--cv-bg-subtle-rgb);
    --cv-background-alt: rgb(var(--cv-background-alt-rgb));
    --cv-card-light-rgb: 255 255 255;  /* #FFFFFF */
    --cv-card-light: rgb(var(--cv-card-light-rgb));
    --cv-card-dark-rgb: 21 21 31;  /* #15151F */
    --cv-card-dark: rgb(var(--cv-card-dark-rgb));
    --cv-surface-light-rgb: 255 255 255;  /* #FFFFFF */
    --cv-surface-light: rgb(var(--cv-surface-light-rgb));
    --cv-surface-dark-rgb: 29 29 42;  /* #1D1D2A */
    --cv-surface-dark: rgb(var(--cv-surface-dark-rgb));
    --cv-text-light-rgb: 36 36 47;  /* #24242F */
    --cv-text-light: rgb(var(--cv-text-light-rgb));
    --cv-text-dark-rgb: 230 230 239;  /* #E6E6EF */
    --cv-text-dark: rgb(var(--cv-text-dark-rgb));
    --cv-text-muted-light-rgb: 91 91 110;  /* #5B5B6E */
    --cv-text-muted-light: rgb(var(--cv-text-muted-light-rgb));
    --cv-text-muted-dark-rgb: 165 165 184;  /* #A5A5B8 */
    --cv-text-muted-dark: rgb(var(--cv-text-muted-dark-rgb));
    --cv-border-light-rgb: 228 228 238;  /* #E4E4EE */
    --cv-border-light: rgb(var(--cv-border-light-rgb));
    --cv-border-dark-rgb: 42 42 58;  /* #2A2A3A */
    --cv-border-dark: rgb(var(--cv-border-dark-rgb));
    --cv-sidebar-light-rgb: 20 20 42;  /* #14142A */
    --cv-sidebar-light: rgb(var(--cv-sidebar-light-rgb));
    --cv-sidebar-dark-rgb: 11 11 20;  /* #0B0B14 */
    --cv-sidebar-dark: rgb(var(--cv-sidebar-dark-rgb));

    /* The mark's three colours, kept as three colours rather than blended into one.
       `--cv-brand-bar` has hard stops, so it paints three solid segments and not a fade: a
       flag. It is the one place the full palette appears at once, and it appears as a 3px
       rule on a card or a hero, never as a field behind text.

       The slant survives because the mark is slanted; it is used for geometry now (the
       offset panels in `.cv-stack`), not for the direction of a blend. */
    --cv-slant: 114deg;
    /* Fixed in both themes. `--cv-primary` lightens in the dark theme so it can be read as
       text on a dark card, which makes it the wrong thing to fill a band with: white on the
       dark-theme primary measures 3.12:1. The logo indigo is one colour everywhere, and white
       on it is 5.96:1 everywhere. */
    --cv-brand-fill-rgb: 81 69 245;   /* #5145F5 — the logo indigo, theme-independent */
    --cv-brand-fill: rgb(var(--cv-brand-fill-rgb));
    --cv-brand-ink-rgb: 255 255 255;  /* #FFFFFF — 5.96:1 on the fill */
    --cv-brand-ink: rgb(var(--cv-brand-ink-rgb));
    --cv-brand-tint: var(--cv-primary-soft);

    /* The band's own colours, as tokens.
     *
     * `.cv-band` is dark in *both* themes -- that is the whole point of it -- so its ink is
     * fixed and these are declared once here and never overridden in `.dark`.
     *
     * They are tokens rather than literals inside the component because the pages need them:
     * a band is a section, and the headings, hairlines and buttons inside it are written in
     * the markup. Eleven inline `#F2F2F7`/`#FFFFFF` declarations and eight
     * `rgb(255 255 255 / .xx)` ones were spread across five .razor files, doing by hand what
     * the band already knew. `brandcheck.py` would not have caught any of them -- its list is
     * the mark's own three colours and the retired palettes, and near-white is neither -- so
     * they were a drift the check could not see, which is the kind worth naming here. */
    --cv-band-bg-rgb: 20 20 42;              /* #14142A */
    --cv-band-bg: rgb(var(--cv-band-bg-rgb));
    --cv-band-ink-rgb: 242 242 247;          /* #F2F2F7 -- 16.18:1 on the band */
    --cv-band-ink: rgb(var(--cv-band-ink-rgb));
    --cv-band-muted-rgb: 165 165 184;        /* #A5A5B8 -- 7:1 on the band */
    --cv-band-muted: rgb(var(--cv-band-muted-rgb));
    --cv-band-brand-muted-rgb: 228 225 255;  /* #E4E1FF -- 4.68:1 on the brand fill */
    --cv-band-brand-muted: rgb(var(--cv-band-brand-muted-rgb));
    /* The hairline a band rules itself with, and the wash its own controls sit on. Fixed
       white at low alpha, because the surface underneath them is fixed too. */
    --cv-band-line: rgb(var(--cv-brand-ink-rgb) / 0.14);
    --cv-band-wash: rgb(var(--cv-brand-ink-rgb) / 0.08);

    --cv-brand-bar: linear-gradient(90deg,
            var(--cv-ribbon-indigo) 0 33.33%,
            var(--cv-ribbon-cyan) 33.33% 66.66%,
            var(--cv-ribbon-violet) 66.66% 100%);

    /* Shape. One radius scale, so a card and the button inside it are visibly related. */
    --cv-r-xs: 0.375rem;
    --cv-r-sm: 0.5rem;
    --cv-r-md: 0.75rem;
    --cv-r-lg: 1rem;
    --cv-r-xl: 1.25rem;
    --cv-r-2xl: 1.5rem;
    --cv-r-full: 9999px;

    /* Depth. Tinted with the brand indigo rather than neutral black — a pure-black shadow on a
       cool page reads as dirt. */
    --cv-shadow-xs: 0 1px 2px rgba(21, 21, 42, 0.06);
    --cv-shadow-sm: 0 2px 8px rgba(21, 21, 42, 0.07);
    --cv-shadow-md: 0 8px 24px rgba(21, 21, 42, 0.09);
    --cv-shadow-lg: 0 20px 48px rgba(21, 21, 42, 0.13);
    --cv-shadow-primary: 0 10px 30px rgba(81, 69, 245, 0.28);

    /* Motion. Named by intent, so a page does not invent its own timing. */
    --cv-t-fast: 120ms;
    --cv-t: 200ms;
    --cv-t-slow: 360ms;
    --cv-ease: cubic-bezier(0.22, 1, 0.36, 1);

    /* Touch. 44px is Apple's floor and the number the last audit spent 570 findings on. */
    --cv-tap: 44px;

    /* The z ladder, declared once. Every previous overlap bug in this product came from two
       elements each picking their own number in isolation. */
    --cv-z-base: 0;
    --cv-z-sticky: 20;
    --cv-z-header: 30;
    --cv-z-drawer: 40;
    --cv-z-bottom-bar: 50;
    --cv-z-sheet: 60;
    --cv-z-modal: 70;
    --cv-z-toast: 80;
    --cv-z-tooltip: 90;

    /* Fixed furniture, as measurements other rules can reserve space against instead of each
       guessing. B1 of the last audit was sixteen pages each guessing. */
    /* The bar's own row, and how far the raised Create button overhangs it. Both measured
       off the built layout rather than chosen: 56px and 28px. Everything that has to clear
       the bar adds these two plus the home-indicator inset, so there is one arithmetic and
       not five. */
    --cv-bottom-bar-h: 3.5rem;
    --cv-bottom-bar-overhang: 1.75rem;
    --cv-header-h: 3.5rem;
    --cv-safe-t: env(safe-area-inset-top, 0px);
    --cv-safe-b: env(safe-area-inset-bottom, 0px);
    --cv-safe-l: env(safe-area-inset-left, 0px);
    --cv-safe-r: env(safe-area-inset-right, 0px);
}

.dark {
    color-scheme: dark;

    /* On a dark surface the logo indigo is 3.04:1 — fine for a fill, not for text. The dark
       theme therefore lightens the readable tokens and leaves the fills where they are.

       The primary is #8B82FF, and the step below it is the instructive part. #7C72FF was
       chosen first because it is 4.93:1 on `--cv-card`, and that looked like enough. The
       measurement found it sitting on two other surfaces as well — `bg-gray-800` at 3.99:1 and
       its own `--cv-primary-soft` tint at 4.40:1 — a hundred and sixty findings, every one of
       them just under the line. A colour is not AA on its own; it is AA against a named
       surface, and a token used on three has to clear the worst of them. #8B82FF does:
       5.81, 4.71, 5.19, with the near-black ink on it at 6.28. */
    --cv-primary-rgb: 139 130 255;  /* #8B82FF — 5.81 on a card, 4.71 on gray-800, 5.19 on the tint */
    --cv-primary: rgb(var(--cv-primary-rgb));
    --cv-primary-hover-rgb: 148 140 255;  /* #948CFF — 6.43:1 */
    --cv-primary-hover: rgb(var(--cv-primary-hover-rgb));
    --cv-primary-active-rgb: 167 159 255;  /* #A79FFF — 7.81:1 */
    --cv-primary-active: rgb(var(--cv-primary-active-rgb));
    --cv-primary-soft-rgb: 30 27 69;  /* #1E1B45 */
    --cv-primary-soft: rgb(var(--cv-primary-soft-rgb));
    --cv-primary-soft-ink-rgb: 167 159 255;  /* #A79FFF — 6.97:1 on that tint */
    --cv-primary-soft-ink: rgb(var(--cv-primary-soft-ink-rgb));
    --cv-primary-ink-rgb: 11 11 20;  /* #0B0B14 — 6.28:1 on the primary fill */
    --cv-primary-ink: rgb(var(--cv-primary-ink-rgb));

    --cv-violet-soft-rgb: 42 22 64;  /* #2A1640 */
    --cv-violet-soft: rgb(var(--cv-violet-soft-rgb));
    --cv-violet-soft-ink-rgb: 210 174 255;  /* #D2AEFF — 8.73:1 on that tint */
    --cv-violet-soft-ink: rgb(var(--cv-violet-soft-ink-rgb));

    --cv-accent-rgb: 6 236 254;  /* #06ECFE */
    --cv-accent: rgb(var(--cv-accent-rgb));
    --cv-accent-hover-rgb: 26 238 254;  /* #1AEEFE */
    --cv-accent-hover: rgb(var(--cv-accent-hover-rgb));
    --cv-accent-ink-rgb: 6 42 46;  /* #062A2E */
    --cv-accent-ink: rgb(var(--cv-accent-ink-rgb));
    --cv-accent-text-rgb: 95 228 242;  /* #5FE4F2 — 11.96:1 on a card */
    --cv-accent-text: rgb(var(--cv-accent-text-rgb));
    --cv-accent-soft-rgb: 8 50 58;  /* #08323A */
    --cv-accent-soft: rgb(var(--cv-accent-soft-rgb));

    --cv-secondary-rgb: 175 99 251;  /* #AF63FB */
    --cv-secondary: rgb(var(--cv-secondary-rgb));
    --cv-secondary-hover-rgb: 181 111 251;  /* #B56FFB */
    --cv-secondary-hover: rgb(var(--cv-secondary-hover-rgb));
    --cv-secondary-ink-rgb: 30 11 46;  /* #1E0B2E */
    --cv-secondary-ink: rgb(var(--cv-secondary-ink-rgb));
    --cv-secondary-text-rgb: 197 140 255;  /* #C58CFF — 7.39:1 on a card */
    --cv-secondary-text: rgb(var(--cv-secondary-text-rgb));
    --cv-secondary-soft-rgb: 42 22 64;  /* #2A1640 */
    --cv-secondary-soft: rgb(var(--cv-secondary-soft-rgb));

    --cv-bg-rgb: 11 11 20;  /* #0B0B14 */
    --cv-bg: rgb(var(--cv-bg-rgb));
    --cv-bg-subtle-rgb: 16 16 25;  /* #101019 */
    --cv-bg-subtle: rgb(var(--cv-bg-subtle-rgb));
    --cv-card-rgb: 21 21 31;  /* #15151F */
    --cv-card: rgb(var(--cv-card-rgb));
    --cv-card-raised-rgb: 29 29 42;  /* #1D1D2A */
    --cv-card-raised: rgb(var(--cv-card-raised-rgb));
    --cv-sidebar-rgb: 11 11 20;  /* #0B0B14 */
    --cv-sidebar: rgb(var(--cv-sidebar-rgb));
    --cv-sidebar-ink-rgb: 242 242 247;  /* #F2F2F7 */
    --cv-sidebar-ink: rgb(var(--cv-sidebar-ink-rgb));
    --cv-sidebar-muted-rgb: 165 165 184;  /* #A5A5B8 */
    --cv-sidebar-muted: rgb(var(--cv-sidebar-muted-rgb));
    --cv-sidebar-accent-rgb: 95 228 242;  /* #5FE4F2 — 12.93:1 on the dark rail */
    --cv-sidebar-accent: rgb(var(--cv-sidebar-accent-rgb));

    --cv-overlay: rgb(0 0 0 / 0.66);
    --cv-glass: rgb(21 21 31 / 0.78);

    --cv-primary-tint-rgb: 167 159 255;  /* #A79FFF — 7.81:1 on a card */
    --cv-primary-tint: rgb(var(--cv-primary-tint-rgb));

    --cv-heading-rgb: 242 242 247;  /* #F2F2F7 — 16.24:1 on a card */
    --cv-heading: rgb(var(--cv-heading-rgb));
    --cv-text-rgb: 230 230 239;  /* #E6E6EF — 14.61:1 */
    --cv-text: rgb(var(--cv-text-rgb));
    --cv-muted-rgb: 165 165 184;  /* #A5A5B8 — 7.48:1 */
    --cv-muted: rgb(var(--cv-muted-rgb));
    --cv-on-fill-rgb: 255 255 255;  /* #FFFFFF */
    --cv-on-fill: rgb(var(--cv-on-fill-rgb));

    --cv-border-rgb: 42 42 58;  /* #2A2A3A */
    --cv-border: rgb(var(--cv-border-rgb));
    --cv-border-strong-rgb: 107 107 124;  /* #6B6B7C — 3.47:1 on a card */
    --cv-border-strong: rgb(var(--cv-border-strong-rgb));

    --cv-success-rgb: 74 222 155;  /* #4ADE9B — 10.54:1 on a card */
    --cv-success: rgb(var(--cv-success-rgb));
    --cv-success-hover-rgb: 91 225 166;  /* #5BE1A6 */
    --cv-success-hover: rgb(var(--cv-success-hover-rgb));
    --cv-success-soft-rgb: 12 43 30;  /* #0C2B1E */
    --cv-success-soft: rgb(var(--cv-success-soft-rgb));
    --cv-warning-rgb: 242 177 85;  /* #F2B155 — 9.66:1 on a card */
    --cv-warning: rgb(var(--cv-warning-rgb));
    --cv-warning-hover-rgb: 243 184 101;  /* #F3B865 */
    --cv-warning-hover: rgb(var(--cv-warning-hover-rgb));
    --cv-warning-soft-rgb: 51 35 10;  /* #33230A */
    --cv-warning-soft: rgb(var(--cv-warning-soft-rgb));
    --cv-danger-rgb: 255 128 152;  /* #FF8098 — 7.59:1 on a card */
    --cv-danger: rgb(var(--cv-danger-rgb));
    --cv-danger-hover-rgb: 255 140 164;  /* #FF8CA4 */
    --cv-danger-hover: rgb(var(--cv-danger-hover-rgb));
    --cv-danger-soft-rgb: 58 16 25;  /* #3A1019 */
    --cv-danger-soft: rgb(var(--cv-danger-soft-rgb));

    /* The fill and its ink do not move — only the green used as text does, because on the
       dark page the brand green itself clears 7.9:1 and the darkened one would not. */
    --cv-linebrand-text-rgb: 6 199 85;      /* #06C755 — 7.94:1 on the dark page */
    --cv-info-rgb: 95 228 242;  /* #5FE4F2 — 11.96:1 on a card */
    --cv-info: rgb(var(--cv-info-rgb));
    --cv-info-hover-rgb: 111 230 243;  /* #6FE6F3 */
    --cv-info-hover: rgb(var(--cv-info-hover-rgb));
    --cv-info-soft-rgb: 8 50 58;  /* #08323A */
    --cv-info-soft: rgb(var(--cv-info-soft-rgb));

    /* The dark theme's status fills are bright, so the ink flips to the near-black: 11.39,
       10.44, 8.2 and 12.93 respectively. White on any of them would be under 2:1. */
    --cv-success-ink-rgb: 11 11 20;  /* #0B0B14 — 11.39:1 on the success fill */
    --cv-success-ink: rgb(var(--cv-success-ink-rgb));
    --cv-warning-ink-rgb: 11 11 20;  /* #0B0B14 — 10.44:1 */
    --cv-warning-ink: rgb(var(--cv-warning-ink-rgb));
    --cv-danger-ink-rgb: 11 11 20;  /* #0B0B14 — 8.2:1 */
    --cv-danger-ink: rgb(var(--cv-danger-ink-rgb));
    --cv-info-ink-rgb: 11 11 20;  /* #0B0B14 — 12.93:1 */
    --cv-info-ink: rgb(var(--cv-info-ink-rgb));

    /* The bar is the same three colours in both themes — it is the trademark. Only the
       flat tint moves, because it is a surface. */
    --cv-brand-tint: var(--cv-primary-soft);

    --cv-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
    --cv-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.45);
    --cv-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5);
    --cv-shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.6);
    --cv-shadow-primary: 0 10px 30px rgba(110, 99, 255, 0.35);
}

/* ── Foundation ──────────────────────────────────────────────────────────────────────── */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    /* Stop iOS enlarging text when the phone is turned, which silently breaks a measured
       layout in landscape — the one orientation the audit checks at 844x390. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body {
    background-color: var(--cv-bg);
    color: var(--cv-text);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* A standalone PWA has no browser chrome to rubber-band against, so an overscroll at the
       top of a list drags the whole app down and looks broken. */
    overscroll-behavior-y: none;
}

/* One focus ring for the whole product, on the cyan band, visible on both themes and on top of
   a primary fill. `:focus-visible` rather than `:focus` so a mouse click does not ring.

   `[tabindex="-1"]` is excluded, and that exclusion is the whole point of writing the selector
   out. An element with tabindex -1 is not in the tab order: it is there to be focused *by
   script*, so that a screen reader announces the new page after an enhanced navigation. The
   marketing site's `<h1>` is exactly that. With `[tabindex]` matching it, every page on that
   site loaded with a two-pixel cyan rectangle drawn around its headline, which reads as an
   error state rather than as focus. */
:where(a, button, input, select, textarea, summary, [tabindex]):not([tabindex="-1"]):focus-visible {
    outline: 2px solid var(--cv-accent);
    outline-offset: 2px;
    border-radius: var(--cv-r-xs);
}

::selection {
    background: var(--cv-primary);
    color: var(--cv-on-fill);
}

/* An element with `tabindex="-1"` is not in the tab order. It is there so that script can move
   focus to it — the marketing site focuses its `<h1>` after an enhanced navigation, so a screen
   reader announces the new page. Nobody pressed anything, so nothing should be drawn.

   Excluding these from the rule above was half the fix: the browser's own `outline: auto` then
   took over and the headline swapped a cyan rectangle for a black one. Both have to go. */
[tabindex="-1"]:focus,
[tabindex="-1"]:focus-visible {
    outline: none;
}


/* Every animation in the product goes through these, so honouring the setting is one rule
   rather than a promise each page has to keep. */
@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;
        /* The delays are the half that was missing, and the marketing site is where it
           showed. Its entrance animations are staggered by class -- `.delay-100` through
           `.delay-800` set `animation-delay`, and `.animate-on-load` starts the element at
           `opacity: 0`. Collapsing the *duration* alone left those delays intact, so somebody
           who had asked their system for less motion still watched a hero assemble itself
           over 800ms: badge, then headline, then sentence, then buttons, each snapping in.
           That is the effect the setting exists to switch off, arriving in one frame instead
           of over one.

           `-1ms` rather than `0s`: a negative delay starts the animation already finished, so
           the element is in its end state on the first frame rather than one frame after it. */
        animation-delay: -1ms !important;
        transition-delay: 0s !important;
    }
}

/* ── The mark ────────────────────────────────────────────────────────────────────────── */

.cv-logo {
    display: inline-flex;
    align-items: center;
    gap: 0.625rem;
    text-decoration: none;
    color: inherit;
    /* It is a link to the home page sitting in a header, which makes it a target — and at
       28px tall it was 102 of the marketing site's remaining tap-target findings. The shared
       tap-target block could not see it for the same reason it could not see `.nav-link`: it
       matches Tailwind class names, and this element carries its own. */
    min-height: var(--cv-tap);
}

/* Below 360px the header cannot hold the wordmark *and* the controls beside it: measured 155px
   of "ContentVelo" in the 135px left to it, on all seventeen marketing pages. The mark alone is
   the logo too — it is the part that is the trademark — so the word steps aside and the link
   keeps its accessible name from the `aria-label` the header already sets. */
@media (max-width: 359px) {
    .cv-logo__word {
        display: none;
    }
}

.cv-logo__mark {
    display: block;
    width: 2rem;
    height: 2rem;
    flex: none;
    border-radius: var(--cv-r-sm);
}

.cv-logo__word {
    font-weight: 700;
    letter-spacing: -0.02em;
    font-size: 1.125rem;
    line-height: 1.3;
    /* It may wrap between words and never inside one.
     *
     * `nowrap` made the wordmark's min-content width the whole phrase, and once grid and flex
     * children were allowed to shrink it clipped instead: the back office's rail is a 256px
     * drawer whose header also carries a close button, leaving 172px for a logo that wanted
     * 192, on all sixteen pages. "ContentVelo Admin" now takes two lines there and fits.
     *
     * `overflow-wrap: normal` is the load-bearing half. Prose is set to `anywhere` a few rules
     * down, and a `<span>` is prose — without this the wordmark would happily break after
     * "ContentVel", which is worse than anything it fixes. */
    white-space: normal;
    overflow-wrap: normal;
    word-break: keep-all;
}

/* The wordmark is one solid colour. Clipping a three-stop blend into letterforms was the
   single most recognisable piece of the look being removed, and it cost contrast too: the
   middle of the word sat on the cyan stop, which is the weakest of the three against a light
   page. Solid indigo measures 8.6:1 and reads at every size.

   Both class names are kept because the markup spells them; they no longer describe a
   gradient, and neither is one. */
.cv-logo__word--gradient,
.cv-gradient-text,
.cv-brand-text {
    color: var(--cv-primary);
    background: none;
    -webkit-background-clip: border-box;
    background-clip: border-box;
}

/* ── Buttons ─────────────────────────────────────────────────────────────────────────── */

.cv-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-height: var(--cv-tap);
    min-width: var(--cv-tap);
    padding: 0 1.125rem;
    border: 1px solid transparent;
    border-radius: var(--cv-r-md);
    font: inherit;
    font-weight: 600;
    line-height: 1.3;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color var(--cv-t) var(--cv-ease),
        border-color var(--cv-t) var(--cv-ease),
        color var(--cv-t) var(--cv-ease),
        box-shadow var(--cv-t) var(--cv-ease),
        transform var(--cv-t-fast) var(--cv-ease);
    /* A tap should not select the label or flash a grey box on Android. */
    -webkit-tap-highlight-color: transparent;
    user-select: none;
}

.cv-btn:active {
    transform: translateY(1px);
}

.cv-btn:disabled,
.cv-btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
    transform: none;
}

.cv-btn--primary {
    background: var(--cv-primary);
    color: var(--cv-primary-ink);
    box-shadow: var(--cv-shadow-primary);
}

.cv-btn--primary:hover:not(:disabled) {
    background: var(--cv-primary-hover);
}

.cv-btn--primary:active:not(:disabled) {
    background: var(--cv-primary-active);
}

/* A LINE action, on LINE's green. The logo is white, as LINE publishes it on its own green; the
   label stays in the dark ink, because white text on #06C755 is 2.26:1 and a label has to be
   read — a logo is exempt from that, a sentence is not. */
.cv-btn--line {
    background: var(--cv-linebrand);
    color: var(--cv-linebrand-ink);
}

.cv-btn--line:hover:not(:disabled) {
    background: var(--cv-linebrand-hover);
}

.cv-btn--line iconify-icon {
    color: #fff;
}

/* "Connect with Facebook", in Facebook's own button: its blue, its f in white. Our indigo with
   Facebook's f on it reads as our button borrowing their logo, which is what Meta's brand
   guidelines ask a partner not to do. #1877F2 is Facebook's, so it does not follow our theme. */
.cv-btn--facebook {
    background: var(--cv-facebook);
    color: #fff;
}

.cv-btn--facebook:hover:not(:disabled) {
    background: #166FE5;
}

/* The brand button. Reserved for the one action a screen exists for — if two of these are on a
   page, neither is the primary action. */
.cv-btn--brand {
    background: var(--cv-brand-fill);
    color: var(--cv-brand-ink);
    box-shadow: var(--cv-shadow-primary);
    position: relative;
    isolation: isolate;
}

.cv-btn--brand::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: rgba(11, 11, 20, 0.16);
    opacity: 0;
    transition: opacity var(--cv-t) var(--cv-ease);
    z-index: -1;
}

.cv-btn--brand:hover:not(:disabled)::after {
    opacity: 1;
}

/* The two buttons that sit *on* a band.
 *
 * A band is dark in both themes, so a button inside one cannot use the page's surface tokens:
 * `--cv-card` is white in the light theme and near-black in the dark one, and only one of
 * those reads on indigo. Five marketing pages had therefore written
 * `style="background: #FFFFFF; color: var(--cv-brand-fill)"` by hand, and two of them a ghost
 * beside it -- the same button seven times, differing in whether it carried a shadow, in
 * whether the ghost's border was .3 or .45, and in whether the label named `--cv-brand-fill`
 * or `--cv-ribbon-indigo`, which are the same colour.
 *
 * `--cv-brand-ink` is that white, declared once and fixed in both themes for the reason in
 * the token note: a colour that identifies the product is not a surface of the product. */
.cv-btn--on-band {
    background: var(--cv-brand-ink);
    color: var(--cv-brand-fill);
}

.cv-btn--on-band:hover:not(:disabled) {
    background: var(--cv-band-brand-muted);
}

.cv-btn--on-band-ghost {
    background: rgb(var(--cv-brand-ink-rgb) / 0.14);
    border-color: rgb(var(--cv-brand-ink-rgb) / 0.45);
    color: var(--cv-brand-ink);
}

.cv-btn--on-band-ghost:hover:not(:disabled) {
    background: rgb(var(--cv-brand-ink-rgb) / 0.24);
}

.cv-btn--secondary {
    background: var(--cv-card);
    color: var(--cv-heading);
    border-color: var(--cv-border-strong);
    box-shadow: var(--cv-shadow-xs);
}

.cv-btn--secondary:hover:not(:disabled) {
    background: var(--cv-bg-subtle);
    border-color: var(--cv-primary);
    color: var(--cv-primary);
}

.cv-btn--ghost {
    background: transparent;
    color: var(--cv-text);
}

.cv-btn--ghost:hover:not(:disabled) {
    background: var(--cv-bg-subtle);
    color: var(--cv-heading);
}

.cv-btn--soft {
    background: var(--cv-primary-soft);
    color: var(--cv-primary-soft-ink);
}

.cv-btn--soft:hover:not(:disabled) {
    background: var(--cv-primary);
    color: var(--cv-primary-ink);
}

.cv-btn--danger {
    background: var(--cv-danger);
    color: var(--cv-on-fill);
}

.dark .cv-btn--danger {
    color: #3A1019;
}

.cv-btn--sm {
    min-height: 2.25rem;
    padding: 0 0.75rem;
    font-size: 0.8125rem;
    border-radius: var(--cv-r-sm);
}

/* A small button is a desktop affordance. A thumb is the same size whatever the button thinks,
   so at a coarse pointer the small variant is not small -- it keeps its type and its padding and
   takes the tap floor back. Found when four new pages used `--sm` for their cross-links and the
   audit reported 115 controls at 36px across nine locales; fixing it here rather than on those
   four pages means the next page to use it is already right. */
@media (pointer: coarse) {
    .cv-btn--sm {
        min-height: var(--cv-tap);
    }
}

/* A small button is still a touch target. Shrinking the box is fine; shrinking what the thumb
   has to hit is not, so the hit area is restored with a pseudo-element that costs no layout. */
@media (pointer: coarse) {
    .cv-btn--sm {
        position: relative;
    }

    .cv-btn--sm::before {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        top: 50%;
        height: var(--cv-tap);
        transform: translateY(-50%);
        min-width: var(--cv-tap);
    }
}

.cv-btn--lg {
    min-height: 3.25rem;
    padding: 0 1.75rem;
    font-size: 1.0625rem;
    border-radius: var(--cv-r-lg);
}

/* A full-width button owns its line, so its label may take two of them. Without this the
   `nowrap` inherited from `.cv-btn` makes the label the button's min-content width, which a
   grid or flex parent then has to honour -- a long localised label (Thai, German) pushed the
   column wider than the page and the label was clipped at the container edge rather than
   wrapping inside its own pill. Wrapping is always better than overflowing. */
.cv-btn--block {
    display: flex;
    width: 100%;
    min-width: 0;
    white-space: normal;
}

.cv-btn--icon {
    padding: 0;
    width: var(--cv-tap);
    border-radius: var(--cv-r-md);
}

/* ── Cards ───────────────────────────────────────────────────────────────────────────── */

.cv-card {
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-xl);
    box-shadow: var(--cv-shadow-sm);
}

.cv-card--pad {
    padding: 1.25rem;
}

@media (min-width: 640px) {
    .cv-card--pad {
        padding: 1.5rem;
    }
}

.cv-card--raised {
    background: var(--cv-card-raised);
    box-shadow: var(--cv-shadow-md);
}

.cv-card--interactive {
    transition: transform var(--cv-t) var(--cv-ease), box-shadow var(--cv-t) var(--cv-ease),
        border-color var(--cv-t) var(--cv-ease);
}

.cv-card--interactive:hover {
    transform: translateY(-2px);
    box-shadow: var(--cv-shadow-md);
    border-color: var(--cv-primary);
}

/* A card whose top edge carries the three bands. The brand, without a logo on every card. */
.cv-card--branded {
    position: relative;
    overflow: hidden;
}

.cv-card--branded::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: var(--cv-brand-bar);
}

.cv-card__title {
    font-weight: 700;
    color: var(--cv-heading);
    letter-spacing: -0.01em;
}

/* ── Picker tokens ───────────────────────────────────────────────────────────────────────
 * cv-inputs.css is shared with ChatMai, so it reads `--cvi-*` rather than `--cv-*`. This is
 * ContentVelo's one mapping of them. Declared on :root, where `.dark` also lives, so each
 * one resolves against whichever theme is active. */
:root {
    --cvi-surface: var(--cv-card);
    --cvi-subtle: var(--cv-bg-subtle);
    --cvi-text: var(--cv-text);
    --cvi-heading: var(--cv-heading);
    --cvi-muted: var(--cv-muted);
    --cvi-border: var(--cv-border);
    --cvi-border-strong: var(--cv-border-strong);
    --cvi-hover: var(--cv-bg-subtle);
    --cvi-track: var(--cv-border);
    --cvi-accent: var(--cv-primary);
    --cvi-accent-hover: var(--cv-primary-hover);
    --cvi-accent-text: var(--cv-primary-soft-ink);
    --cvi-on-accent: var(--cv-primary-ink);
    --cvi-ring: color-mix(in srgb, var(--cv-primary) 28%, transparent);
    --cvi-danger: var(--cv-danger);
    --cvi-overlay: var(--cv-overlay);
    --cvi-shadow: var(--cv-shadow-lg);
    --cvi-radius: var(--cv-r-lg);
    --cvi-radius-sm: var(--cv-r-sm);
    --cvi-z: var(--cv-z-tooltip);
    --cvi-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23858599' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
    --cvi-calendar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23858599' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='4.5' width='13' height='12' rx='2'/%3E%3Cpath d='M3.5 8.5h13M7 3v3M13 3v3'/%3E%3C/svg%3E");
    --cvi-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23858599' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10' cy='10' r='6.5'/%3E%3Cpath d='M10 6.5V10l2.5 1.5'/%3E%3C/svg%3E");
}

.dark {
    --cvi-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23A5A5B8' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
    --cvi-calendar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23A5A5B8' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='4.5' width='13' height='12' rx='2'/%3E%3Cpath d='M3.5 8.5h13M7 3v3M13 3v3'/%3E%3C/svg%3E");
    --cvi-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23A5A5B8' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10' cy='10' r='6.5'/%3E%3Cpath d='M10 6.5V10l2.5 1.5'/%3E%3C/svg%3E");
}

/* ── Social sign-in ──────────────────────────────────────────────────────────────────── */

/* Full-width, outlined, the provider's mark in a fixed slot on the start edge and the label
 * centred on the button rather than on what is left of it — so three labels of different
 * lengths, in nine languages, still line up as one column. Outlined because white on the
 * providers' own fills fails contrast (Facebook 4.23:1, LINE 2.2:1); the mark carries the
 * brand, and the mark is what a reader scans for. */

.cv-social-divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 1rem;
    font-size: 0.8125rem;
    color: var(--cv-muted);
}

.cv-social-divider::before,
.cv-social-divider::after {
    content: "";
    flex: 1 1 0;
    height: 1px;
    background: var(--cv-border);
}

.cv-social-list {
    display: grid;
    gap: 0.625rem;
}

.cv-social {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 3rem;
    padding: 0 3.25rem;
    border: 1px solid var(--cv-border-strong);
    border-radius: var(--cv-r-md);
    background: var(--cv-card);
    color: var(--cv-heading);
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.25;
    text-align: center;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--cv-t) var(--cv-ease),
        border-color var(--cv-t) var(--cv-ease),
        transform var(--cv-t-fast) var(--cv-ease);
}

.cv-social:hover {
    background: var(--cv-bg-subtle);
    border-color: var(--cv-heading);
}

.cv-social:active {
    transform: translateY(1px);
}

.cv-social:focus-visible {
    outline: none;
    border-color: var(--cv-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--cv-primary) 28%, transparent);
}

.cv-social__logo {
    position: absolute;
    inset-inline-start: 1rem;
    top: 50%;
    width: 1.25rem;
    height: 1.25rem;
    transform: translateY(-50%);
    flex: none;
}

.cv-social__logo--facebook {
    color: var(--cv-facebook);
}

/* ── Forms ───────────────────────────────────────────────────────────────────────────── */

.cv-label {
    display: block;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--cv-muted);
    margin-bottom: 0.375rem;
}

.cv-input,
.cv-select,
.cv-textarea {
    display: block;
    width: 100%;
    min-height: var(--cv-tap);
    padding: 0.625rem 0.875rem;
    background: var(--cv-card);
    color: var(--cv-text);
    border: 1px solid var(--cv-border-strong);
    border-radius: var(--cv-r-md);
    /* 16px exactly. Below it, iOS Safari zooms the page on focus and never zooms back, which
       leaves the customer on a viewport wider than the one everything was measured at. */
    font-size: 16px;
    line-height: 1.45;
    transition: border-color var(--cv-t) var(--cv-ease), box-shadow var(--cv-t) var(--cv-ease);
}

.cv-textarea {
    min-height: 7rem;
    resize: vertical;
}

.cv-input::placeholder,
.cv-textarea::placeholder {
    color: var(--cv-muted);
    opacity: 1;
}

.cv-input:focus,
.cv-select:focus,
.cv-textarea:focus {
    outline: none;
    border-color: var(--cv-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--cv-primary) 28%, transparent);
}

.cv-input[aria-invalid="true"],
.cv-select[aria-invalid="true"],
.cv-textarea[aria-invalid="true"],
.cv-input.invalid,
.cv-textarea.invalid {
    border-color: var(--cv-danger);
}

.cv-select {
    /* A select's intrinsic width is its widest option, and a flex item's default
       `min-width: auto` refuses to shrink below it. The drafts filter is populated with
       customers' Facebook page names, so its width is decided by data: measured 167px past the
       edge of a 312px phone, unreachable because the ancestor that clips is <main>. Third time
       `min-width: auto` has done this — see `.cv-hscroll` and `.cv-reveal`. */
    min-width: 0;
    max-width: 100%;
    appearance: none;
    padding-right: 2.5rem;
    /* The chevron is drawn rather than fetched: an inline SVG costs no request and cannot 404,
       and it is `currentColor`, so it follows the theme without a second asset. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23858599' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 1.25rem;
}

.dark .cv-select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23A5A5B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
}

.cv-help {
    font-size: 0.8125rem;
    color: var(--cv-muted);
    margin-top: 0.375rem;
}

.cv-error {
    font-size: 0.8125rem;
    color: var(--cv-danger);
    margin-top: 0.375rem;
    display: flex;
    align-items: flex-start;
    gap: 0.25rem;
}

/* A switch that is a real 44px target and shows its state without relying on colour alone. */
.cv-switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    flex: none;
    width: 3.25rem;
    height: var(--cv-tap);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.cv-switch input {
    position: absolute;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
}

.cv-switch__track {
    width: 3.25rem;
    height: 1.875rem;
    border-radius: var(--cv-r-full);
    background: var(--cv-bg-subtle);
    border: 1px solid var(--cv-border-strong);
    transition: background-color var(--cv-t) var(--cv-ease), border-color var(--cv-t) var(--cv-ease);
    pointer-events: none;
}

.cv-switch__thumb {
    position: absolute;
    left: 0.25rem;
    width: 1.375rem;
    height: 1.375rem;
    border-radius: var(--cv-r-full);
    background: #FFFFFF;
    box-shadow: var(--cv-shadow-sm);
    transition: transform var(--cv-t) var(--cv-ease);
    pointer-events: none;
}

.cv-switch input:checked~.cv-switch__track {
    background: var(--cv-primary);
    border-color: var(--cv-primary);
}

.cv-switch input:checked~.cv-switch__thumb {
    transform: translateX(1.375rem);
}

.cv-switch input:focus-visible~.cv-switch__track {
    outline: 2px solid var(--cv-accent);
    outline-offset: 2px;
}

/* The input has to stay invisible in every state. The checkbox rules further down this file
   (`input[type="checkbox"]`, and `:disabled` at 0-2-1) come later and out-rank `.cv-switch input`,
   so a *disabled* switch had its hidden checkbox drawn back on top of the track at half
   opacity — the stray tick inside the purple pill on AutoPilot's "ask me on LINE" row
   (reported 2026-09-24). Three classes beats both. */
.cv-switch.cv-switch.cv-switch input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    opacity: 0;
    width: 100%;
    height: 100%;
    border: 0;
    background: none;
}

/* Disabled: dimmed by the component, not by an `opacity-40` each page remembers or forgets. */
.cv-switch:has(input:disabled) {
    cursor: not-allowed;
}

.cv-switch:has(input:disabled) .cv-switch__track,
.cv-switch:has(input:disabled) .cv-switch__thumb {
    opacity: 0.45;
}

.cv-switch.cv-switch.cv-switch input:disabled {
    cursor: not-allowed;
}

/* ── Tooltip ─────────────────────────────────────────────────────────────────────────── */

/* Why a control cannot be used, on hover and on focus. Put it on a focusable wrapper
 * (`tabindex="0"`), never on the disabled control itself: a disabled input takes no focus and,
 * in several browsers, no pointer events, so a tooltip attached to it is one nobody sees.
 * The same sentence must also be on the page (and named by `aria-describedby`) — a tooltip
 * cannot be the only place a reason lives, because a phone has no hover. */
.cv-tip {
    position: relative;
    display: inline-flex;
    flex: none;
    border-radius: var(--cv-r-md);
    outline: none;
}

.cv-tip:focus-visible {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--cv-primary) 28%, transparent);
}

.cv-tip__bubble {
    position: absolute;
    bottom: calc(100% + 0.375rem);
    inset-inline-end: 0;
    z-index: var(--cv-z-tooltip);
    width: max-content;
    max-width: min(16rem, calc(100vw - 2rem));
    padding: 0.5rem 0.625rem;
    border-radius: var(--cv-r-sm);
    background: var(--cv-heading);
    color: var(--cv-card);
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.45;
    text-align: start;
    white-space: normal;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transform: translateY(2px);
    transition: opacity var(--cv-t-fast) var(--cv-ease), transform var(--cv-t-fast) var(--cv-ease),
        visibility 0s linear var(--cv-t-fast);
}

.cv-tip:hover .cv-tip__bubble,
.cv-tip:focus-visible .cv-tip__bubble,
.cv-tip:active .cv-tip__bubble {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
}

/* ── Badges and chips ────────────────────────────────────────────────────────────────── */

.cv-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    padding: 0.1875rem 0.5rem;
    border-radius: var(--cv-r-full);
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.4;
    /* `nowrap` without a cap makes the badge's min-content width the full pill, and a grid or
       flex ancestor then sizes itself to fit it. The marketing hero's illustration was 362px
       wide inside a 320px phone for exactly this reason: one badge reading "AutoPilot is
       writing your next post" refused to be narrower than that sentence, and the three panels
       around it went with it.

       It still does not wrap — a pill broken over two lines stops looking like a pill — but it
       may now be narrower than its text and say so with an ellipsis. */
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cv-badge--neutral {
    background: var(--cv-bg-subtle);
    color: var(--cv-muted);
}

.cv-badge--primary {
    background: var(--cv-primary-soft);
    color: var(--cv-primary-soft-ink);
}

.cv-badge--success {
    background: var(--cv-success-soft);
    color: var(--cv-success);
}

.cv-badge--warning {
    background: var(--cv-warning-soft);
    color: var(--cv-warning);
}

.cv-badge--danger {
    background: var(--cv-danger-soft);
    color: var(--cv-danger);
}

/* The one plan badge. See `--cv-violet-soft`: the ribbon violet is a band colour, not a
   surface to write white on. */
.cv-badge--premium {
    background: var(--cv-violet-soft);
    color: var(--cv-violet-soft-ink);
}

.cv-badge--info {
    background: var(--cv-info-soft);
    color: var(--cv-info);
}

/* A chip is a control, so it is a touch target; a badge is a label, so it is not. */
.cv-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-height: var(--cv-tap);
    padding: 0 0.875rem;
    border-radius: var(--cv-r-full);
    border: 1px solid var(--cv-border-strong);
    background: var(--cv-card);
    color: var(--cv-text);
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: all var(--cv-t) var(--cv-ease);
    -webkit-tap-highlight-color: transparent;
}

.cv-chip:hover {
    border-color: var(--cv-primary);
    color: var(--cv-primary);
}

.cv-chip[aria-pressed="true"],
.cv-chip.is-active {
    background: var(--cv-primary);
    border-color: var(--cv-primary);
    color: var(--cv-primary-ink);
}

/* ── Tables, and what a table becomes on a phone ─────────────────────────────────────── */

.cv-table-wrap {
    width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border-radius: var(--cv-r-xl);
    border: 1px solid var(--cv-border);
    background: var(--cv-card);
}

.cv-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: 0.875rem;
}

.cv-table th {
    position: sticky;
    top: 0;
    z-index: var(--cv-z-sticky);
    background: var(--cv-bg-subtle);
    color: var(--cv-muted);
    font-weight: 600;
    font-size: 0.75rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: start;
    padding: 0.75rem 1rem;
    white-space: nowrap;
    border-bottom: 1px solid var(--cv-border);
}

.cv-table td {
    padding: 0.875rem 1rem;
    color: var(--cv-text);
    border-bottom: 1px solid var(--cv-border);
    vertical-align: middle;
}

.cv-table tbody tr:last-child td {
    border-bottom: 0;
}

.cv-table tbody tr:hover td {
    background: var(--cv-bg-subtle);
}

/* Below `lg` the table becomes a stack of records. Each cell keeps its column name in
   `data-label`, because a value with no label is not data — it is a number in a row of
   numbers, and on a 360px screen that is all a squeezed table ever gives you.
   At `lg` and above it stays a real table. A `display: block` outside the media query
   (removed 2026-10-06) left the rows in a shrink-to-fit anonymous table box, so the header
   band, row borders and hover stopped partway across the card on narrow tables. */
@media (max-width: 1023px) {

    .cv-table--stacked thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .cv-table--stacked,
    .cv-table--stacked tbody,
    .cv-table--stacked tr,
    .cv-table--stacked td {
        display: block;
        width: 100%;
    }

    .cv-table--stacked tr {
        border: 1px solid var(--cv-border);
        border-radius: var(--cv-r-lg);
        background: var(--cv-card);
        padding: 0.25rem 0;
        margin-bottom: 0.75rem;
    }

    .cv-table--stacked td {
        display: flex;
        /* The row is a label and a value on one line, and neither of them could shrink: the
           label was `flex: none` by design, and the value is an anonymous flex item, which
           takes `min-width: auto` and cannot be selected to be told otherwise. So a long
           Indonesian phrase in a retention table pushed the row 12px past the page on three
           legal pages, in every one of the nine locales that spells it long.

           Wrapping is the answer rather than truncation: the value drops under its own label
           and the row grows, which on a phone is what the stacked table is for. `anywhere` and
           not `break-word`, because `break-word` is ignored when the browser computes the
           min-content width that caused this. */
        flex-wrap: wrap;
        align-items: baseline;
        justify-content: space-between;
        gap: 0.125rem 1rem;
        overflow-wrap: anywhere;
        border-bottom: 0;
        padding: 0.5rem 1rem;
        text-align: end;
    }

    .cv-table--stacked td::before {
        content: attr(data-label);
        flex: 0 1 auto;
        min-width: 0;
        font-size: 0.75rem;
        font-weight: 600;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--cv-muted);
        text-align: start;
    }

    .cv-table--stacked td:empty {
        display: none;
    }

    .cv-table-wrap--stacked {
        border: 0;
        background: transparent;
        overflow: visible;
    }
}

/* ── Sheets, modals, menus ───────────────────────────────────────────────────────────── */

.cv-scrim {
    position: fixed;
    inset: 0;
    background: var(--cv-overlay);
    z-index: var(--cv-z-sheet);
}

.cv-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: calc(var(--cv-z-sheet) + 1);
    background: var(--cv-card);
    border-radius: var(--cv-r-2xl) var(--cv-r-2xl) 0 0;
    box-shadow: var(--cv-shadow-lg);
    /* Never taller than the screen minus the notch, and it scrolls inside itself rather than
       pushing the page. */
    max-height: calc(100dvh - var(--cv-safe-t) - 2rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-bottom: max(var(--cv-safe-b), 0.75rem);
    /* In landscape this is full-width against both edges, so its content needs the side
       insets the bottom bar already takes. */
    padding-inline: max(var(--cv-safe-l), 0px) max(var(--cv-safe-r), 0px);
    animation: cv-sheet-in var(--cv-t-slow) var(--cv-ease);
}

@keyframes cv-sheet-in {
    from {
        transform: translateY(100%);
    }

    to {
        transform: translateY(0);
    }
}

/* The grab handle. Decorative, so it is hidden from assistive technology rather than
   announced as a meaningless element. */
.cv-sheet__handle {
    width: 2.5rem;
    height: 0.25rem;
    border-radius: var(--cv-r-full);
    background: var(--cv-border-strong);
    margin: 0.75rem auto 0.5rem;
}

.cv-modal {
    position: fixed;
    inset: 0;
    z-index: var(--cv-z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    padding-top: max(1rem, var(--cv-safe-t));
    padding-bottom: max(1rem, var(--cv-safe-b));
    /* And the sides. The safe area is not only the notch at the top: turn the phone and the
       notch is on the left or the right, and the rounded corners are always there. Every
       fixed thing in here had the vertical insets and none of them had the horizontal ones,
       which is a defect you only ever see in landscape -- the orientation nobody screenshots. */
    padding-inline: max(1rem, var(--cv-safe-l)) max(1rem, var(--cv-safe-r));
}

.cv-modal__panel {
    width: 100%;
    max-width: 32rem;
    max-height: calc(100dvh - 2rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-xl);
    box-shadow: var(--cv-shadow-lg);
    animation: cv-modal-in var(--cv-t) var(--cv-ease);
}

@keyframes cv-modal-in {
    from {
        opacity: 0;
        transform: scale(0.96) translateY(8px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

.cv-menu {
    position: absolute;
    z-index: var(--cv-z-drawer);
    min-width: 12rem;
    padding: 0.375rem;
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-lg);
    box-shadow: var(--cv-shadow-lg);
    animation: cv-menu-in var(--cv-t-fast) var(--cv-ease);
}

@keyframes cv-menu-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

.cv-menu__item {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    width: 100%;
    min-height: var(--cv-tap);
    padding: 0 0.75rem;
    border: 0;
    border-radius: var(--cv-r-sm);
    background: transparent;
    color: var(--cv-text);
    font: inherit;
    font-size: 0.875rem;
    text-align: start;
    text-decoration: none;
    cursor: pointer;
}

.cv-menu__item:hover {
    background: var(--cv-bg-subtle);
    color: var(--cv-heading);
}

/* ── Navigation ──────────────────────────────────────────────────────────────────────── */

.cv-nav-link {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-height: var(--cv-tap);
    padding: 0 0.875rem;
    flex: none;
    border-radius: var(--cv-r-md);
    color: var(--cv-sidebar-muted);
    text-decoration: none;
    font-weight: 500;
    font-size: 0.9375rem;
    transition: background-color var(--cv-t) var(--cv-ease), color var(--cv-t) var(--cv-ease);
}

.cv-nav-link:hover {
    background: rgba(255, 255, 255, 0.07);
    color: var(--cv-sidebar-ink);
}

.cv-nav-link.is-active {
    background: rgba(255, 255, 255, 0.1);
    color: var(--cv-sidebar-ink);
    font-weight: 600;
    position: relative;
}

/* The active marker is one of the ribbons, on the same slant idea: a vertical band on the
   leading edge. Logical inset so it moves to the right edge in an RTL locale. */
.cv-nav-link.is-active::before {
    content: "";
    position: absolute;
    inset-inline-start: -0.5rem;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 1.5rem;
    border-radius: var(--cv-r-full);
    background: var(--cv-accent);
}

/* On a desktop rail the 44px touch floor is the wrong number and it costs a lot of height:
   fifteen destinations plus four group headings comes to about 790px of navigation in roughly
   600px of rail, so the rail scrolled and the last rows looked cut off. Above `lg` there is no
   bottom bar and the pointer is a mouse; WCAG 2.2's figure for a pointer is 24px, and 36 is
   comfortable. The touch floor still applies everywhere it should. */
@media (min-width: 1024px) {
    .cv-nav-link {
        min-height: 2.25rem;
    }

    .cv-nav-group {
        margin-block: 0.75rem 0.25rem;
    }
}

.cv-bottom-bar {
    position: fixed;
    inset: auto 0 0 0;
    z-index: var(--cv-z-bottom-bar);
    display: flex;
    align-items: stretch;
    background: var(--cv-card);
    border-top: 1px solid var(--cv-border);
    padding-bottom: var(--cv-safe-b);
    padding-inline: max(var(--cv-safe-l), 0px) max(var(--cv-safe-r), 0px);
}

.cv-bottom-bar__item {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.125rem;
    min-height: var(--cv-bottom-bar-h);
    padding: 0.375rem 0.25rem;
    border: 0;
    background: transparent;
    color: var(--cv-muted);
    font: inherit;
    font-size: 0.6875rem;
    font-weight: 600;
    line-height: 1.25;
    text-decoration: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    /* Thai does not hyphenate and a long label will not break, so the label is allowed one
       line and an ellipsis rather than being allowed to wreck the row. */
    min-width: 0;
}

.cv-bottom-bar__item>span:last-child {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cv-bottom-bar__item.is-active {
    color: var(--cv-primary);
}

/* Reserve the bar's height once, here, instead of on every page. Sixteen pages each carrying
   their own spacer was the previous arrangement, and the three pages that forgot were the bug.

   Doubled class, for the reason `.cv-safe-bottom.cv-safe-bottom` is: both layouts put this on
   `<main class="cv-scroll … p-3 lg:p-6">`, and Tailwind's `p-3` is also 0-1-0 and is injected
   by the CDN after this file, so it won the tie and the reservation was 0.75rem — the last
   card on every portal page sat under the bar, and in the installed PWA under the home
   indicator too (reported 2026-09-23 on the dashboard's Recent Activity). */
.cv-scroll.cv-scroll {
    padding-bottom: calc(var(--cv-bottom-bar-h) + var(--cv-bottom-bar-overhang) + var(--cv-safe-b) + 1rem);
}

@media (min-width: 1024px) {
    .cv-scroll.cv-scroll {
        padding-bottom: 1.5rem;
    }
}

/* Anything floating above the bottom bar shares one clearance, rather than each element
   picking its own number. */
.cv-above-bar {
    bottom: calc(var(--cv-bottom-bar-h) + var(--cv-bottom-bar-overhang) + var(--cv-safe-b) + 0.75rem);
}

@media (min-width: 1024px) {
    .cv-above-bar {
        bottom: 1.5rem;
    }
}

/* A focused flow (a setup wizard) replaces the portal's bottom bar with its own.
   Two bars stacked at the foot of a phone, each with buttons, read as one broken control; while
   a `.cv-focus-flow` element is on the page the portal bar is hidden, the content's bottom
   reservation shrinks to the flow bar, and the flow provides its own way out (a close button in
   its header). The bar itself sits on the bottom edge, clear of the home indicator. */
body:has(.cv-focus-flow) .cv-bottom-bar {
    display: none;
}

body:has(.cv-focus-flow) .cv-scroll.cv-scroll {
    padding-bottom: calc(5rem + var(--cv-safe-b));
}

.cv-flow-bar {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 40;
    padding: 0.625rem 0.75rem calc(0.625rem + var(--cv-safe-b));
    background: var(--cv-card);
    border-top: 1px solid var(--cv-border);
}

@media (min-width: 1024px) {
    .cv-flow-bar {
        inset-inline-start: 16rem;
    }
}

.cv-app-header {
    position: sticky;
    top: 0;
    z-index: var(--cv-z-header);
    background: var(--cv-card);
    border-bottom: 1px solid var(--cv-border);
    padding-top: var(--cv-safe-t);
    /* The side inset is NOT here. It was, for an hour, and it was right at the time: landscape
       on a notched phone puts the inset on the sides, the header is the one strip that runs
       edge to edge, and the marketing wordmark was measured at x=24 inside a 47px inset -- 23px
       under the notch. `.cv-safe-x` was inert then, so the header rule was the only thing that
       could do it.

       `.cv-safe-x` works now, and both rules firing is worse than neither. The marketing header
       carries the class on its inner container and the portal headers carry it on the header
       itself, so with a rule here as well the marketing logo moved in by 120px at a 60px inset
       instead of 60: measured 16 -> 136, when 16 -> 76 is the answer. One inset, applied by one
       rule, on whichever element in each header actually holds the ink. */
}

/* ── Empty, loading, and the states nobody designs until they are seen ───────────────── */

/* A flat block that breathes, rather than a highlight sweeping across a blend. It says the
   same thing — something is coming — without a moving gradient. */
.cv-skeleton {
    background: var(--cv-bg-subtle);
    animation: cv-pulse 1.5s ease-in-out infinite;
    border-radius: var(--cv-r-sm);
}

@keyframes cv-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
}


.cv-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.75rem;
    padding: 3rem 1.5rem;
    color: var(--cv-muted);
}

.cv-empty__icon {
    display: grid;
    place-items: center;
    width: 4rem;
    height: 4rem;
    border-radius: var(--cv-r-full);
    background: var(--cv-brand-tint);
    color: var(--cv-primary-soft-ink);
}

.cv-empty__title {
    font-size: 1.0625rem;
    font-weight: 700;
    color: var(--cv-heading);
}

/* ── Utilities the layouts need and Tailwind has no token for ────────────────────────── */

/* A transparent border, not padding, and the class doubled.
 *
 * These three were inert everywhere they were used, and it took an installed PWA with a
 * simulated notch to see it: with the inset forced to 200px, `.cv-app-header`'s padding grew by
 * the full 200 and `div.max-w-7xl … px-4 sm:px-6 lg:px-8 cv-safe-x` and
 * `footer.…py-14 sm:py-20 cv-safe-bottom` grew by 0 of 200. Tailwind here is
 * `cdn.tailwindcss.com`, a runtime script: its sheet is injected into <head> after this file, so
 * it wins every one-class-to-one-class tie, and `px-4` beat `.cv-safe-x` on the one element that
 * had to respect the notch.
 *
 * Doubling the selector (0-2-0) wins that tie. But winning it with `padding` would be a second
 * bug: the same rule would then also replace the 16-32px of ordinary gutter Tailwind was there
 * to provide, and on every screen without a notch — which is nearly all of them — the content
 * would sit against the edge of the glass. The inset has to be added to that padding, not
 * substituted for it, and CSS cannot read a value it is overriding.
 *
 * A transparent border is the one box-model space that adds rather than replaces. It sits
 * outside the padding, it collapses to nothing when the inset is 0 (so an ordinary phone and
 * every desktop are byte-identical to before), and the element's own background paints under
 * it — which is exactly right for a header or a bottom bar, where the bar should run under the
 * notch and only the ink should move in.
 *
 * `border-inline-width` with two values is start/end, so this respects a right-hand notch in
 * landscape and an RTL document without naming left or right. */
.cv-safe-top.cv-safe-top {
    border-block-start: solid transparent;
    border-block-start-width: var(--cv-safe-t);
}

.cv-safe-bottom.cv-safe-bottom {
    border-block-end: solid transparent;
    border-block-end-width: var(--cv-safe-b);
}

.cv-safe-x.cv-safe-x {
    border-inline: solid transparent;
    border-inline-width: max(var(--cv-safe-l), 0px) max(var(--cv-safe-r), 0px);
}

/* Visible to a screen reader, invisible to everyone else. */
.cv-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.cv-sr-only:focus,
.cv-skip-link:focus-visible {
    position: fixed;
    top: calc(var(--cv-safe-t) + 0.5rem);
    /* Held 0.5rem from the edge, which in landscape on a notched phone is underneath the
       notch -- on the one control whose entire job is to be reachable before anything else. */
    inset-inline-start: calc(max(var(--cv-safe-l), var(--cv-safe-r)) + 0.5rem);
    width: auto;
    height: auto;
    padding: 0.75rem 1rem;
    margin: 0;
    clip: auto;
    clip-path: none;
    z-index: var(--cv-z-tooltip);
    background: var(--cv-primary);
    color: var(--cv-primary-ink);
    border-radius: var(--cv-r-md);
    box-shadow: var(--cv-shadow-lg);
}

/* A row that scrolls sideways on purpose, with the snap points that make it feel like a phone
   control rather than an accident. */
.cv-hscroll {
    display: flex;
    gap: 0.75rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 0.25rem;
}

.cv-hscroll::-webkit-scrollbar {
    display: none;
}

.cv-hscroll>* {
    scroll-snap-align: start;
    flex: none;
}

/* Grid and flex children may shrink. All of them.
 *
 * This is the fifth instance of one bug. A grid or flex item's `min-width` defaults to `auto`,
 * which resolves to its min-content width, and that beats every other constraint in the box —
 * `overflow-x: auto` on a descendant, the width of the container, all of it. Found so far in:
 *
 *   .cv-hscroll     the portal dashboard laid out 768px of content inside a 390px phone
 *   .cv-reveal      the same, one wrapper further out
 *   .cv-select      sized by the longest Facebook page name in the customer's account
 *   a Thai heading  an unbreakable cluster sizing the column it sat in
 *   a mockup panel  on the ChatMai page, sized by something deep inside it
 *
 * Each was fixed where it was found, which is how there came to be five of them. The general
 * statement is simply that a child of a grid or a flex container should be allowed to be
 * narrower than its contents want to be; the contents then wrap, scroll or ellipsize according
 * to their own rules, which is what those rules are for.
 *
 * Paired with `overflow-wrap: anywhere` on prose below, so that text which is now allowed to be
 * narrow actually wraps rather than spilling out of its own box. */
:where(.grid, .flex, .inline-flex) > * {
    min-width: 0;
}

/* ...and the same floor for the design system's own layout components.
 *
 * The rule above matches Tailwind *class names*. A component that is `display: grid` under its
 * own name — `.cv-stack`, `.cv-rail`, `.cv-spec` — is invisible to it, so its children kept
 * `min-width: auto` and could not shrink below their own min-content.
 *
 * That is not theoretical. On the marketing home page at 360px in Korean, one badge reading
 * "AutoPilot이 다음 게시물을 만들고 있습니다…" contributed 265px of min-content to its panel.
 * `max-width: 100%` caps what the badge *paints*; it does nothing to what the badge
 * *contributes*, which is the part that sizes the parent. So the panel was 333px wide inside a
 * 320px stack, the hero grid went with it, and the section clipped the difference. Korean,
 * Vietnamese and Indonesian only — the English sentence fits, which is why it survived every
 * look at the page.
 *
 * It is the third time `min-width: auto` has produced a finding in this rebuild, and the second
 * time the cause was a rule that matches class names rather than computed layout. */
:where(.cv-stack, .cv-rail, .cv-spec, .cv-spec__row, .cv-steps, .cv-ticks, .cv-ticks__item,
       .cv-media-card__body, .cv-media-card__actions, .cv-quick-tile, .cv-empty,
       .cv-bottom-bar, .cv-pager, .cv-hscroll, .cv-stack-loading) > * {
    min-width: 0;
}


/* Layout primitives that must be allowed to shrink.
 *
 * See the note on `.cv-hscroll` below. A grid or flex item defaults to `min-width: auto`, and
 * `overflow-x: auto` on a descendant cannot overrule an ancestor that has already sized itself
 * to its content. So the shrink permission has to be granted the whole way up, and these two
 * are the wrappers every page puts between its grid and its content. */
.cv-reveal,
.cv-stagger > * {
    min-width: 0;
}

/* `min-width: 0` is the whole reason this scroller scrolls.
 *
 * A grid or flex item's default `min-width` is `auto`, which means "at least as wide as my
 * content" — and it beats `overflow-x: auto`, so the rail simply grew instead of clipping.
 * The portal's dashboard put this rail in a grid, and the result was a <main> whose content
 * was 768px wide inside a 390px phone: everything past the fold on the right was drawn and
 * unreachable, because the *document* did not overflow, so nothing scrolled to reveal it.
 *
 * The document-level overflow check passed the whole time. It measures `documentElement`, and
 * the clipping happened one box further in. */
.cv-hscroll {
    min-width: 0;
}

/* A list that is still loading, drawn as the list it is about to be.
 *
 * Thirteen pages answered "is it loading?" with a rotating ring centred on an otherwise empty
 * page. A spinner says only "wait". A skeleton says what is coming and roughly how much of it,
 * so the layout does not jump when the data lands and the reader can start reading the shape
 * before there is anything to read. */
.cv-stack-loading {
    display: grid;
    gap: 0.75rem;
}

/* ── PWA ─────────────────────────────────────────────────────────────────────────────── */

/* Launched from the home screen there is no browser chrome, so the app owns the status bar
   area and has to paint it itself. In a tab these rules do nothing. */
@media (display-mode: standalone) {
    /* `.cv-hide-standalone` is in no markup in any of the three apps -- a grep returns this
       file and nothing else -- which is worth saying out loud, because `standalone.py` has
       been printing "standalone-only shown 0/0" and the zero on the left reads like a pass
       when the zero on the right is the reason for it.

       `.cv-install-card` is the one thing that genuinely must not appear here, and it is
       named explicitly rather than left to the class above. `cv-pwa.js` already hides it and
       an installed window was measured with 0 cards in all 30 states, so this is a belt: the
       card is an invitation to install an app the reader is already inside, and a JS file
       that fails to load or runs late should not be the only thing preventing it. */
    .cv-hide-standalone,
    .cv-install-card {
        display: none !important;
    }

    body {
        /* Nothing to pull-to-refresh against once there is no browser. */
        overscroll-behavior: none;
    }
}

.cv-only-standalone {
    display: none;
}

@media (display-mode: standalone) {
    .cv-only-standalone {
        display: revert;
    }
}

.cv-install-card {
    position: fixed;
    z-index: var(--cv-z-toast);
    inset-inline: calc(var(--cv-safe-l) + 0.75rem) calc(var(--cv-safe-r) + 0.75rem);
    /* `--cv-consent-h` is set by cv-consent.js only while the cookie notice is open, and it
       already carries the gap that goes between the two. Unset it resolves to 0px and this is
       the bottom-bar calculation it always was. Both cards are `position: fixed` at the same
       edge, so without this they are drawn on top of each other on a phone. */
    bottom: calc(var(--cv-bottom-bar-h) + var(--cv-bottom-bar-overhang) + var(--cv-safe-b) + var(--cv-consent-h, 0px) + 0.75rem);
    max-width: 26rem;
    margin-inline: auto;
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-xl);
    box-shadow: var(--cv-shadow-lg);
    padding: 1rem;
    animation: cv-sheet-in var(--cv-t-slow) var(--cv-ease);
}

@media (min-width: 1024px) {
    .cv-install-card {
        bottom: calc(1.5rem + var(--cv-consent-h, 0px));
        inset-inline: auto 1.5rem;
    }
}

/* ── The cookie notice ───────────────────────────────────────────────────────────────── */

/* This is the notice and nothing else. Whether it appears, what it remembers, and whether the
 * analytics tag is ever fetched are cv-consent.js's business.
 *
 * The marketing site is static SSR by design and stays that way, so the markup is rendered on
 * the server with `hidden` on it and the script takes the attribute off. A reader with
 * JavaScript disabled therefore sees no notice — and gets no analytics either, because the
 * same script is what appends the tag. Nothing is set, so there is nothing to ask about. */
.cv-consent {
    position: fixed;
    z-index: var(--cv-z-toast);
    inset-inline: calc(var(--cv-safe-l) + 0.75rem) calc(var(--cv-safe-r) + 0.75rem);
    /* Above the bottom bar and the home indicator, never across them. */
    bottom: calc(var(--cv-bottom-bar-h) + var(--cv-bottom-bar-overhang) + var(--cv-safe-b) + 0.75rem);
    max-width: 44rem;
    margin-inline: auto;
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-xl);
    box-shadow: var(--cv-shadow-lg);
    padding: 1rem;
    animation: cv-sheet-in var(--cv-t-slow) var(--cv-ease);
}

/* Stated rather than left to the user agent. `[hidden]` is a UA rule at specificity 0-0-1, and
   anything in this file that later sets a `display` on the notice would outrank it and put the
   notice on screen before the script had decided anything. */
.cv-consent[hidden] {
    display: none;
}

@media (min-width: 1024px) {
    .cv-consent {
        bottom: 1.5rem;
        padding: 1.25rem;
    }
}

.cv-consent__inner {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
}

.cv-consent__icon {
    flex: none;
    color: var(--cv-primary);
    font-size: 24px;
    line-height: 1;
}

.cv-consent__body {
    flex: 1;
    min-width: 0;
}

.cv-consent__title {
    margin: 0;
    font-weight: 700;
    color: var(--cv-heading);
}

.cv-consent__text {
    margin: 0.25rem 0 0;
    font-size: 0.875rem;
    line-height: 1.6;
    color: var(--cv-muted);
}

.cv-consent__text a {
    color: var(--cv-primary);
    font-weight: 600;
    text-decoration: underline;
}

/* Wrapping, not scrolling. Three controls do not fit one phone line in German, and a row that
   overflows instead of wrapping puts the second of two choices off the side of the screen. */
.cv-consent__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.875rem;
}

/* ── Print ───────────────────────────────────────────────────────────────────────────── */

@media print {

    .cv-bottom-bar,
    .cv-app-header,
    .cv-consent,
    .cv-install-card,
    .cv-skip-link {
        display: none !important;
    }

    .cv-scroll.cv-scroll {
        padding-bottom: 0;
    }
}

/* ── The theme control ───────────────────────────────────────────────────────────────── */

.cv-theme-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--cv-tap);
    height: var(--cv-tap);
    flex: none;
    border: 0;
    border-radius: var(--cv-r-md);
    background: transparent;
    color: var(--cv-muted);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--cv-t) var(--cv-ease), color var(--cv-t) var(--cv-ease);
}

.cv-theme-btn:hover {
    background: var(--cv-bg-subtle);
    color: var(--cv-heading);
}

/* Which of the three icons shows is decided in CSS from the stored preference, so the control
   is correct on the server-rendered first paint — a Blazor component would have had to wait
   for its circuit and would have flickered through the wrong icon every time.

   Written as `.cv-theme-btn .cv-theme-btn__icon` rather than on the icon alone. The icon also
   carries `.material-symbols-rounded`, and Google's font stylesheet — which loads later in the
   head than this file — sets `display: inline-block` on that class at the same specificity. At
   0-1-0 each, the later sheet won the tie and all three icons showed at once. */
.cv-theme-btn .cv-theme-btn__icon {
    display: none;
}

.cv-theme-btn[data-pref="system"] .cv-theme-btn__icon--system,
.cv-theme-btn[data-pref="light"] .cv-theme-btn__icon--light,
.cv-theme-btn[data-pref="dark"] .cv-theme-btn__icon--dark {
    display: block;
}

/* Before the runtime has stamped `data-pref`, show the system icon rather than nothing. */
.cv-theme-btn:not([data-pref]) .cv-theme-btn__icon--system {
    display: block;
}

/* ── Quick tiles ─────────────────────────────────────────────────────────────────────── */

/* A shortcut on a dashboard, not a call to action. These were three saturated gradients —
   blue, green and purple — each with white text and its own drop shadow, sitting directly
   beneath the page's actual primary button and shouting louder than it. One indigo now, told
   apart by the icon, quiet enough that the eye reaches the real action first. */

.cv-quick-tile {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-height: var(--cv-tap);
    padding: 0.75rem;
    border-radius: var(--cv-r-lg);
    border: 1px solid var(--cv-border);
    background: var(--cv-bg-subtle);
    color: var(--cv-text);
    text-decoration: none;
    transition: background-color var(--cv-t) var(--cv-ease),
        border-color var(--cv-t) var(--cv-ease),
        transform var(--cv-t) var(--cv-ease);
    -webkit-tap-highlight-color: transparent;
}

.cv-quick-tile:hover {
    background: var(--cv-primary-soft);
    border-color: var(--cv-primary);
    transform: translateY(-1px);
}

.cv-quick-tile__icon {
    display: grid;
    place-items: center;
    flex: none;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--cv-r-md);
    background: var(--cv-primary-soft);
    color: var(--cv-primary-soft-ink);
    transition: background-color var(--cv-t) var(--cv-ease), color var(--cv-t) var(--cv-ease);
}

.cv-quick-tile:hover .cv-quick-tile__icon {
    background: var(--cv-primary);
    color: var(--cv-primary-ink);
}

.cv-quick-tile__title {
    display: block;
    font-weight: 600;
    font-size: 0.9375rem;
    line-height: 1.35;
    color: var(--cv-heading);
}

.cv-quick-tile__desc {
    display: block;
    font-size: 0.75rem;
    line-height: 1.4;
    margin-top: 0.125rem;
    color: var(--cv-muted);
}

/* ── The back office rail ────────────────────────────────────────────────────────────── */

/* Positioned relative so the three-band stripe across its top has something to attach to, and
   given its z-index from the ladder rather than picking one — as a drawer it has to sit above
   the scrim, and the scrim's own value is declared two hundred lines up. */
.cv-admin-rail {
    position: fixed;
    z-index: calc(var(--cv-z-sheet) + 1);
    background: var(--cv-sidebar);
    color: var(--cv-sidebar-ink);
    /* A drawer against the left edge, which in landscape is where the notch is. */
    padding-inline-start: var(--cv-safe-l);
}

@media (min-width: 1024px) {
    .cv-admin-rail {
        position: static;
        z-index: auto;
    }
}

/* A drawer parked off the side of the screen is still in the tab order and still read out.
   `translate-x-full` moves the pixels and nothing else: the back office's closed navigation
   held sixteen links and a Close button that a keyboard reached before the page, and that a
   screen reader read as the first thing on every phone page. It also made the Close button
   unclickable-but-visible, which is how the harness found it -- sixteen pages, every one
   reporting a control it could not press.

   `visibility: hidden` takes it out of both. The delay is only on the way out, so the drawer
   still slides closed rather than vanishing; opening removes the class outright and is
   immediate. */
.cv-offcanvas {
    visibility: hidden;
    transition: visibility 0s linear 300ms;
}

@media (min-width: 1024px) {
    .cv-offcanvas {
        visibility: visible;
    }
}

/* ── Tap targets ─────────────────────────────────────────────────────────────────────── */

/* ═══════════════════════════════════════════════════
   Tap targets on a phone
   ═══════════════════════════════════════════════════

   The audit measured every control at 390px and 360px: row actions were 26x37, form fields were
   34-42px tall, and the delete icon on a history row was 26px square. The floor is 44x44 —
   Apple's, and the one a Thai customer approving posts one-handed on the train feels first.

   Set as a baseline here rather than on each control, because there are several hundred of them
   across sixty-six pages and the next page added would start out wrong again. This lived in
   the customer portal's own stylesheet until the rebuild, which is why the back office had
   never had it: 466 undersized controls on its first measured run, every one of them a shape
   this block already knew how to fix.

   `:has()` picks out the icon-only buttons precisely — a button or link whose only child is an
   icon glyph — so a link inside a sentence is left alone and only the things a finger aims at
   grow. Desktop keeps the compact sizes; a mouse is not a thumb.

   **What this block cannot see.** `a[href]:where(.flex, .inline-flex, .block, .w-full)`
   matches Tailwind *class names*, not computed layout. A component class that happens to be
   `display: flex` — `.legal-toc-link`, `.nav-link` and `.legal-cross-link` on the marketing
   site — carries none of those class names and is invisible to this rule. Those three
   accounted for most of that app's 870 tap-target findings, and each now sets its own
   `min-height` where it is defined.

   So: if you write a component that is a target, give it `min-height: var(--cv-tap)` there.
   Do not expect this block to find it. */
@media (max-width: 1023px) {

    /* `:not(.cv-btn)` is load-bearing, and the reason is a trap worth writing down.
     *
     * `:only-child` counts *element* children. A button holding one icon span and a text label
     * has exactly one element child, so it matches this "icon-only" selector — and the
     * `display` here then beats Tailwind's `.hidden`, because `button:has(> .x:only-child)`
     * is 0-2-1 against 0-1-0.
     *
     * The symptom was the portal dashboard's "create a post" button, written
     * `class="cv-btn cv-btn--brand hidden sm:inline-flex"` so it would give way to the bottom
     * bar's raised Create on a phone. It showed anyway, at every width, and the class that was
     * supposed to hide it was being overruled by a rule about tap targets.
     *
     * Anything on the design system declares an icon-only button as `.cv-btn--icon`, which
     * sizes itself. This heuristic is for markup that has not been converted yet, so excluding
     * `.cv-btn` costs nothing and removes the conflict. */
    /* `:not(:where(.flex, …))` is the second half of the same lesson.
     *
     * The exclusion above stops this rule fighting `.hidden` on design-system buttons. It does
     * nothing for an ordinary link that happens to hold one icon element and a text node —
     * `<a class="inline-flex"><span>mail</span> support@contentvelo.com</a>` — which is still
     * "icon-only" as far as `:only-child` is concerned, because a text node is not a child
     * element. The `justify-content: center` below then centred both email addresses on the
     * contact page inside their own full-width links, while every other line on the page was
     * flush left. It read as a mistake because it was one.
     *
     * An element carrying a layout class has had its arrangement decided already. All this
     * rule still owes it is the 44px floor. The selector twenty lines down uses the same
     * `:where(...)` test to decide what counts as a finger target. */
    button:not(.cv-btn):not(:where(.flex, .inline-flex, .grid, .inline-grid, .w-full)):has(> .material-symbols-rounded:only-child),
    a:not(.cv-btn):not(:where(.flex, .inline-flex, .grid, .inline-grid, .w-full)):has(> .material-symbols-rounded:only-child),
    button:not(.cv-btn):not(:where(.flex, .inline-flex, .grid, .inline-grid, .w-full)):has(> iconify-icon:only-child),
    a:not(.cv-btn):not(:where(.flex, .inline-flex, .grid, .inline-grid, .w-full)):has(> iconify-icon:only-child) {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* The floor still applies to the ones excluded above; only the arrangement does not.
       `min-width` is kept here too: it changes nothing for a control that is already wider,
       and it is the whole point of the rule for one that is not. */
    button:not(.cv-btn):where(.flex, .inline-flex, .grid, .inline-grid, .w-full):has(> .material-symbols-rounded:only-child),
    a:not(.cv-btn):where(.flex, .inline-flex, .grid, .inline-grid, .w-full):has(> .material-symbols-rounded:only-child),
    button:not(.cv-btn):where(.flex, .inline-flex, .grid, .inline-grid, .w-full):has(> iconify-icon:only-child),
    a:not(.cv-btn):where(.flex, .inline-flex, .grid, .inline-grid, .w-full):has(> iconify-icon:only-child) {
        min-width: 44px;
        min-height: 44px;
    }

    input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="range"]),
    select,
    textarea {
        min-height: 44px;
    }

    /* Every button, and the links in the header — the two places on a phone where the target is
       something a finger aims at rather than a word inside a sentence. The bottom bar sets its
       own heights and is left alone. */
    button:not(.btm-nav-item) {
        min-width: 44px;
    }

    button:not(.btm-nav-item),
    header a[href],
    /* A link laid out as a button is a target in the same sense a button is — the social
       sign-ins, "switch to sign in", a card's primary action. A link inside a sentence is not,
       and is left alone. */
    a[href]:where(.flex, .inline-flex, .block, .w-full) {
        min-height: 44px;
    }

    /* A checkbox stays its own size — the label beside it is the target, so the label is what
       has to be big enough to hit. */
    label:has(> input[type="checkbox"]),
    label:has(> input[type="radio"]) {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }
}

/* ── The pointer floor ───────────────────────────────────────────────────────────────── */

/* Above `lg` the touch floor is lifted, and until now nothing replaced it — a text button
   measured 64x16, which is under WCAG 2.2's 24px minimum for a pointer as well. This is that
   minimum and nothing more: it does not turn a compact desktop control into a thumb-sized one,
   it stops one being sixteen pixels tall. */
@media (min-width: 1024px) {

    button,
    [role="button"],
    a[href]:where(.flex, .inline-flex, .block, .w-full) {
        min-height: 24px;
        min-width: 24px;
    }
}

/* ── The 16px floor on form fields ───────────────────────────────────────────────────── */

/* Below 16px, iOS Safari zooms the page when a field takes focus — and it does not zoom back.
   The customer is then on a viewport wider than the one every measurement in this project was
   taken at, with the layout they were given no longer the layout they see.
 
   `text-sm` is 14px and sits on form fields throughout this product: the sign-in form, the
   register form, every search box and every filter. Fixing them one at a time means fixing the
   next one somebody writes, too.

   `!important` is deliberate, and this is the case it exists for. A Tailwind utility is a class
   at 0-1-0; an element selector is 0-0-1, and the CDN's stylesheet is injected into the head
   after this file, so it wins the cascade twice over. The rule is scoped to coarse pointers, so
   a mouse still gets whatever size the page asked for. */
@media (pointer: coarse) {

    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="hidden"]),
    select,
    textarea {
        font-size: 16px !important;
    }
}

/* ── Beating the forms plugin ────────────────────────────────────────────────────────── */

/* The Tailwind CDN is loaded with `?plugins=forms`, which emits
 *
 *     [type='text'],[type='email'],[type='password'],… { background-color:#fff; … }
 *
 * That is an attribute selector at 0-1-0 — the same specificity as `.cv-input` — and the CDN
 * injects its stylesheet into the head at runtime, so it lands last and wins the tie.
 *
 * The symptom was strange enough to be worth recording: on the sign-in form the email field
 * was correct and the password field was a white box in the dark theme. Both carry `cv-input`.
 * The difference is that Blazor's `InputText` renders no `type` attribute unless one is given,
 * so `[type='email']` never matched the email field — and `type="password"` was written by
 * hand on the other one, so it did.
 *
 * `input.cv-input` is 0-1-1 and settles it. Repeating the element name is the whole fix.
 */

input.cv-input,
select.cv-select,
textarea.cv-textarea {
    background-color: var(--cv-card);
    border-color: var(--cv-border-strong);
    border-radius: var(--cv-r-md);
    color: var(--cv-text);
    font-size: 16px;
}

input.cv-input:focus,
select.cv-select:focus,
textarea.cv-textarea:focus {
    border-color: var(--cv-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--cv-primary) 28%, transparent);
    outline: none;
}

/* ── Checkboxes and radios ───────────────────────────────────────────────────────────── */

/* The forms plugin sets `appearance: none` on these and then draws them itself. Its drawing
 * assumes its own palette, so in the dark theme an unchecked box was a solid white square —
 * and this project's Tailwind config sets `borderRadius.DEFAULT` to 0.75rem, so `rounded` on a
 * 16px box made it a white *circle*. On the sign-in form that is what "keep me signed in"
 * looked like.
 *
 * Drawn here instead, on the tokens, with the tick as an inline SVG so it needs no request and
 * cannot 404. The box stays 18px: the label around it is the tap target, which is what the
 * 44px rule above gives it.
 */
input[type="checkbox"],
input[type="radio"] {
    appearance: none;
    -webkit-appearance: none;
    width: 1.125rem;
    height: 1.125rem;
    flex: none;
    margin: 0;
    border: 1px solid var(--cv-border-strong);
    border-radius: var(--cv-r-xs);
    background-color: var(--cv-card);
    background-repeat: no-repeat;
    background-position: center;
    background-size: 0.875rem;
    cursor: pointer;
    transition: background-color var(--cv-t-fast) var(--cv-ease),
        border-color var(--cv-t-fast) var(--cv-ease);
}

input[type="radio"] {
    border-radius: var(--cv-r-full);
}

input[type="checkbox"]:checked,
input[type="radio"]:checked {
    background-color: var(--cv-primary);
    border-color: var(--cv-primary);
}

input[type="checkbox"]:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.5 3.5L13 5'/%3E%3C/svg%3E");
}

input[type="radio"]:checked {
    background-image: radial-gradient(circle, #FFFFFF 0 34%, transparent 36%);
    background-size: 100% 100%;
}

input[type="checkbox"]:disabled,
input[type="radio"]:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   Composition primitives
   ══════════════════════════════════════════════════════════════════════════════════════

   The parts a page is *composed* from, as opposed to the controls it is furnished with. They
   exist because the first pass at this rebuild put every page onto the tokens and left the
   compositions alone — nine sections of `py-24`, each a centred heading over a grid of cards,
   which is the shape the pages already had. Paint, not structure.

   What follows is the vocabulary for building differently: a rail instead of a row of boxes, a
   timeline instead of a 2x2 grid, a spec list instead of six feature cards. Each is chosen so
   that the mobile form is the natural one and the desktop form is its elaboration, rather than
   the other way round. */

/* ── Section rhythm ──────────────────────────────────────────────────────────────────── */

/* One scale for vertical rhythm, so sections relate to each other instead of each picking a
   `py-` value. Tighter on a phone than the 96px every section used to carry: six rems of air
   above a heading on a 390px screen is a third of the viewport spent on nothing. */
.cv-section {
    padding-block: 3.5rem;
}

@media (min-width: 768px) {
    .cv-section {
        padding-block: 5.5rem;
    }
}

.cv-section--tight {
    padding-block: 2.5rem;
}

@media (min-width: 768px) {
    .cv-section--tight {
        padding-block: 3.5rem;
    }
}

.cv-wrap {
    width: 100%;
    max-width: 76rem;
    margin-inline: auto;
    padding-inline: 1.25rem;
}

@media (min-width: 768px) {
    .cv-wrap {
        padding-inline: 2rem;
    }
}

.cv-wrap--narrow {
    max-width: 48rem;
}

/* An eyebrow: the small line above a heading that says what kind of thing follows. */
.cv-eyebrow {
    display: inline-flex;
    /* It is an icon and a line of small caps, and both were flex items that refused to shrink
       -- so in Korean, Vietnamese and Indonesian the hero eyebrow sized the whole hero grid and
       took the home page 29 to 42px wider than the phone it was on. The English copy fits, which
       is why it survived every look at it. */
    flex-wrap: wrap;
    max-width: 100%;
    overflow-wrap: anywhere;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--cv-primary);
}

/* Display type. Clamped rather than stepped through breakpoints, so a headline is as large as
   the viewport can carry at every width instead of jumping at three points. */
.cv-display {
    font-size: clamp(2rem, 7vw, 3.75rem);
    font-weight: 800;
    letter-spacing: -0.03em;
    line-height: 1.12;
    color: var(--cv-heading);
}

.cv-h2 {
    font-size: clamp(1.5rem, 4.2vw, 2.5rem);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.2;
    color: var(--cv-heading);
}

.cv-lead {
    font-size: clamp(1rem, 2.2vw, 1.1875rem);
    line-height: 1.65;
    color: var(--cv-muted);
}

/* Thai stacks a vowel above the consonant and a tone mark above that, and 1.12 clips the tone
   mark off a display heading. Given back per script, so Latin keeps its tight setting. */
:lang(th) .cv-display,
:lang(th) .cv-h2 {
    line-height: 1.35;
}

/* ── Rail ────────────────────────────────────────────────────────────────────────────── */

/* Numbers separated by hairlines rather than boxed. Three boxed statistics in a row is the
   most-used shape on the web, and it makes each number look like a separate claim; a rail
   reads as one sentence with three parts. It stays a rail on a phone and scrolls, because
   stacking three numbers vertically loses the comparison that is the point of grouping them. */
.cv-rail {
    display: grid;
    gap: 0;
}

/* Below `sm` the rail stacks and the hairline moves to the top of each row.

   It was a horizontal scroller at every width, and at 390px three items sized to their content
   do not fit: the third sat off the right-hand edge with nothing on screen to say it was there.
   A number the reader never sees is not proof of anything. Stacked, all three are visible and
   the hairlines still group them. */
.cv-rail__item {
    padding-block: 0.875rem;
    border-top: 1px solid var(--cv-border);
    min-width: 0;
}

.cv-rail__item:first-child {
    border-top: 0;
    padding-top: 0;
}

@media (min-width: 640px) {
    .cv-rail {
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
    }

    .cv-rail__item {
        padding-block: 0;
        padding-inline: 1.25rem;
        border-top: 0;
        border-inline-start: 1px solid var(--cv-border);
    }

    .cv-rail__item:first-child {
        padding-inline-start: 0;
        border-inline-start: 0;
    }
}

.cv-rail__value {
    display: block;
    font-size: clamp(1.5rem, 4vw, 2rem);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.2;
    color: var(--cv-heading);
}

.cv-rail__label {
    display: block;
    margin-top: 0.25rem;
    font-size: 0.75rem;
    line-height: 1.45;
    color: var(--cv-muted);
}

/* ── Steps ───────────────────────────────────────────────────────────────────────────── */

/* A sequence, drawn as one. Four cards in a 2x2 grid say "four things"; a timeline says "this,
   then this" — which is what a loop that runs itself actually is. The connecting line carries
   the mark's three bands, so the progression is the brand rather than a decoration borrowed
   from it. */
.cv-steps {
    position: relative;
    display: grid;
    gap: 2rem;
    counter-reset: cv-step;
}

.cv-steps::before {
    content: "";
    position: absolute;
    inset-inline-start: 1.0625rem;
    top: 0.5rem;
    bottom: 0.5rem;
    width: 2px;
    border-radius: var(--cv-r-full);
    background: linear-gradient(180deg,
            var(--cv-ribbon-indigo) 0 33.33%,
            var(--cv-ribbon-cyan) 33.33% 66.66%,
            var(--cv-ribbon-violet) 66.66% 100%);
    opacity: 0.5;
}

.cv-step {
    position: relative;
    min-width: 0;
    padding-inline-start: 3.5rem;
    counter-increment: cv-step;
}

.cv-step::before {
    content: counter(cv-step, decimal-leading-zero);
    position: absolute;
    inset-inline-start: 0;
    top: 0;
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: var(--cv-r-full);
    background: var(--cv-card);
    border: 2px solid var(--cv-primary);
    color: var(--cv-primary);
    font-size: 0.75rem;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

.cv-step__title {
    font-size: 1.0625rem;
    font-weight: 700;
    line-height: 1.35;
    color: var(--cv-heading);
    margin-bottom: 0.375rem;
}

.cv-step__body {
    color: var(--cv-muted);
    line-height: 1.6;
}

/* Above `lg` the line turns and runs across the top, so the sequence reads left to right the
   way the mark does. */
@media (min-width: 1024px) {
    .cv-steps {
        grid-auto-flow: column;
        /* `1fr` is `minmax(auto, 1fr)`: a column refuses to shrink below its content's
           min-content, so one wide child -- a button, a long unbroken label -- widens every
           column and overflows the track. `minmax(0, 1fr)` keeps three equal columns whatever
           they hold. */
        grid-auto-columns: minmax(0, 1fr);
        gap: 2rem;
        padding-top: 3.5rem;
    }

    .cv-steps::before {
        inset-inline: 1.125rem 1.125rem;
        top: 1.0625rem;
        bottom: auto;
        width: auto;
        height: 2px;
    }

    .cv-step {
        padding-inline-start: 0;
        padding-top: 0;
    }

    .cv-step::before {
        top: -3.5rem;
    }
}

/* ── Spec list ───────────────────────────────────────────────────────────────────────── */

/* Six capabilities as six padded cards in a grid is a lot of furniture around not much text. A
   spec list is denser, scans faster, and does not pretend each item is a separate product. */
.cv-spec {
    display: grid;
    gap: 0;
    border-top: 1px solid var(--cv-border);
}

@media (min-width: 768px) {
    .cv-spec {
        grid-template-columns: 1fr 1fr;
        column-gap: 3rem;
    }
}

.cv-spec__row {
    display: flex;
    gap: 1rem;
    align-items: flex-start;
    padding-block: 1.25rem;
    border-bottom: 1px solid var(--cv-border);
}

.cv-spec__icon {
    display: grid;
    place-items: center;
    flex: none;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: var(--cv-r-md);
    background: var(--cv-primary-soft);
    color: var(--cv-primary-soft-ink);
}

/* A third party's logo in the icon square: the mark itself fills the square, in its own colours,
   with no chip of ours behind it. `color` only reaches the one mark that uses currentColor —
   TikTok's note — which its owner publishes in black on light and white on dark. */
.cv-spec__icon--logo {
    background: none;
    color: var(--cv-heading);
}

.cv-spec__title {
    font-weight: 700;
    line-height: 1.4;
    color: var(--cv-heading);
}

.cv-spec__body {
    margin-top: 0.25rem;
    font-size: 0.9375rem;
    line-height: 1.6;
    color: var(--cv-muted);
}

/* ── Ticked list ─────────────────────────────────────────────────────────────────────── */

.cv-ticks {
    display: grid;
    gap: 0;
    border-top: 1px solid var(--cv-border);
}

.cv-ticks__item {
    display: flex;
    gap: 0.875rem;
    align-items: flex-start;
    padding-block: 1rem;
    border-bottom: 1px solid var(--cv-border);
    color: var(--cv-text);
    line-height: 1.55;
}

.cv-ticks__item::before {
    content: "";
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
    margin-top: 0.125rem;
    border-radius: var(--cv-r-full);
    background-color: var(--cv-primary-soft);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%233F33CF' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3L12.5 5'/%3E%3C/svg%3E");
    background-size: 0.875rem;
    background-position: center;
    background-repeat: no-repeat;
}

.dark .cv-ticks__item::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23A79FFF' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3L12.5 5'/%3E%3C/svg%3E");
}

/* ── Glow ────────────────────────────────────────────────────────────────────────────── */

/* This was a three-colour radial wash behind every hero, and it was the most template-like
   thing in the product. What replaces it is a flat band of the page's own subtle tone, closed
   by a hairline — the headline sits on a surface with an edge instead of in a haze. The class
   name stays because the markup spells it in a dozen places; there is no glow left in it. */
.cv-glow {
    position: relative;
    isolation: isolate;
    /* `clip`, not `hidden`. The glow below bleeds 10% past each edge so the wash reaches the
       corners without a visible seam — and an unclipped absolute child that wide makes the
       document scroll sideways: measured at 429px on a 390px viewport and 1584 on 1440, which
       is exactly the 10% on each side. `overflow: hidden` would fix it and would also turn
       this into a scroll container, which breaks `position: sticky` for anything inside it.
       `overflow: clip` cuts the overflow without creating one. */
    overflow: clip;
}

.cv-glow::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 100%;
    z-index: -1;
    pointer-events: none;
    background: var(--cv-bg-subtle);
    border-bottom: 1px solid var(--cv-border);
}

/* The identity, as a rule rather than a field: three solid segments across the very top. */
.cv-glow::after {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    z-index: 0;
    pointer-events: none;
    background: var(--cv-brand-bar);
}

/* ── Slanted stack ───────────────────────────────────────────────────────────────────── */

/* Three panels on the mark's own slant. The logo is three bands offset along a 114-degree
   axis; this is the same idea at the scale of a layout, which is how a page can be tied to a
   trademark without printing the trademark on it. */
.cv-stack {
    display: grid;
    gap: 0.75rem;
}

.cv-stack__panel {
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-xl);
    padding: 1rem 1.125rem;
    box-shadow: var(--cv-shadow-md);
}

/* The offset applies only where there is room for it. On a phone the panels stack square,
   because a slanted layout at 390px is a layout with less usable width. */
@media (min-width: 768px) {
    .cv-stack__panel:nth-child(1) {
        margin-inline-end: 3rem;
    }

    .cv-stack__panel:nth-child(2) {
        margin-inline-start: 1.5rem;
        margin-inline-end: 1.5rem;
    }

    .cv-stack__panel:nth-child(3) {
        margin-inline-start: 3rem;
    }
}

.cv-stack__panel--indigo {
    border-inline-start: 3px solid var(--cv-ribbon-indigo);
}

.cv-stack__panel--cyan {
    border-inline-start: 3px solid var(--cv-ribbon-cyan);
}

.cv-stack__panel--violet {
    border-inline-start: 3px solid var(--cv-ribbon-violet);
}

/* ── Band ────────────────────────────────────────────────────────────────────────────── */

/* A full-bleed dark section, used to break the rhythm once rather than to decorate. It carries
   its own colours because it is dark in both themes and the page's tokens are not.

   #14142A rather than #0B0B14, and the difference is the whole point: #0B0B14 *is* the dark
   theme's page colour, so in dark mode the band and the page were the same black and the break
   in rhythm the band exists to provide simply was not there. The navy is obviously dark against
   the light page and visibly a panel against the dark one. */
.cv-band {
    position: relative;
    overflow: hidden;
    background: var(--cv-band-bg);
    color: var(--cv-band-ink);
}

/* A hairline top and bottom, so the band reads as a deliberate panel in the dark theme rather
   than as a patch where the background happens to change. */
.cv-band::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-block: 1px solid var(--cv-band-wash);
}

.cv-band__muted {
    color: var(--cv-band-muted);
}

/* The hairlines a band rules its own content with -- a dl of specifications, a row of notes.
   They were `style="border-top: 1px solid rgb(255 255 255 / .14)"` written out six times on
   the home page, which is a colour decision living in markup. */
.cv-band-rule-t {
    border-top: 1px solid var(--cv-band-line);
}

.cv-band-rule-b {
    border-bottom: 1px solid var(--cv-band-line);
}

/* The closing band, filled with the mark's own indigo rather than the near-black one.
 *
 * It needs its own secondary tone: #A5A5B8 is tuned for #14142A and measures 7:1 there, but
 * only 2.46:1 on #5145F5. This one is 4.68:1 on the fill, and the fill is fixed in both themes
 * for the reason in the token note — a colour that identifies the product is not a surface of
 * the product, so it does not lighten when the page does. */
.cv-band--brand {
    background: var(--cv-brand-fill);
    color: var(--cv-brand-ink);
}

.cv-band--brand .cv-band__muted {
    color: var(--cv-band-brand-muted);
}

/* Type inside a band takes the band's ink, not the page's.
 *
 * This is the whole of what was wrong with the marketing site's dark mode, and it is worth
 * writing down because it looks like the opposite of a dark-mode bug. `.cv-h2` sets
 * `color: var(--cv-heading)`, which is the *page's* heading colour and therefore switches with
 * the theme -- but a band does not switch, so the two move independently. On the closing band
 * of /features, /how-it-works, /pricing and /product the heading is #F2F2F7 on the indigo in
 * the dark theme (5.35:1) and #15151F on the same indigo in the light one (3.04:1): the theme
 * that is *supposed* to be the default is the one that reads worse, which is why looking at
 * the site in dark mode would have found nothing.
 *
 * On `.cv-band` proper it is not marginal. The band is #14142A and the light theme's heading is
 * #15151F -- **1.00:1**, the same colour to a rounding error. Nothing on that band was rendered
 * that way only because six headings on the home page carried `style="color: #F2F2F7"`, written
 * out by hand, which is the same fix applied one element at a time and only where somebody
 * remembered.
 *
 * The bare element selectors are here so an `<h2>` with no component class is covered too --
 * three band headings on the site are written that way. */
.cv-band h1,
.cv-band h2,
.cv-band h3,
.cv-band h4,
.cv-band h5,
.cv-band h6,
.cv-band dt,
.cv-band .cv-display,
.cv-band .cv-h2 {
    color: var(--cv-band-ink);
}

.cv-band .cv-lead {
    color: var(--cv-band-muted);
}

.cv-band--brand .cv-lead {
    color: var(--cv-band-brand-muted);
}

/* The band is already a solid block of colour. It used to carry two radial washes over the
   top of that, which only softened an edge that is better left hard. */
.cv-band::after {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    pointer-events: none;
    background: var(--cv-brand-bar);
}

/* ── Disclosure ──────────────────────────────────────────────────────────────────────── */

/* `<details>` rather than a component with state, because the marketing site is static SSR —
   no circuit — and this works with the keyboard, with a screen reader and with JavaScript
   turned off, for no script at all. */
.cv-faq {
    border-top: 1px solid var(--cv-border);
}

.cv-faq__item {
    border-bottom: 1px solid var(--cv-border);
}

.cv-faq__q {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    min-height: var(--cv-tap);
    padding-block: 1rem;
    font-weight: 600;
    color: var(--cv-heading);
    cursor: pointer;
    list-style: none;
}

.cv-faq__q::-webkit-details-marker {
    display: none;
}

.cv-faq__q::after {
    content: "";
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
    background-color: currentColor;
    transition: transform var(--cv-t) var(--cv-ease);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
}

.cv-faq__item[open] .cv-faq__q::after {
    transform: rotate(180deg);
}

.cv-faq__a {
    padding-bottom: 1.25rem;
    color: var(--cv-muted);
    line-height: 1.65;
}

/* ── Long document ───────────────────────────────────────────────────────────────────── */

/* Six thousand pixels of text on a 390px screen, and until this component there was nothing in
 * the system for it. Terms, Privacy, Refund, Cookies, Data Protection and Security are all that
 * shape, and a reader inside one of them cannot answer three questions that a printed document
 * answers for free: where am I, how much is left, and how do I get back to the top without a
 * hundred flicks of the thumb. A contents list at the very top answers the first one once, and
 * then scrolls away and is no more use than the page number on a torn-off cover.
 *
 * So the contents list stays. `.cv-docnav` is a strip pinned under the site header for the
 * length of the document: collapsed it is one 44px row, which is the price; opened it is every
 * section of the document, in reading order, one tap from wherever the reader has got to — and
 * §1 is in that list, so it is also the way back to the top without a control that would have
 * needed a word nobody has translated.
 *
 * The list itself is a <details>, not a component with state, for the same reason `.cv-faq`
 * is: the marketing site renders as static SSR, there is no circuit, and an `@onclick` there
 * is markup that does nothing. The list opens and closes with the keyboard, with a screen
 * reader, and with JavaScript off.
 *
 * THE JUMP ITSELF NEEDS THE SCRIPT, and that is not this component's doing. `<base href="/">`
 * in App.razor makes a fragment-only link resolve against the site root, so `href="#s6"` on
 * /privacy is the HOME PAGE — measured on 2026-09-21, on every contents list on the site and on
 * the skip link too. `cv-ui.js` carries the repair and the full post-mortem; it also closes this
 * list once a section has been chosen, which is the part that is merely convenience.
 */
.cv-docnav {
    position: sticky;
    /* The header is 4rem of content plus whatever the notch takes. `--cv-header-h` is 3.5rem,
       so the half-rem is the difference, not a gap: this strip sits on the header's bottom
       edge rather than half a centimetre under it. */
    top: calc(var(--cv-header-h) + var(--cv-safe-t) + 0.5rem);
    z-index: var(--cv-z-sticky);
    display: block;
    margin-block: 2rem;
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-lg);
    background: var(--cv-card);
    box-shadow: var(--cv-shadow-sm);
    /* `clip` rather than `hidden`: it rounds the progress line into the top corners without
       making this a scroll container. The distinction cost `.cv-glow` an afternoon. */
    overflow: clip;
}

/* The <details> inside the strip. It is a child rather than the strip itself because
   `<summary>` has to be a <details>'s first child, and the progress line belongs above it. */
.cv-docnav__bar {
    display: block;
}

.cv-docnav__q {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    min-height: var(--cv-tap);
    padding-inline: 0.875rem;
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--cv-heading);
    cursor: pointer;
    list-style: none;
}

.cv-docnav__q::-webkit-details-marker {
    display: none;
}

.cv-docnav__q::after {
    content: "";
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
    background-color: currentColor;
    transition: transform var(--cv-t) var(--cv-ease);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
}

.cv-docnav__bar[open] .cv-docnav__q::after {
    transform: rotate(180deg);
}

/* Eleven sections is taller than a phone, and a pinned panel that runs off the bottom of the
   screen is a list with its last third amputated. It scrolls inside itself instead, and
   `overscroll-behavior` stops that scroll from continuing into the document underneath once
   the list reaches its end — which on a touch screen is how a reader loses their place while
   trying to read the thing that tells them where their place is. */
.cv-docnav__list {
    max-height: min(60vh, 22rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    margin: 0;
    padding: 0.375rem;
    border-top: 1px solid var(--cv-border);
    list-style: none;
}

.cv-docnav__link {
    display: flex;
    align-items: center;
    min-height: var(--cv-tap);
    padding-inline: 0.5rem;
    border-radius: var(--cv-r-sm);
    font-size: 0.875rem;
    line-height: 1.4;
    color: var(--cv-text);
    text-decoration: none;
}

.cv-docnav__link:hover {
    background: var(--cv-primary-soft);
    color: var(--cv-primary-soft-ink);
}

/* How much is left.
 *
 * A scroll-driven animation rather than a scroll handler: `animation-timeline: scroll()` is
 * read off the scroller on the compositor, so this costs no listener, no layout read per frame
 * and no script at all. It is not in every browser, and the whole track lives inside the
 * `@supports` for that reason — a browser without it gets the strip with no line, rather than
 * a line frozen at zero that looks exactly like a document you have not started.
 *
 * Not covered by the reduced-motion block below, and deliberately: nothing here moves on its
 * own. It is the reader's own scrolling, drawn. */
.cv-docnav__progress {
    display: none;
}

@supports (animation-timeline: scroll()) {
    .cv-docnav__progress {
        display: block;
        height: 3px;
        background: var(--cv-border);
    }

    .cv-docnav__progress > span {
        display: block;
        height: 100%;
        background: var(--cv-primary);
        transform: scaleX(0);
        transform-origin: left center;
        animation: cv-docnav-read linear both;
        animation-timeline: scroll(root block);
    }
}

@keyframes cv-docnav-read {
    from {
        transform: scaleX(0);
    }

    to {
        transform: scaleX(1);
    }
}

/* An anchor target under two pinned bars, not one. `html { scroll-padding-top }` clears the
   site header; a section jumped to from the contents list has this strip over it as well, and
   without this the heading you asked for lands behind the control you asked with. */
.cv-doc-anchor {
    scroll-margin-top: calc(var(--cv-header-h) + var(--cv-safe-t) + 4.25rem);
}

/* ── Summary figures ─────────────────────────────────────────────────────────────────── */

/* Four numbers, not four boxes.
 *
 * Every list page in the back office opens with a strip of summary figures — *Total coupons 7,
 * Active 0, Total redemptions 5, Expired 6* — written as four separate cards in a
 * `grid-cols-1 sm:grid-cols-2 lg:grid-cols-4`. Above `lg` that reads as a row. At 390px the
 * four cards stack, each one a full-width box about 200px tall holding one short label and one
 * number, so **eight hundred pixels** of the first screen and a half is four boxes containing
 * about twenty characters between them. The first row of actual data began at 1,250px.
 *
 * The figures belong together, so they are one panel with hairlines between the cells rather
 * than four panels with gutters: two across on a phone, and `--cv-stats-n` across from `sm`.
 * The same four numbers, in about 160px.
 *
 * `gap: 1px` over a border-coloured background is how the hairlines are drawn — one rule
 * instead of a border on each cell that has to know which edge it is on. */

.cv-stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1px;
    background: var(--cv-border);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-xl);
    overflow: hidden;
}

@media (min-width: 640px) {
    .cv-stats {
        grid-template-columns: repeat(var(--cv-stats-n, 4), minmax(0, 1fr));
    }
}

.cv-stat {
    min-width: 0;
    background: var(--cv-card);
    padding: 0.625rem 0.875rem;
}

.cv-stat__label {
    display: block;
    font-size: 0.75rem;
    line-height: 1.4;
    color: var(--cv-muted);
    overflow-wrap: anywhere;
}

.cv-stat__value {
    display: block;
    margin-top: 0.125rem;
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1.2;
    color: var(--cv-heading);
    overflow-wrap: anywhere;
}

/* ── Pager ───────────────────────────────────────────────────────────────────────────── */

/* A page control that does not grow with the number of pages. The portal's drafts list
   rendered a button for every page in a loop and then hid most of them with a chain of
   conditions, which meant the markup carried a hundred buttons to show seven. This shows a
   window around the current page and nothing else. */

.cv-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    flex-wrap: wrap;
}

.cv-pager__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--cv-tap);
    min-height: var(--cv-tap);
    padding: 0 0.625rem;
    border: 1px solid transparent;
    border-radius: var(--cv-r-md);
    background: transparent;
    color: var(--cv-text);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    transition: background-color var(--cv-t) var(--cv-ease), color var(--cv-t) var(--cv-ease);
    -webkit-tap-highlight-color: transparent;
}

.cv-pager__btn:hover:not(:disabled) {
    background: var(--cv-bg-subtle);
}

.cv-pager__btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.cv-pager__btn.is-current {
    background: var(--cv-primary);
    color: var(--cv-primary-ink);
}

.cv-pager__gap {
    padding-inline: 0.25rem;
    color: var(--cv-muted);
    user-select: none;
}

/* ── Media card ──────────────────────────────────────────────────────────────────────── */

/* A card whose top is an image and whose bottom is a decision. The distinction that matters is
   in the actions: the drafts list gave five icon buttons the same size, the same shape and the
   same weight — preview, edit, publish, schedule, delete — so "put this on a customer's
   Facebook page" and "destroy it" were the same object, 44 pixels apart. */

.cv-media-card {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-xl);
    box-shadow: var(--cv-shadow-sm);
    transition: box-shadow var(--cv-t) var(--cv-ease), border-color var(--cv-t) var(--cv-ease);
}

.cv-media-card:hover {
    box-shadow: var(--cv-shadow-md);
    border-color: var(--cv-primary);
}

.cv-media-card__media {
    position: relative;
    aspect-ratio: 16 / 9;
    background: var(--cv-bg-subtle);
    overflow: hidden;
}

.cv-media-card__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* No image is a state, not an absence. A flat grey block says "broken"; the brand's own tint,
   with the ribbon along the top edge, says "text only, on purpose". */
.cv-media-card__media--empty {
    display: grid;
    place-items: center;
    background: var(--cv-brand-tint);
    border-top: 3px solid transparent;
    border-image: var(--cv-brand-bar) 1;
    color: var(--cv-primary-soft-ink);
}

.cv-media-card__body {
    display: flex;
    flex-direction: column;
    flex: 1;
    gap: 0.75rem;
    padding: 1rem;
}

/* The decision row sits at the bottom of the card whatever the text length, so a grid of
   cards has its actions on one line instead of at five different heights. */
.cv-media-card__actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: auto;
    padding-top: 0.75rem;
    border-top: 1px solid var(--cv-border);
}

/* Destructive actions are separated by space rather than distinguished only by colour —
   colour alone is not a difference somebody in a hurry sees. */
.cv-media-card__actions--danger {
    margin-inline-start: auto;
}

/* Three lines and then an ellipsis. `line-clamp` needs all four declarations; without
   `overflow: hidden` it does nothing at all. */
.cv-clamp-3 {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* ── Grouped navigation ──────────────────────────────────────────────────────────────── */

/* A heading above a set of destinations.
 *
 * The portal's rail carried fourteen links in one flat list and the back office fifteen. A flat
 * list of fifteen is not a menu, it is an inventory: there is nothing to scan by, so finding
 * anything means reading every row. Four or five short groups turn that into two glances —
 * pick the group, then pick the row.
 *
 * The heading is a real `<h2>` inside its `<nav>`, not a styled div, so the groups are landmarks
 * a screen reader can jump between rather than decoration only sighted users benefit from.
 */
.cv-nav-group {
    padding-inline: 0.875rem;
    margin-block: 1rem 0.375rem;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--cv-sidebar-muted);
}

.cv-nav-group:first-child {
    margin-top: 0.25rem;
}

/* ── Menu with a trigger, for a page that has no circuit ─────────────────────────────── */

/* A dropdown in the marketing header. It is `data-cv-toggle` + a `hidden` panel rather than a
   component with state, because that site renders as static SSR — there is no circuit for a
   Blazor handler to run on. cv-ui.js does the opening, the Escape, the click-away and the
   close-on-navigate. */
.cv-dropdown {
    position: relative;
}

.cv-dropdown__panel {
    position: absolute;
    inset-inline-start: 0;
    top: calc(100% + 0.5rem);
    z-index: var(--cv-z-drawer);
    min-width: 15rem;
    padding: 0.375rem;
    background: var(--cv-card);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-r-lg);
    box-shadow: var(--cv-shadow-lg);
    animation: cv-menu-in var(--cv-t-fast) var(--cv-ease);
}

.cv-dropdown__item {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    min-height: var(--cv-tap);
    padding: 0.5rem 0.625rem;
    border-radius: var(--cv-r-sm);
    color: var(--cv-text);
    text-decoration: none;
}

.cv-dropdown__item:hover {
    background: var(--cv-bg-subtle);
}

.cv-dropdown__item strong {
    display: block;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--cv-heading);
    line-height: 1.35;
}

.cv-dropdown__item span {
    display: block;
    font-size: 0.75rem;
    line-height: 1.4;
    color: var(--cv-muted);
    margin-top: 0.0625rem;
}

.cv-dropdown__icon {
    display: grid;
    place-items: center;
    flex: none;
    width: 1.875rem;
    height: 1.875rem;
    margin-top: 0.0625rem;
    border-radius: var(--cv-r-sm);
    background: var(--cv-primary-soft);
    color: var(--cv-primary-soft-ink);
}

/* The chevron turns when the panel is open. `aria-expanded` is the state the button already
   has to carry for assistive technology, so it is what the style keys on -- no second source
   of truth to fall out of step. */
.cv-dropdown__chevron {
    transition: transform var(--cv-t) var(--cv-ease);
}

[aria-expanded="true"] .cv-dropdown__chevron {
    transform: rotate(180deg);
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   Motion
   ══════════════════════════════════════════════════════════════════════════════════════

   Two rules govern everything below.

   **Content is never hidden by default.** The reveal states only apply under `html.cv-js`, a
   class `cv-ui.js` sets on itself as its first act. If the script fails, is blocked, or has not
   run yet, every one of these elements is simply visible. The common way scroll animation goes
   wrong is a page that is blank for everyone whose JavaScript did not arrive, and that is a
   one-line mistake: putting `opacity: 0` in the stylesheet unconditionally.

   **Reduced motion is honoured by not animating at all**, not by animating faster. The global
   block near the top of this file collapses every duration to 0.01ms, and the `:where()` guard
   below means the reveal class does not even set an opacity in that case — so somebody who asks
   their system for less motion gets a static page, not a jittery one. */

/* The reveal: a short rise and fade as an element enters the viewport. 16px, not 40 — a big
   translate reads as the page assembling itself, which is a novelty the second time and an
   irritation the tenth. */
@media (prefers-reduced-motion: no-preference) {

    .cv-js .cv-reveal {
        opacity: 0;
        transform: translateY(16px);
        transition:
            opacity 620ms var(--cv-ease),
            transform 620ms var(--cv-ease);
        transition-delay: var(--cv-reveal-delay, 0ms);
        will-change: opacity, transform;
    }

    .cv-js .cv-reveal.is-in {
        opacity: 1;
        transform: none;
    }

    /* Once it has arrived, stop promising the compositor a layer it no longer needs. */
    .cv-js .cv-reveal.is-done {
        will-change: auto;
        transition: none;
    }

    /* Directional variants, which no longer move sideways. See the note below. */
    .cv-js .cv-reveal--start,
    .cv-js .cv-reveal--end {
        transform: translateY(16px);
    }

    .cv-js .cv-reveal--scale {
        transform: scale(0.97);
    }

    .cv-js .cv-reveal--start.is-in,
    .cv-js .cv-reveal--end.is-in,
    .cv-js .cv-reveal--scale.is-in {
        transform: none;
    }
}

/* The sideways reveals, which are gone at every width now.
 *
 * `--start` and `--end` existed for a two-column section: the left column entering from the
 * left, the right from the right. Below `lg` those columns are stacked, so there was no side to
 * enter from, and `translateX(24px)` on the second one made the page 24px wider than the phone
 * for as long as it took the observer to fire -- unreachable content on first paint, on the
 * marketing home page. That was fixed below `lg` and left alone above it, on the reasoning that
 * a desktop has the room.
 *
 * It does not. A transform on a grid item extends that grid's scrollable overflow whatever the
 * viewport is: `/pricing` and `/product` reported a 1152px container holding 1176px of content,
 * in nine locales and both themes, until the section scrolled into view. Nobody could see it --
 * 24px into a 1440px window with room to spare -- and it was still true, and the check was
 * right to say so.
 *
 * There is no version of "enter from the side" that does not make the element wider than its
 * container for the length of the animation. The rise says the same thing and costs nothing, so
 * both variants are now the rise. The class names stay: the markup uses them, and they still
 * mean "this is the second column", which is worth knowing when the layout changes again.
 *
 * (The block below is what is left of the old override, and is now redundant with the rule
 * above. It stays only so that a future change to the base reveal cannot resurrect the sideways
 * motion below `lg` by accident.) */
@media (prefers-reduced-motion: no-preference) and (max-width: 1023px) {

    .cv-js .cv-reveal--start,
    .cv-js .cv-reveal--end {
        transform: translateY(16px);
    }
}

/* A group whose children arrive one after another. The delay is set per child by cv-ui.js
   rather than by `:nth-child`, so a list of unknown length staggers correctly and a list of
   forty does not end up with a two-second delay on its last item — the step is capped. */
.cv-stagger>* {
    --cv-reveal-delay: 0ms;
}

/* The header shrinks slightly once the page has moved. It is a small thing and it does the
   work of telling somebody they are no longer at the top. */
.cv-app-header {
    transition: box-shadow var(--cv-t) var(--cv-ease), background-color var(--cv-t) var(--cv-ease);
}

.cv-app-header.is-scrolled {
    box-shadow: var(--cv-shadow-sm);
}

/* Anchor navigation lands below the sticky header rather than under it, and does so smoothly
   unless the visitor asked otherwise. `scroll-padding-top` is the part people forget: without
   it the heading you jumped to is hidden behind the bar you jumped past. */
html {
    scroll-padding-top: calc(var(--cv-header-h) + var(--cv-safe-t) + 1rem);
}

@media (prefers-reduced-motion: no-preference) {
    html {
        scroll-behavior: smooth;
    }
}

/* A slow drift for decorative artwork. Transform only — no `top`, no `background-position` —
   so it runs on the compositor and does not force layout on every frame. */
@media (prefers-reduced-motion: no-preference) {
    .cv-drift {
        animation: cv-drift 14s ease-in-out infinite alternate;
    }

    @keyframes cv-drift {
        from {
            transform: translate3d(0, 0, 0) rotate(0deg);
        }

        to {
            transform: translate3d(0, -14px, 0) rotate(1.5deg);
        }
    }
}

/* The press. Every interactive surface in the system already transitions its colours; this is
   the one-frame acknowledgement that a finger landed, which matters more on a touch screen
   than on a mouse because there is no hover state to have told you the target was live. */
@media (hover: none) {

    .cv-card--interactive:active,
    .cv-quick-tile:active,
    .cv-media-card:active {
        transform: scale(0.985);
        transition-duration: var(--cv-t-fast);
    }
}

/* ── Illustrated empty states ────────────────────────────────────────────────────────── */

/* An empty state with artwork instead of a glyph in a circle.
 *
 * The art is decorative — every one of these is accompanied by a heading and a sentence that
 * say the same thing in words — so it is `aria-hidden` in the markup and carries an empty alt.
 * An illustration that gets announced to a screen reader as "empty drafts illustration" is
 * noise on top of the sentence immediately below it.
 *
 * Deliberately small. These are 1254px square PNGs, and shown at 112px they read as a mark
 * rather than as a picture, which is what an empty state wants: a moment of colour, then the
 * words, then the action. */
.cv-empty__art {
    width: 7rem;
    height: 7rem;
    object-fit: contain;
    /* Not full strength. An empty state is a quiet moment in the interface and the artwork
       should not be the loudest thing on a screen that is, by definition, not busy. */
    opacity: 0.9;
}

@media (min-width: 640px) {
    .cv-empty__art {
        width: 9rem;
        height: 9rem;
    }
}

/* A page-level empty state, where the artwork can be larger because it is the whole screen. */
.cv-empty--page .cv-empty__art {
    width: 10rem;
    height: 10rem;
}

@media (min-width: 640px) {
    .cv-empty--page .cv-empty__art {
        width: 13rem;
        height: 13rem;
    }
}

/* ── Blazor's reconnect dialog ─────────────────────────────────────────────────────────────
 *
 * The framework supplies this markup and its own stylesheet, and that stylesheet is written
 * for a light page: white panel, dark text, no dark-theme rule anywhere. In the dark theme it
 * rendered white text on a white panel — measured 1.00:1, which is to say invisible.
 *
 * It only appears when the circuit drops, so no amount of clicking through the product finds
 * it; it turned up because an audit photographed a page whose socket had closed. That makes it
 * exactly the kind of screen worth getting right: the user sees it at the moment something has
 * already gone wrong, and a blank panel is the worst possible answer to "what happened?".
 */
.components-reconnect-container {
    background: var(--cv-card) !important;
    color: var(--cv-text) !important;
    border: 1px solid var(--cv-border);
    border-radius: 16px;
    box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.45);
    padding: 1.5rem 1.75rem;
}
.components-reconnect-container p,
.components-reconnect-container h5,
.components-reconnect-container div {
    color: var(--cv-text) !important;
}
.components-reconnect-container a,
.components-reconnect-container button {
    color: var(--cv-primary) !important;
}
#components-reconnect-modal::backdrop {
    background: rgb(var(--cv-bg-rgb) / 0.72);
}

/* ── Wrapping ────────────────────────────────────────────────────────────────────────── */

/* Thai is written without spaces between words.
 *
 * A browser breaks Thai using a dictionary, and where it cannot find a break it does not make
 * one — so a long cluster has a min-content width of whatever it happens to be, and a heading
 * containing one is simply wider than its column. On the ChatMai page that was 387px of h2 in
 * a 358px grid cell, sticking out of the page on every phone.
 *
 * `anywhere` rather than `break-word`, and the difference is the whole fix. Both break a word
 * only when there is no other opportunity, so Thai still breaks at its dictionary boundaries
 * wherever it can and English still breaks at spaces. They differ in one place: `break-word` is
 * ignored when the browser computes **min-content width**, and `anywhere` is not.
 *
 * That matters because the element was not overflowing its box — its box was 387px wide. A grid
 * item's `min-width: auto` resolves to its min-content width, so an unbreakable Thai cluster
 * sized the *column*, and no amount of wrapping inside it would have helped. `break-word` was
 * measured and changed nothing.
 *
 * This lives on the elements that carry prose rather than on `*`, because a table cell or a
 * code span breaking mid-token is its own defect. */
h1, h2, h3, h4, h5, h6,
p, li, dt, dd, figcaption, blockquote,
/* Inline containers carry prose too, and in Thai they carry the unbreakable kind. The
   marketing hero's illustration was 355px wide inside a 320px phone because one
   `<span>` holding a Thai label had nowhere to break, and a span was not on this list. */
span, a, strong, em, label, summary,
.cv-display,
.cv-h2,
.cv-lead,
.cv-step__title,
.cv-spec__title,
.cv-card__title {
    overflow-wrap: anywhere;
}
