/* ============================================================
   custom.css
   Global custom styling that is NOT a framework override:
   design tokens (light/dark), base elements, and the
   Bento Grid design-system classes (page header/section, stat
   tiles, surface cards, account/identity shell, wasm boot
   splash, helpers).
   Framework restyling lives in mudblazor-overrides.css /
   bootstrap-overrides.css. Tokens are defined here — reference
   var(--token) everywhere, literals live only in :root below.
   NO COMPONENT OWNS A RULE HERE: styling for one component's own
   markup belongs in its collocated ComponentName.razor.css (the
   .Client scoped bundle is linked globally in App.razor, so scoped
   rules reach the static-SSR pages too). What lives here is shared
   vocabulary any component may reach for — tokens, base elements,
   the design-system classes and the small utility helpers below —
   plus the two cases a scoped sheet cannot express: markup rendered
   by more than one component (.acc-*, .device__label, .wasm-boot*)
   and a component whose ROOT is another component, so there is no
   own element to anchor ::deep on (.field-validation-message).
   Canonical spec: /09-Design-System.md
   ============================================================ */

:root {
    /* surfaces — E-Vibe Bento (Butter & Forest), dark = deep forest-teal canvas */
    --bg: #001714;
    --surface: #002520;
    --surface-2: #00302A;
    --surface-3: #003C35;
    --surface-translucent: rgba(0, 37, 32, .72); /* floating app bar */
    /* text */
    --text-primary: #B8EDE2;
    --text-secondary: #8FA69D;
    --text-disabled: #5C726B;
    --on-accent: #FFFFFF;          /* text/icons over the gradient */
    --on-accent-dim: rgba(255, 255, 255, .74);   /* secondary text over the gradient */
    --on-accent-soft: rgba(255, 255, 255, .16);  /* chips/fills over the gradient */
    --on-accent-shadow: 0 1px 2px rgba(0, 0, 0, .35); /* white letters keep their edge over the Butter end */
    /* text/icons over SOLID Terracotta (primary buttons, active nav, active toggles). The bright dark-theme
       Terracotta holds white at only 2.95:1, so it takes forest ink (6.3:1); the deep light one keeps white. */
    --on-accent-solid: #001714;
    --nav-active-text: #FFB4A1;
    --nav-active-bg: var(--accent);   /* filled active-nav pill — bright terracotta pops on the dark canvas */
    /* lines */
    --outline: rgba(255, 255, 255, .10);
    --outline-strong: rgba(255, 255, 255, .16);
    /* skeleton loader — shimmer bars while a self-loading surface fetches (Skeleton.razor) */
    --skeleton-base: rgba(255, 255, 255, .06);
    --skeleton-shine: rgba(255, 255, 255, .13);
    /* accent — Terracotta (primary / CTA / eyebrow / active-nav / focus underline) */
    --accent: #E07A5F;
    --accent-soft: rgba(224, 122, 95, .16);
    --accent-line: rgba(224, 122, 95, .42);
    /* spotlight — Butter (rare highlight: spotlight tiles, focal metrics, "needs review") */
    --spotlight: #FFEFB3;
    --spotlight-soft: rgba(255, 239, 179, .16);
    --spotlight-line: rgba(255, 239, 179, .40);
    /* signature gradient — Forest → Terracotta → Butter (brand mark, hero text, progress) */
    --grad-from: #013E37;
    --grad-mid: #E07A5F;
    --grad-to: #FFEFB3;
    --ai-gradient: linear-gradient(135deg, var(--grad-from), var(--grad-mid), var(--grad-to));
    /* The same gradient as INK (.gradient-text). A word painted with all three stops loses its first
       letters on the dark canvas (Forest on forest-teal) and its last ones on cream (Butter on cream),
       so the text version keeps only the two stops that read on this theme's surface. */
    --text-gradient: linear-gradient(135deg, var(--grad-mid), var(--grad-to));
    /* text selection + focus ring (keyboard) */
    --selection: rgba(224, 122, 95, .34);
    --focus-ring: #FFB4A1;
    /* ambient canvas glows (warm, earthy) */
    --glow-violet: rgba(224, 122, 95, .12);
    --glow-blue: rgba(1, 62, 55, .22);
    --glow-rose: rgba(255, 239, 179, .05);
    /* status */
    --success: #7FD0A8;
    --warning: #E8C36B;
    --error: #FFB4AB;
    --error-soft: rgba(255, 180, 171, .12);
    --success-soft: rgba(127, 208, 168, .12);
    --warning-soft: rgba(232, 195, 107, .12);
    /* scrollbar */
    --scrollbar: #0C4A42;
    --scrollbar-hover: #14564D;
    /* scrim over a photographic hero (Join enrollment banner) — theme-agnostic, like --ai-gradient */
    --hero-scrim: linear-gradient(180deg, rgba(0, 0, 0, .2), rgba(0, 0, 0, .5));
    /* shape */
    --radius: 24px;
    --radius-sm: 16px;
    --radius-pill: 999px;
    --radius-input: 10px; /* rounded TOP corners of a filled form input (MudBlazor "Filled" look; flat underlined bottom) */
    /* motion — one curve and two durations for every state change (hover, press, reveal). The curve
       starts fast, so the interface answers the moment it is touched. */
    --ease-out: cubic-bezier(.23, 1, .32, 1);
    --duration-press: 140ms;
    --duration-state: 200ms;
    --duration-travel: 420ms; /* something crossing real distance on the page (a pass changing place) */
    /* filled form-input fill (the subtle shaded area, theme-agnostic refs to surface tokens) */
    --field-fill: var(--surface-2);
    --field-fill-hover: var(--surface-3);
    /* shared horizontal inset for data-grid internals (toolbar, edge cells, pager) */
    --grid-inset: 1.25rem;
    /* floating-chrome spacing (responsive — overridden on small screens) */
    --chrome-gap: 16px;
    /* side gutter of a public page's content column: never under 16px, phones included */
    --page-gutter: 1rem;
    /* floating-chrome pills (MainLayout's theme + language FABs): one size, one stacking gap,
       so the two pills can never be sized apart or overlap. */
    --fab-size: 44px;
    --fab-stack-gap: 10px;
    /* card-builder sticky preview column — its fixed width beside the form */
    --builder-preview-width: 460px;
    /* WebAssembly boot splash — wasm-boot.js updates the two --blazor-load-* vars live */
    --blazor-load-percentage: 0%;
    --blazor-load-percentage-text: "";
    --boot-bar-width: min(420px, 76vw);
    --boot-bar-height: 8px;
    --boot-gap: 1.5rem;

    /* Wallet card preview (Add-card form) — faithful Apple Wallet store card + Google Wallet
       loyalty card mocks. Theme-agnostic like --ai-gradient / --on-accent: a phone mock and the
       native wallet chrome look the same whatever the app theme, so these live in :root only.
       --pass-ink / --pass-ink-dim are set per-card inline (computed from the chosen background's
       luminance) and reference the *-light / *-dark tokens below. */
    --wallet-ink-light: #FFFFFF;
    --wallet-ink-light-dim: rgba(255, 255, 255, .72);
    --wallet-ink-dark: #1A1A1A;
    --wallet-ink-dark-dim: rgba(26, 26, 26, .56);
    /* Google pass-builder ink (FONT_COLOR_DARK / white) — the Google mock mirrors it exactly. */
    --gwallet-ink-dark: #202124;
    --gwallet-ink-light: #FFFFFF;
    --wallet-fallback-fill: rgba(127, 127, 127, .26); /* logo / banner placeholder */
    /* Card-builder WYSIWYG editing affordances + edit popovers (theme-agnostic). */
    --overlay-edit-outline: rgba(255, 255, 255, .5); /* hover outline fallback when --pass-ink is unset */
    --overlay-edit-tint: rgba(127, 127, 127, .12);   /* hover tint over an editable region */
    --shadow-popover: 0px 4px 20px rgba(0, 0, 0, .2);
    --shadow-popover-lg: 0 6px 24px rgba(0, 0, 0, .25);
    /* Apple Wallet back-of-card colours — used by the Apple preview's scoped stylesheet
       (back-of-pass links + Remove Pass). Fixed iOS system colours, theme-agnostic. */
    --pass-link: #0A84FF;             /* iOS system blue — back-of-pass links */
    --pass-danger: #FF3B30;           /* iOS system red — Remove Pass */
    --pass-danger-tint: rgba(255, 59, 48, .15); /* red hover wash on Remove Pass */

    /* legacy aliases (back-compat for any lingering refs) */
    --muted: var(--text-secondary);
    --ink: var(--text-primary);
    --hairline: var(--outline);

    /* The share/referral graph (Wallet.UI ShareGraphCanvas): the plain edge, the dot fills, and one colour
       per declared relationship (a loyalty referral's edge). Both themes define all of them. */
    --graph-edge-default: rgba(255, 255, 255, .18);
    --graph-node-default: #B8EDE2;
    --graph-node-anonymous-default: rgba(255, 255, 255, .35);
    --graph-edge-family-default: #E07A5F;
    --graph-edge-friend-default: #7FD0A8;
    --graph-edge-colleague-default: #7FB4E0;
    --graph-edge-neighbor-default: #E8C36B;
    --graph-edge-other-default: rgba(255, 255, 255, .35);
}

/* ---------- Light theme: token overrides only ----------
   :root above is the dark default. The early script in App.razor adds .theme-light /
   .theme-dark to <html> from the saved preference (resolving "system" via matchMedia)
   before first paint — so the canvas flips with no flash on every render mode. The MudBlazor
   palette flips in lock-step via the layout's IsDarkMode binding (ThemeService). The gradient,
   radii, --on-accent* (text over the gradient) and shape tokens are theme-agnostic. */
html.theme-light {
    --graph-edge-default: rgba(0, 0, 0, .15);
    --graph-node-default: #013E37;
    --graph-node-anonymous-default: rgba(0, 0, 0, .28);
    --graph-edge-family-default: #C2563C;
    --graph-edge-friend-default: #2E7D32;
    --graph-edge-colleague-default: #3B6FB6;
    --graph-edge-neighbor-default: #B58A2E;
    --graph-edge-other-default: rgba(0, 0, 0, .28);
    /* surfaces — light = pale cream canvas, white cards */
    --bg: #FBF7EC;
    --surface: #FFFFFF;
    --surface-2: #F4EFE0;
    --surface-3: #ECE6D6;
    --surface-translucent: rgba(255, 255, 255, .72);
    /* text — Forest ink on cream */
    --text-primary: #013E37;
    --text-secondary: #5B6B61;
    --text-disabled: #9AA79F;
    --on-accent-solid: #FFFFFF;
    --nav-active-text: #B5482E;
    /* deeper terracotta than --accent so the white icon/label pops on the bright cream drawer */
    --nav-active-bg: #B5482E;
    /* lines (forest-tinted hairlines) */
    --outline: rgba(1, 62, 55, .12);
    --outline-strong: rgba(1, 62, 55, .20);
    /* skeleton loader — forest-tinted shimmer on cream */
    --skeleton-base: rgba(1, 62, 55, .07);
    --skeleton-shine: rgba(1, 62, 55, .14);
    /* accent — deep Terracotta: 4.8:1 as ink on the cream canvas, 5.2:1 under white button text */
    --accent: #B44D34;
    --accent-soft: rgba(180, 77, 52, .10);
    --accent-line: rgba(180, 77, 52, .40);
    /* gradient ink on cream: Forest → Terracotta (the Butter stop would vanish) */
    --text-gradient: linear-gradient(135deg, var(--grad-from), var(--accent));
    --selection: rgba(180, 77, 52, .22);
    --focus-ring: #B44D34;
    /* spotlight — Butter highlight (deep amber ink for contrast on light) */
    --spotlight: #83690F;
    --spotlight-soft: rgba(224, 179, 65, .20);
    --spotlight-line: rgba(224, 179, 65, .45);
    /* ambient canvas glows (softer on light) */
    --glow-violet: rgba(224, 122, 95, .10);
    --glow-blue: rgba(1, 62, 55, .06);
    --glow-rose: rgba(255, 239, 179, .12);
    /* status */
    --success: #27765B;
    --warning: #8B6700;
    --error: #B3402A;
    --error-soft: rgba(179, 64, 42, .10);
    --success-soft: rgba(46, 139, 107, .10);
    --warning-soft: rgba(185, 137, 0, .12);
    /* scrollbar */
    --scrollbar: #D8D0BC;
    --scrollbar-hover: #C4BBA3;
}

html, body {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    font-family: "Google Sans Text", "Inter", system-ui, sans-serif;
    background-color: var(--bg);
    color: var(--text-primary);
    /* the browser's own surfaces carry the palette too: the caret, and native checkboxes/radios/ranges */
    caret-color: var(--accent);
    accent-color: var(--accent);
}

::selection { background: var(--selection); color: var(--text-primary); }

/* ---------- Phones ----------
   No grey flash painted over a tapped control (each control answers with its own :active state or
   ripple), no text inflation when the phone turns to landscape, no double-tap-zoom wait before a tap
   counts, and a long press on a button does not select its label. Body text stays selectable. */
html { -webkit-tap-highlight-color: transparent; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
a, button, summary, label, [role="button"] { touch-action: manipulation; }
button, [role="button"] { -webkit-user-select: none; user-select: none; }

/* ---------- Keyboard focus ----------
   One ring for everything a keyboard can reach that is not a text field (a field already shows focus
   with its accent underline). :focus-visible only, so a mouse click never draws it. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

/* ---------- Text rhythm ----------
   Headings break into lines of even length instead of leaving one word alone on the last line;
   running text avoids the same orphan. Figures that sit in columns or change in place keep one
   width per digit, so a column of numbers lines up and a counter does not jitter. */
h1, h2, h3, h4, .display { text-wrap: balance; }
p, li, .cell-sub { text-wrap: pretty; }
.mud-table-cell, .stat .value, .metric-value, .focal-metric, .tabular { font-variant-numeric: tabular-nums; }

/* ---------- Brand mark (drawer / shells) ---------- */
.drawer-brand {
    display: flex;
    align-items: center;
    gap: .7rem;
    padding: 1.4rem 1.4rem 1.1rem;
}

/* ---------- Surface card hover (interactive variant; base lives in mudblazor-overrides) ----------
   Lift only where a pointer really hovers: a touch screen fires :hover on tap and would leave the
   tile stuck in its raised state. */
@media (hover: hover) and (pointer: fine) {
    .surface-card.interactive:hover {
        transform: translateY(-2px);
        background: var(--surface-2);
        border-color: var(--outline-strong);
        box-shadow: var(--shadow-bento-hover);
    }
}

/* ---------- Public pages shared ----------
   Classes used by several public pages/components (scoped CSS can't cross components). */
/* the content column of every public page: the suite's landing, a product's page, legal, 404 */
.lp { max-width: 1140px; margin: 0 auto; padding: 0 var(--page-gutter); }
.lp-section { padding: clamp(2.5rem, 5vw, 4rem) 0; }

.lp-ghost {
    border: 1px solid var(--outline-strong);
    color: var(--text-primary);
    background: transparent;
}
/* Bootstrap paints its own hover/active colours on a .btn: the resting ones are restated for every
   state, and the hover wash is left to a real pointer (a touch screen keeps :hover after the tap). */
.lp-ghost:hover, .lp-ghost:active { border-color: var(--outline-strong); background: transparent; color: var(--text-primary); }

.lp-link-btn {
    color: var(--text-secondary);
    text-decoration: none;
}
.lp-link-btn:hover { color: var(--text-secondary); }

@media (hover: hover) and (pointer: fine) {
    .lp-ghost:hover { border-color: var(--accent-line); background: var(--surface-2); }
    .lp-link-btn:hover { color: var(--text-primary); }
}

/* ---------- Public motion layer ----------
   The one place public pages get their movement from. Every rule sits inside "no-preference" and, for
   the scroll-driven ones, inside @supports: a visitor who asked for less motion, or whose browser has
   no scroll timelines, gets the finished page with nothing moving.

   .reveal / .reveal-group — a block rises into place while it crosses the bottom edge of the screen (a
   group's children one after the other when they share a row: each next one takes a little longer). It
   only moves: its text is never faded, so what sits at the fold when the page opens reads at full
   contrast. The animation has no fill either, so a block that has not reached the screen yet is already
   in its finished place. The travel is a fixed length, not a share of the block's height, so a tall tile
   is settled well before it is read. */
@keyframes reveal-rise {
    from { transform: translateY(1.5rem); }
}

@media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: view()) {
        .reveal, .reveal-group > * {
            animation: reveal-rise linear;
            animation-timeline: view();
            animation-range: cover 0% cover calc(7rem + var(--reveal-i, 0) * 2.5rem);
        }
        /* side by side only: stacked on a phone, each child already arrives on its own */
        @media (min-width: 900px) {
            .reveal-group > :nth-child(3n + 2) { --reveal-i: 1; }
            .reveal-group > :nth-child(3n) { --reveal-i: 2; }
        }
    }

    /* A real pass keeps its place between the catalogue and its product's page (same image, same
       view-transition-name on both): the eye follows the card instead of losing it in a page swap.
       Only the pass travels — the page itself swaps at once, as it always did. */
    @view-transition { navigation: auto; }
}
::view-transition-old(root) { display: none; }
::view-transition-new(root) { animation: none; }
/* a pass the next page does not show leaves with the page, it does not linger over the new one */
::view-transition-old(*):only-child { display: none; }
::view-transition-group(*) { animation-duration: var(--duration-travel); animation-timing-function: var(--ease-out); }

/* ---------- Inline text utilities ----------
   The recurring two-line data-grid cell (bold primary + muted caption) and small
   meta text. Keeps the literals in the stylesheet — markup stays token-clean. */
.cell-title { font-weight: 600; color: var(--text-primary); }
.cell-sub { font-size: .82rem; color: var(--text-secondary); line-height: 1.4; }
.cell-strong { font-weight: 700; }
.cell-end { text-align: end; }
/* a grid's identity cell (avatar + name + caption) keeps room to read in a line or two when a row of
   action buttons claims the other end of the table; below 600px the grid stacks its cells anyway */
@media (min-width: 600px) { .cell-lead { min-width: 14rem; } }
/* small muted meta; resets the gradient text-fill so it stays visible inside .gradient-text */
.meta-xs { font-size: .7rem; color: var(--text-secondary); -webkit-text-fill-color: var(--text-secondary); }
/* a pre-reduction price, struck through next to the discounted total */
.text-strike { text-decoration: line-through; color: var(--text-secondary); }
/* read-only link / mid-width field that still collapses on phones */
.field-w-md { max-width: 26rem; width: 100%; }
/* brand-gradient avatar (user chip, menus) */
.avatar-brand { background: var(--ai-gradient); color: var(--on-accent); font-weight: 700; text-shadow: var(--on-accent-shadow); }

/* stat tiles */
/* label column + icon chip: the text takes the free width and wraps inside it, so a long label
   ("CARDS IN WALLET" at five tiles a row) never runs under the chip */
.stat__row { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; }
.stat__text { min-width: 0; }
/* The metric voice, once: a muted micro-label, an Outfit number, a quiet hint. The KPI tile (.stat) wears
   it, and so does any metric that lives inside a bigger card (.micro-label / .metric-value / .metric-hint,
   e.g. the suite Overview), so a number reads the same on every page. */
.stat .label, .micro-label {
    font-size: .68rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--text-secondary);
    line-height: 1.35;
}
/* five tiles a row: some labels take two lines, so every label keeps two lines' room and the numbers
   stay on one level across the row */
.stat.bento-5th .label { min-height: calc(2em * 1.35); }
.stat .value, .metric-value {
    font-family: "Outfit", sans-serif; font-weight: 600; line-height: 1; color: var(--text-primary);
    letter-spacing: -.02em;
}
.stat .value { font-size: clamp(1.7rem, 1.4rem + 1.4vw, 2.1rem); margin-top: .55rem; }
.stat .delta, .metric-hint { font-size: .78rem; font-weight: 500; line-height: 1.35; color: var(--text-secondary); }
.stat .delta { margin-top: .5rem; }
.stat .delta.up { color: var(--success); }
.stat .icon-chip {
    flex: none;
    width: 44px; height: 44px; border-radius: var(--radius-sm); display: grid; place-items: center;
    background: var(--accent-soft); color: var(--accent);
}

/* eyebrow / overline accent */
.eyebrow {
    font-size: .68rem; font-weight: 700; letter-spacing: .2em; text-transform: uppercase;
    color: var(--accent);
}
/* on a public page the overline is read by a visitor, often on a phone: never under 11px */
.lp .eyebrow { font-size: .7rem; }
/* Arabic micro-labels. The overline voice (tiny size, wide tracking, capitals) has no Arabic form: the
   script has no capitals, tracking pulls its joined letters apart, and at .68rem it turns to specks.
   In Arabic these labels keep their colour and weight and take a size that reads. The scoped labels
   (drawer sections, lockup tagline, user-menu role, plan-card labels) carry the same rule in their sheets. */
.eyebrow:lang(ar), .stat .label:lang(ar), .micro-label:lang(ar), .acc-eyebrow:lang(ar), .acc-divider:lang(ar),
.acc-key-label:lang(ar) {
    font-size: .8rem; letter-spacing: 0;
}
/* display face (headings) */
.display { font-family: "Outfit", "Google Sans", sans-serif; }
/* text painted with the signature gradient */
.gradient-text {
    background: var(--text-gradient); -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent;
}

/* ---------- Page header: accent eyebrow + display title + subtitle ---------- */
.page-header {
    display: flex; align-items: flex-end; justify-content: space-between;
    flex-wrap: wrap; gap: 1rem 1.5rem; margin-bottom: 1.75rem;
}
/* back link to the parent listing — sits top-start above the eyebrow/title */
.page-header__back { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
/* RTL: "back" points toward the inline start, so mirror the arrow glyph */
html[dir="rtl"] .page-header__back .mud-icon-root { transform: scaleX(-1); }
/* Any other glyph that means a direction ("open →", "back ←") points the reading way: .icon-dir on the
   MudIcon, or on a MudButton to mirror its start/end icon. */
html[dir="rtl"] .icon-dir:not(.mud-button-root),
html[dir="rtl"] .mud-button-root.icon-dir :is(.mud-button-icon-start, .mud-button-icon-end) { transform: scaleX(-1); }
.page-header__eyebrow { margin-bottom: .4rem; }
.page-header__title {
    margin: 0; font-weight: 600; font-size: clamp(1.5rem, 1.15rem + 1.7vw, 1.95rem); line-height: 1.08;
    letter-spacing: -.02em; color: var(--text-primary);
}
.page-header__subtitle {
    margin: .55rem 0 0; max-width: 58ch; color: var(--text-secondary);
    font-size: .95rem; line-height: 1.6;
}
.page-header__actions { display: flex; align-items: center; gap: .6rem; padding-bottom: .15rem; }

/* compact variant: a page nested under another page's header (the admin workspace's card pages) */
.page-header--compact { margin-bottom: 1.1rem; }
.page-header__title--compact { font-size: 1.3rem; font-weight: 600; }

/* ---------- Page section: titled surface-card ---------- */
.page-section { padding: 1.5rem 1.6rem; }
.page-section + .page-section { margin-top: 1.25rem; }
.page-section__head {
    display: flex; align-items: flex-start; gap: .85rem;
    padding-bottom: 1.05rem; margin-bottom: 1.15rem;
    border-bottom: 1px solid var(--outline);
}
.page-section__icon {
    flex: none; width: 40px; height: 40px; border-radius: var(--radius-sm);
    display: grid; place-items: center;
    background: var(--accent-soft); color: var(--accent);
}
.page-section__heading { min-width: 0; }
.page-section__title {
    margin: 0; font-weight: 600; font-size: 1.12rem; letter-spacing: -.01em; color: var(--text-primary);
}
.page-section__subtitle { margin: .25rem 0 0; font-size: .85rem; color: var(--text-secondary); line-height: 1.5; }
.page-section__actions { margin-left: auto; display: flex; gap: .5rem; align-items: center; }

/* ---------- Form group: an accent-eyebrow-labelled cluster of fields in a form/dialog ----------
   Consistent vertical rhythm between groups lives here, so a form never hand-manages top margins
   (the FormGroup component just wraps fields under a .eyebrow label). */
.form-group + .form-group { margin-top: 1.5rem; }
.form-group__label { margin-bottom: .65rem; }
.form-group__hint { margin-top: .4rem; }
/* Required marker on a group label — same asterisk a Mud input renders for Required="true", so a group
   whose field is an upload zone (no Mud label of its own) still reads as mandatory. */
.form-group__required { color: var(--mud-palette-error); margin-inline-start: .15rem; }

/* refined scrollbars */
* { scrollbar-width: thin; scrollbar-color: var(--scrollbar) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: var(--radius-pill); border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); background-clip: content-box; }

/* ============================================================
   Responsive — mobile, tablet, laptop. Fluid type via clamp()
   above; these tighten the floating chrome + spacing on small
   screens. Grids use MudBlazor xs/sm/md/lg breakpoints in markup.
   ============================================================ */

/* tablet & down: smaller floating-chrome gap */
@media (max-width: 960px) {
    :root { --chrome-gap: 12px; }
}

/* phones: drawer/app bar hug the edges a little tighter, calmer padding */
@media (max-width: 600px) {
    :root { --chrome-gap: 8px; }
    .page-section { padding: 1.15rem 1.15rem; }
    .page-header { margin-bottom: 1.25rem; }
    .page-header--compact { margin-bottom: 1rem; }
    .page-header__actions { width: 100%; }
    .stat .icon-chip { width: 40px; height: 40px; }
    .drawer-brand { padding: 1.1rem 1.1rem .9rem; }
}

/* ============================================================
   Account / MFA — server-rendered (SSR) Identity pages.
   Self-contained on the forest-teal canvas; tokens only.
   Covers the login-flow auth shell + the account-management
   surface (2FA, authenticator setup, recovery codes).
   ============================================================ */

/* ---------- centered shell for login-flow pages (2fa / recovery) ---------- */
.acc-shell { display: flex; justify-content: center; padding: 2.5rem var(--chrome-gap); }

/* ---------- surface card ---------- */
.acc-card {
    width: 100%; max-width: 460px;
    background: var(--surface);
    border: 1px solid var(--outline);
    border-radius: var(--radius);
    padding: 2rem 1.9rem;
}
/* after the base rule, so the wider variant's max-width wins */
.acc-card--wide { max-width: 540px; }
.acc-card__foot { margin-top: 1.5rem; font-size: .86rem; color: var(--text-secondary); text-align: center; }

/* ---------- brand mark ---------- */
.acc-brand { display: flex; align-items: center; gap: .7rem; margin-bottom: 1.6rem; }
.acc-brand a { display: inline-flex; align-items: center; min-height: 2.75rem; }

/* ---------- header (icon + eyebrow + title + subtitle) ---------- */
.acc-head { margin-bottom: 1.6rem; }
.acc-head__icon {
    width: 48px; height: 48px; border-radius: var(--radius-sm);
    display: grid; place-items: center; margin-bottom: 1.1rem;
    background: var(--accent-soft); color: var(--accent);
}
.acc-eyebrow {
    display: block; font-size: .7rem; font-weight: 700; letter-spacing: .2em; text-transform: uppercase;
    color: var(--accent); margin-bottom: .5rem;
}
.acc-title {
    margin: 0; font-family: "Outfit", sans-serif; font-weight: 600;
    font-size: clamp(1.35rem, 1.1rem + 1.1vw, 1.6rem); line-height: 1.12;
    letter-spacing: -.02em; color: var(--text-primary);
}
.acc-subtitle { margin: .6rem 0 0; max-width: 56ch; color: var(--text-secondary); font-size: .92rem; line-height: 1.6; }
.acc-head__meta { margin-top: 1rem; }

/* ---------- form fields ---------- */
.acc-form { display: flex; flex-direction: column; gap: 1.1rem; }
.acc-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
.acc-field { display: flex; flex-direction: column; gap: .45rem; }
.acc-field > .acc-badge, .acc-field > .acc-btn--sm { align-self: flex-start; }
.acc-label { font-size: .8rem; font-weight: 600; color: var(--text-secondary); }
/* Filled look (mirrors the MudBlazor "Filled" variant the client app uses): a subtly
   shaded box, rounded only at the top, flat underlined bottom. The underline is an inset
   box-shadow (not a border) so the 1px→2px accent transition on focus causes no reflow. */
.acc-input {
    width: 100%; box-sizing: border-box;
    background: var(--field-fill); color: var(--text-primary);
    border: none; border-radius: var(--radius-input) var(--radius-input) 0 0;
    box-shadow: inset 0 -1px 0 0 var(--outline-strong);
    /* 16px, not less: iOS Safari zooms the whole page into a smaller field on focus */
    padding: .8rem .95rem; font-size: 1rem; font-family: inherit;
    transition: background var(--duration-state) var(--ease-out), box-shadow var(--duration-state) var(--ease-out);
}
/* an example to read, so it takes the secondary ink (the disabled one sat at 2.2:1 on the field) */
.acc-input::placeholder { color: var(--text-secondary); opacity: .85; }
@media (hover: hover) and (pointer: fine) {
    .acc-input:hover { background: var(--field-fill-hover); box-shadow: inset 0 -1px 0 0 var(--text-secondary); }
}
.acc-input:focus {
    outline: none; background: var(--field-fill);
    box-shadow: inset 0 -2px 0 0 var(--accent);
}
.acc-input--code { font-size: 1.4rem; letter-spacing: .4em; text-align: center; font-weight: 600; }

/* Native date input (e.g. Birthday on the Join page). On real iOS Safari / Android Chrome the native
   type=date control renders its inner value box with ~zero height while empty, so — because .acc-input
   takes its height from padding + text line-height — the whole field collapses and shows blank on a
   phone (desktop browsers and DevTools responsive emulation use the desktop engine, so it looks fine
   there). Normalise the WebKit appearance and pin the inner value box to a text-line height (1.2em) so
   the empty date field keeps the same height and left alignment as the other filled inputs on-device. */
.acc-input[type="date"] { -webkit-appearance: none; appearance: none; }
.acc-input[type="date"]::-webkit-date-and-time-value { text-align: left; min-height: 1.2em; margin: 0; }
.acc-input[type="date"]::-webkit-datetime-edit { line-height: 1.2em; }
.acc-input[type="date"]::-webkit-calendar-picker-indicator { opacity: .55; }

/* Phone + password field styles moved to their components' collocated scoped sheets
   (PhoneNumberInput.razor.css / PasswordInput.razor.css) — the scoped bundle is linked
   globally in App.razor, so they apply on the static-SSR pages too. */

/* ---------- checkbox row ---------- */
.acc-check { display: inline-flex; align-items: center; gap: .6rem; min-height: 2.75rem; cursor: pointer; }
.acc-check input { width: 1.05rem; height: 1.05rem; accent-color: var(--accent); }
.acc-check span { font-size: .9rem; color: var(--text-secondary); }

/* ---------- buttons ---------- */
.acc-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
    min-height: 2.75rem; /* a 44px tap target whatever padding a page gives it */
    padding: .8rem 1.4rem; border-radius: var(--radius-pill);
    font-size: .92rem; font-weight: 600; font-family: inherit;
    border: 1px solid transparent; cursor: pointer; text-decoration: none;
    transition: transform var(--duration-press) var(--ease-out), filter var(--duration-state) var(--ease-out),
        background var(--duration-state) var(--ease-out), border-color var(--duration-state) var(--ease-out),
        color var(--duration-state) var(--ease-out);
}
/* the button answers the finger the moment it lands */
.acc-btn:active { transform: scale(.97); }
.acc-btn--block { width: 100%; }
.acc-btn--primary { background: var(--accent); color: var(--on-accent-solid); }
.acc-btn--ghost { background: transparent; color: var(--text-primary); border-color: var(--outline-strong); }
.acc-btn--danger { background: var(--error-soft); color: var(--error); }

/* ---------- inline links ---------- */
.acc-link { color: var(--accent); text-decoration: none; font-weight: 500; }
/* a link standing alone in an action row is a tap target, not a word in a sentence */
.acc-actions > .acc-link { display: inline-flex; align-items: center; min-height: 2.75rem; }

@media (hover: hover) and (pointer: fine) {
    .acc-btn--primary:hover { filter: brightness(1.05); }
    .acc-btn--ghost:hover { background: var(--surface-2); border-color: var(--accent-line); }
    .acc-btn--danger:hover { filter: brightness(1.08); }
    .acc-link:hover { text-decoration: underline; }
}

/* ---------- divider ---------- */
.acc-divider {
    display: flex; align-items: center; gap: 1rem; margin: 1.5rem 0;
    color: var(--text-secondary); font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
}
.acc-divider::before, .acc-divider::after { content: ""; flex: 1; height: 1px; background: var(--outline); }

/* ---------- alerts ---------- */
.acc-alert {
    display: flex; gap: .7rem; align-items: flex-start;
    border-radius: var(--radius-sm); padding: .9rem 1rem;
    font-size: .88rem; line-height: 1.55; margin-bottom: 1.25rem;
}
.acc-alert__icon { flex: none; margin-top: .05rem; }
.acc-alert > div { min-width: 0; }
.acc-alert p { margin: .35rem 0 0; }
.acc-alert strong { font-weight: 700; }
.acc-alert a { color: inherit; font-weight: 700; text-decoration: underline; }
.acc-alert--success { background: var(--success-soft); color: var(--success); }
.acc-alert--warning { background: var(--warning-soft); color: var(--warning); }
.acc-alert--danger  { background: var(--error-soft);   color: var(--error); }
.acc-alert--info    { background: var(--accent-soft);  color: var(--accent); }

/* validation text */
.acc-validation { color: var(--error); font-size: .82rem; }
.acc-validation:empty { display: none; }

/* ---------- steps (enable authenticator) ---------- */
.acc-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1.6rem; }
.acc-step { display: flex; gap: 1rem; }
.acc-step__num {
    flex: none; width: 30px; height: 30px; border-radius: var(--radius-pill);
    display: grid; place-items: center;
    background: var(--accent-soft); color: var(--accent); font-weight: 700; font-size: .85rem;
}
.acc-step__body { min-width: 0; flex: 1; }
.acc-step__body > p:first-child { margin-top: .3rem; }
.acc-step__body p { margin: 0 0 .7rem; color: var(--text-secondary); font-size: .9rem; line-height: 1.6; }

/* ---------- QR + shared secret ---------- */
.acc-qr-row { display: flex; flex-wrap: wrap; align-items: center; gap: 1.4rem; margin: .2rem 0 .4rem; }
.acc-qr { display: inline-block; background: var(--on-accent); padding: .9rem; border-radius: var(--radius-sm); line-height: 0; }
.acc-qr__img { width: 188px; height: 188px; display: block; }
.acc-key-wrap { min-width: 0; }
.acc-key-label { font-size: .74rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: .5rem; }
.acc-key {
    display: inline-block; background: var(--bg); border: 1px solid var(--outline);
    border-radius: var(--radius-pill); padding: .55rem 1rem;
    font-family: ui-monospace, "SF Mono", monospace; font-size: .98rem;
    color: var(--text-primary); letter-spacing: .12em; word-break: break-all;
}

/* ---------- recovery codes grid ---------- */
.acc-codes { display: grid; grid-template-columns: repeat(2, 1fr); gap: .6rem; margin: 1.4rem 0; }
.acc-code {
    background: var(--bg); border: 1px solid var(--outline); border-radius: var(--radius-sm);
    padding: .75rem .9rem; text-align: center;
    font-family: ui-monospace, "SF Mono", monospace; font-size: 1rem; font-weight: 600;
    letter-spacing: .06em; color: var(--text-primary);
}

/* ---------- status badge ---------- */
.acc-badge {
    display: inline-flex; align-items: center; gap: .45rem;
    border-radius: var(--radius-pill); padding: .32rem .8rem;
    font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
}
.acc-badge--on { background: var(--success-soft); color: var(--success); }
.acc-badge--off { background: var(--surface-2); color: var(--text-secondary); }
.acc-badge__dot { width: 7px; height: 7px; border-radius: var(--radius-pill); background: currentColor; }

/* ---------- action rows ---------- */
.acc-actions { display: flex; flex-wrap: wrap; gap: .65rem; }
.acc-actions--end { margin-top: 1.5rem; }
.acc-actions--split { justify-content: space-between; align-items: center; }

/* ---------- compact list (passkeys, external logins) ---------- */
.acc-list { list-style: none; margin: 0 0 1.4rem; padding: 0; display: flex; flex-direction: column; gap: .6rem; }
.acc-list__row {
    display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
    background: var(--bg); border: 1px solid var(--outline); border-radius: var(--radius-sm);
    padding: .85rem 1rem;
}
.acc-list__main { min-width: 0; flex: 1; }
.acc-list__name { font-weight: 600; color: var(--text-primary); font-size: .95rem; }
.acc-list__actions { margin-left: auto; display: flex; gap: .5rem; flex-wrap: wrap; }
.acc-btn--sm { min-height: 0; padding: .5rem .95rem; font-size: .84rem; }

/* empty / muted note */
.acc-empty { color: var(--text-secondary); font-size: .92rem; margin: .25rem 0 1.4rem; }

/* ============ Manage shell (nav rail + body) ============ */
.acc-manage { display: grid; grid-template-columns: 248px 1fr; gap: 1.5rem; align-items: start; padding: var(--chrome-gap); }
.acc-manage__head { grid-column: 1 / -1; }
.acc-manage__nav {
    background: var(--surface); border: 1px solid var(--outline); border-radius: var(--radius);
    padding: .6rem; position: sticky; top: var(--chrome-gap);
}
.acc-nav { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.acc-nav__link {
    display: block; padding: .6rem .85rem; border-radius: var(--radius-pill);
    color: var(--text-secondary); font-size: .9rem; font-weight: 500; text-decoration: none;
    transition: background .18s ease, color .18s ease;
}
.acc-nav__link:hover { background: var(--surface-2); color: var(--text-primary); }
.acc-nav__link.active { background: var(--nav-active-bg); color: var(--on-accent-solid); font-weight: 600; }
.acc-nav__link.active:hover { background: var(--nav-active-bg); color: var(--on-accent-solid); }
.acc-manage__body { min-width: 0; }

/* nav footer: role-aware portal button + sign-out */
.acc-nav__foot { margin-top: .8rem; padding: .8rem .35rem .35rem; border-top: 1px solid var(--outline); display: flex; flex-direction: column; gap: .4rem; }
.acc-nav__logout { width: 100%; }
.acc-nav__logout-btn {
    all: unset; box-sizing: border-box; cursor: pointer;
    display: flex; align-items: center; justify-content: center; gap: .5rem; width: 100%;
    padding: .65rem 1rem; border-radius: var(--radius-pill);
    font-size: .88rem; font-weight: 600; color: var(--text-secondary);
    transition: background .18s ease, color .18s ease;
}
.acc-nav__logout-btn:hover { background: var(--error-soft); color: var(--error); }

/* ---------- content panel (manage pages) ---------- */
.acc-panel {
    background: var(--surface); border: 1px solid var(--outline); border-radius: var(--radius);
    padding: 1.85rem 1.9rem;
}
.acc-panel + .acc-panel { margin-top: 1.25rem; }
.acc-panel__title { margin: 0 0 .35rem; font-family: "Outfit", sans-serif; font-weight: 600; font-size: 1.12rem; letter-spacing: -.01em; color: var(--text-primary); }
.acc-panel__sub { margin: 0 0 1.35rem; color: var(--text-secondary); font-size: .9rem; line-height: 1.6; }

@media (max-width: 960px) {
    .acc-manage { grid-template-columns: 1fr; }
    .acc-manage__nav { position: static; }
    .acc-nav { flex-direction: row; flex-wrap: wrap; }
}
@media (max-width: 600px) {
    .acc-card { padding: 1.5rem 1.25rem; }
    .acc-row { grid-template-columns: 1fr; }
    .acc-panel { padding: 1.4rem 1.25rem; }
    .acc-codes { grid-template-columns: 1fr; }
    .acc-actions .acc-btn { width: 100%; }
    .acc-qr-row { gap: 1rem; }
}

/* ---------- WebAssembly boot splash (post-login loading + role redirect) ---------- */
.wasm-boot {
    min-height: 100vh;
    min-height: 100dvh;
    width: 100%;
    display: grid;
    place-items: center;
    padding: 2rem 1.25rem 4rem;
    text-align: center;
}

.wasm-boot__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--boot-gap);
    max-width: 480px;
}

.wasm-boot__title {
    font-family: "Outfit", sans-serif;
    font-weight: 600;
    font-size: clamp(1.25rem, 4vw, 1.6rem);
    color: var(--text-primary);
    margin: 0;
}

.wasm-boot__subtitle {
    font-size: clamp(.9rem, 3vw, 1rem);
    color: var(--text-secondary);
    margin: 0;
    max-width: 36ch;
}

.wasm-boot__bar {
    position: relative;
    width: var(--boot-bar-width);
    height: var(--boot-bar-height);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    overflow: hidden;
}

/* Determinate fill driven live by --blazor-load-percentage (set in wasm-boot.js). */
.wasm-boot__bar::after {
    content: "";
    position: absolute;
    inset: 0;
    transform-origin: left center;
    transform: scaleX(var(--blazor-load-percentage, 0%));
    background: var(--ai-gradient);
    transition: transform .35s ease-out;
}

.wasm-boot__pct {
    font-variant-numeric: tabular-nums;
    font-size: .9rem;
    color: var(--text-secondary);
    min-height: 1.2em;
}

/* The number itself comes from the CSS var so it tracks the real download without round-trips. */
.wasm-boot__pct::before {
    content: var(--blazor-load-percentage-text, "");
}

/* Indeterminate shimmer shown until the first real percentage arrives. */
.wasm-boot__bar--indeterminate::after {
    width: 35%;
    inset: 0 auto 0 0;
    transform: translateX(-120%);
    animation: wasm-boot-slide 1.15s ease-in-out infinite;
}

@keyframes wasm-boot-slide {
    0%   { transform: translateX(-120%); }
    100% { transform: translateX(320%); }
}

/* ============================================================
   Wallet card preview (Add-card form) — the platform caption.
   Each card is drawn pixel-per-pixel by its own scoped stylesheet
   (InteractiveAppleWalletCard / InteractiveGoogleWalletCard) and
   the stage that stands them side by side lives in
   WalletCardPreview.razor.css. Two things stay global: .device__label
   (BOTH cards render it) and the .wallet-stage--compact modifier,
   which both card sheets key off as an ANCESTOR — scoping it to
   WalletCardPreview would make those descendant rules unreachable.
   --pass-ink/-dim are set per-card from bg luminance (CSS-var
   injection).
   ============================================================ */
/* compact stage modifier — the ancestor hook for the two card sheets' shrink rules */
.wallet-stage--compact { gap: .5rem; flex-wrap: nowrap; align-items: stretch; }

/* platform caption above each bare card */
.device__label {
    align-self: center; display: inline-flex; align-items: center; gap: .4rem;
    font-size: .64rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
    color: var(--text-secondary);
    white-space: nowrap;
}
.wallet-stage--compact .device__label { font-size: .52rem; letter-spacing: .08em; }
.device__label .mud-icon-root { font-size: 1rem; }
.wallet-stage--compact .device__label .mud-icon-root { font-size: .8rem; }

/* ============================================================
   Bento Grid — the house layout language.
   A mosaic of rounded, generously-padded tiles of varying sizes
   packed into one grid with a consistent gap. Tiles are the matte
   surface boxes (MudPaper.surface-card / Bootstrap .card / .bento-box);
   this section owns the GRID + span helpers + the floating-tile depth.
   Used by both the client app and public pages.
   ============================================================ */
:root {
    --bento-gap: 1rem;
    --radius-bento: 28px; /* chunkier corner for hero tiles */
    /* soft floating-tile depth — carries the Bento look. Subtle in dark (surface
       contrast does most of the work), gently present in light. */
    --shadow-bento: 0 1px 2px rgba(0, 0, 0, .26), 0 10px 28px rgba(0, 0, 0, .20);
    --shadow-bento-hover: 0 2px 6px rgba(0, 0, 0, .30), 0 16px 38px rgba(0, 0, 0, .26);
    /* a real wallet pass lifted off the page (PassFan): a hairline edge so a dark card still
       separates from the Forest canvas, a contact shadow, and a long soft drop below */
    --shadow-pass: 0 0 0 1px rgba(255, 255, 255, .07), 0 2px 4px rgba(0, 0, 0, .35), 0 26px 48px -18px rgba(0, 0, 0, .7);
}
html.theme-light {
    /* tinted with the palette's own Forest: a cool grey shadow reads as a stain on the warm cream canvas */
    --shadow-bento: 0 1px 2px rgba(1, 62, 55, .06), 0 10px 28px rgba(1, 62, 55, .08);
    --shadow-bento-hover: 0 2px 6px rgba(1, 62, 55, .08), 0 16px 40px rgba(1, 62, 55, .12);
    --shadow-pass: 0 0 0 1px rgba(1, 62, 55, .07), 0 2px 4px rgba(1, 62, 55, .07), 0 26px 44px -20px rgba(1, 62, 55, .24);
}
@media (max-width: 600px) {
    :root { --bento-gap: .75rem; }
}

/* The mosaic. A 12-column grid, laid on 60 tracks so a row of five tiles is expressible
   (12 has no fifth; 60 is the LCM of 12 and 5). The helpers keep their twelfths-based
   names and each spans 5 tracks per twelfth, so .bento-3 is still a quarter of the row.
   Dense auto-placement, even gaps. Tiles default to a full row on phones and take their
   span at sm/md+ via the helpers below.

   ⚠ THE TRACK COUNT IS BREAKPOINT-SCOPED, AND THAT IS LOAD-BEARING — not tidiness.
   A grid of N tracks with a column gap has an intrinsic minimum width of (N-1) × gap
   whatever its content: `minmax(0, 1fr)` lets the TRACKS collapse to zero, but the gaps
   between them never collapse. At 60 tracks that floor is 59 × 1rem = 944px (59 × .75rem
   = 708px on phones), so a 60-track grid physically cannot fit a phone or tablet — every
   tile blew out to 708px and the whole page scrolled sideways (measured: +334px at 390,
   +200px at 768). Below the desktop breakpoint the helpers are all full- or half-width
   anyway, so those widths use 1 and 2 real tracks — same layout, no floor. Keep it that
   way: raising the track count at a narrow breakpoint brings the overflow straight back. */
.bento-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-auto-rows: minmax(7rem, auto);
    grid-auto-flow: dense;
    gap: var(--bento-gap);
}
.bento-grid > * { grid-column: 1 / -1; min-width: 0; }

/* Mobile-first full-width default for the span helpers themselves, so a tile still spans
   the whole row on phones even when a layout-transparent wrapper (e.g. Skeleton's
   display:contents) sits between it and .bento-grid and defeats the `> *` rule above —
   which is also why min-width:0 is repeated here: a grid GRANDCHILD keeps min-width:auto
   and would refuse to shrink below its content. */
.bento-3, .bento-4, .bento-5th, .bento-6, .bento-8, .bento-9 { grid-column: 1 / -1; min-width: 0; }

/* Phones: KPI tiles sit two to a row. Stacked one per row, five tiles pushed the first real content
   four screens down; a metric is a number and a label and reads fine at half width. Two tracks keep the
   intrinsic-width floor at ONE gap (see the warning above), every other tile still spans the full row
   through the rules above, and an odd tile left alone on its row takes the whole row
   (.stat--row-end, set by StatTiles). The icon chip is dropped at this width: it would take a third
   of the tile and the label already says what the number is. */
@media (max-width: 599.98px) {
    .bento-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: auto; }
    .bento-grid .stat { grid-column: span 1; }
    .bento-grid .stat.stat--row-end { grid-column: 1 / -1; }
    .bento-grid .stat.mud-paper { padding: 1rem 1.1rem; }
    .bento-grid .stat .icon-chip { display: none; }
}

/* Tablet: two real tracks — a half-width tile is `span 1`, a full-width one `span 2`,
   exactly what `span 30` / `span 60` of 60 tracks used to resolve to here. */
@media (min-width: 600px) {
    .bento-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .bento-3 { grid-column: span 1; }
    .bento-4 { grid-column: span 1; }
    .bento-5th { grid-column: span 1; }
    .bento-6 { grid-column: span 1; }
    .bento-8 { grid-column: span 2; }
    .bento-9 { grid-column: span 2; }
}
/* Desktop: the full 60-track mosaic. 1024px is the narrowest width where the 944px floor
   fits the content column (measured: 976px of grid box at a 1024px viewport). */
@media (min-width: 1024px) {
    .bento-grid { grid-template-columns: repeat(60, minmax(0, 1fr)); }
    .bento-3 { grid-column: span 15; }
    .bento-4 { grid-column: span 20; }
    .bento-5th { grid-column: span 12; }
    .bento-6 { grid-column: span 30; }
    .bento-8 { grid-column: span 40; }
    .bento-9 { grid-column: span 45; }
}
/* Row span for taller hero/feature tiles. */
.bento-tall { grid-row: span 2; }

/* A standalone Bento box for non-framework markup (most boxes are MudPaper.surface-card
   or Bootstrap .card, which already get the box treatment via the override files). */
.bento-box {
    background: var(--surface);
    border: 1px solid var(--outline);
    border-radius: var(--radius);
    box-shadow: var(--shadow-bento);
    padding: 1.5rem 1.6rem;
}

/* ============================================================
   Token utility classes — replace one-off inline styles.
   Applied via a component's Class param (global, so no ::deep
   needed); never write the equivalent as an inline style.
   ============================================================ */
.ico-muted { color: var(--text-secondary); }
.ico-accent { color: var(--accent); }
.ico-success { color: var(--success); }
.ico-error { color: var(--error); }
.text-warning-token { color: var(--warning); }
.measure-46 { max-width: 46ch; }
.w-fit { width: max-content; }
.minw-9 { min-width: 9rem; }
.h-control { height: 2.5rem; }
/* card-list avatar swatch — data-driven bg + ink fed via the injected --swatch/--swatch-ink custom
   properties; the outline keeps white (= uncoloured) swatches visible on the surface */
.cell-swatch {
    background: var(--swatch, #FFFFFF);
    color: var(--swatch-ink, var(--wallet-ink-dark));
    border: 1px solid var(--outline);
}

/* ---------- Spotlight tile: a Bento box with a fixed accent glow ----------
   Draws the eye to the page's single most actionable tile (merchant "your plan",
   admin "pending applications"). The glow is anchored top-left over the surface. */
.surface-card.spotlight-tile {
    background: radial-gradient(140% 120% at 0% 0%, var(--spotlight-soft), transparent 60%), var(--surface);
    border-color: var(--spotlight-line);
}

/* ---------- Quick-action row: a clickable nav row inside a tile ----------
   The control-room shortcut pattern (admin overview). Icon chip + two-line label +
   trailing chevron; the whole row is the tap target and nudges right on hover. */
.action-list { display: flex; flex-direction: column; gap: .15rem; }
.action-row {
    display: flex; align-items: center; gap: .9rem;
    padding: .85rem .6rem; border-radius: var(--radius-sm);
    color: var(--text-primary); text-decoration: none;
    transition: background var(--duration-state) var(--ease-out);
}
.action-row:hover { background: var(--surface-2); color: var(--text-primary); }
.action-row + .action-row { border-top: 1px solid var(--outline); }
.action-row__chip {
    width: 42px; height: 42px; flex: none; border-radius: var(--radius-sm);
    display: grid; place-items: center; background: var(--accent-soft); color: var(--accent);
}
.action-row__text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: .15rem; }
.action-row__title { font-weight: 600; color: var(--text-primary); line-height: 1.25; }
.action-row__sub { font-size: .82rem; color: var(--text-secondary); line-height: 1.4; }
/* the row stays put on hover (a row that slides drags its hairline with it); only the arrow answers,
   moving toward where the link goes. RTL mirrors both the glyph and the nudge. */
.action-row__arrow { color: var(--text-secondary); flex: none; transition: transform var(--duration-state) var(--ease-out), color var(--duration-state) var(--ease-out); }
.action-row:hover .action-row__arrow { color: var(--accent); transform: translateX(3px); }
html[dir="rtl"] .action-row__arrow { transform: scaleX(-1); }
html[dir="rtl"] .action-row:hover .action-row__arrow { transform: scaleX(-1) translateX(3px); }

/* A big focal metric inside a spotlight tile (admin "pending applications"). */
.focal-metric {
    font-family: "Outfit", sans-serif; font-weight: 700; line-height: 1;
    font-size: clamp(2.6rem, 2rem + 3vw, 3.6rem); letter-spacing: -.03em; color: var(--text-primary);
}

/* ---------- Legal documents (Terms, Privacy, DPA, Cookies) ----------
   Shared reading typography for the static legal pages. The page chrome lives in LegalPageShell's scoped
   CSS; these rules style the document body (ChildContent), which scoped CSS can't reach across components. */
/* 58ch of the display "0" holds about 70 characters of running text: inside the 60–75 a line reads best at */
.legal-doc { max-width: 58ch; color: var(--text-primary); line-height: 1.7; }
.legal-doc h2 {
    font-family: "Outfit", "Google Sans", sans-serif; font-weight: 600;
    font-size: 1.15rem; color: var(--text-primary); margin: 2rem 0 .6rem;
}
.legal-doc h2:first-child { margin-top: 0; }
/* Subsection inside a numbered clause (e.g. "5.1 Changing plan during a subscription"). Lets a long
   clause be broken up without renumbering the document — the numbering is what other clauses and the
   DPA cross-reference, so h2 numbers have to stay put. */
.legal-doc h3 {
    font-family: "Outfit", "Google Sans", sans-serif; font-weight: 600;
    font-size: 1rem; color: var(--text-primary); margin: 1.4rem 0 .5rem;
}
.legal-doc p { margin: 0 0 1rem; color: var(--text-secondary); }
.legal-doc ul { margin: 0 0 1rem; padding-inline-start: 1.25rem; color: var(--text-secondary); }
.legal-doc li { margin: 0 0 .4rem; }
/* underlined at rest: inside a paragraph, colour alone does not say "link" */
.legal-doc a { color: var(--accent); text-decoration: underline; text-underline-offset: .15em; }

/* ---------- Field-level validation (FieldValidationMessage) ----------
   The error line for inputs that can't carry For= themselves (MudColorPicker, wrapper components), so a
   field's message sits under the field like every Mud input's does. MudBlazor's own helper-text classes do
   the colouring; these only give it a Mud input's spacing and hide the empty element when the field is
   valid (ValidationMessage still renders its wrapper). Global rather than scoped because the element is
   rendered by <ValidationMessage> itself — the component has no own markup to hang a ::deep on. */
.field-validation-message { margin: 3px 0 0; padding: 0 12px; line-height: 1.66; }
.field-validation-message:empty { display: none; }

/* The floating app bar overlays the top of the page, so the error summary that form-scroll.js scrolls to
   (its fallback when the invalid field sits on a tab that isn't rendered) must reserve room for it —
   without this it lands tucked under the bar and reads as if nothing happened. */
.validation-summary {
    scroll-margin-top: calc(var(--mud-appbar-height, 64px) + var(--chrome-gap) * 2);
}


/* ============================================================
   Reduced motion — someone who asked their system for less
   movement keeps every state change (colour, opacity) and every
   progress indicator, but loses the travel: no lift, no nudge.
   (Skeleton shimmer and the publish toggle carry their own rule.)
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
    .surface-card.interactive:hover,
    .action-row:hover .action-row__arrow,
    .btn:active, .acc-btn:active { transform: none; }
    html[dir="rtl"] .action-row:hover .action-row__arrow { transform: scaleX(-1); }
}
