:root {
    /* ---------- Brand palette ----------
       Derived from the MudBlazor theme in Components/Hedging/CpdTheme.cs,
       which is the single source of truth for colour (both palettes).

       The literal fallbacks are load-bearing, not belt-and-braces.
       --mud-palette-* exists only where a <MudThemeProvider> renders, and for
       a long time that was MainLayout's provider set alone (now ClientShell's)
       — AuthLayout (/Account/Login) and LandingLayout (/) didn't nest into
       it and left --mud-palette-* undefined. Every layout now renders a
       static provider (Components/Layout/StaticThemeProvider.razor), so
       the gap these fallbacks cover has narrowed to an edge rather than
       the whole page: a document whose provider did not render (a render
       that failed before reaching it). MudBlazor.min.css
       supplies no defaults of its own — it only ever consumes
       var(--mud-palette-*).

       Without the fallbacks the whole palette collapses in those gaps:
       --cpd-surface-*, --cpd-border*, --cpd-text and --cpd-focus are all
       composed from --cpd-contrast / --cpd-contrast-rgb, so an undefined channel
       triplet makes every rgba() built on it invalid. Both pages are
       signed-out, so this is the first thing a user sees.

       Keep each fallback identical to the corresponding value in
       CpdTheme.cs's PaletteDark; they are two spellings of one colour.

       --cpd-success and --cpd-error alias Mud's Success and Error (2026-10-19,
       tile styles): one green and one red per scheme, the same for a Mud
       alert or chip and for a figure's tone (.tone-good / .tone-bad), where
       there used to be an unthemed #27ae60 / #e74c3c beside Mud's olive and
       crimson. Border tokens below are likewise near-misses against Mud's divider vars
       (0.05/0.1/0.2 vs 0.06/0.08/0.12) and stay literal.

       --cpd-contrast is NOT "white" — it is "whichever colour contrasts with
       the current background" (PaletteDark.TextPrimary is white-on-black;
       PaletteLight.TextPrimary is near-black-on-ivory). Everything below
       that composes rgba(var(--cpd-contrast-rgb), alpha) reads as "dark
       overlay" in light mode and "light overlay" in dark mode, for free,
       because the channel triplet itself flips with the palette. This was
       --cpd-light / --cpd-light-rgb before light mode existed, when "light"
       and "white" were the same thing; renamed once they weren't, so the
       next reader isn't misled into thinking it's a literal colour. */
    --cpd-gold: var(--mud-palette-primary, #f6d46b);
    --cpd-gold-rgb: var(--mud-palette-primary-rgb, 246, 212, 107);
    /* PaletteDark.Background — NOT PaletteDark.Dark, which is a different colour. */
    --cpd-dark: var(--mud-palette-background, #061221);
    /* Background gets no -rgb export from Mud (confirmed against MudBlazor 9.5's
       generated :root style block — it emits -rgb triplets for Primary/Secondary/
       Tertiary/Text*/Action*/etc. but not for Background or Surface), so this
       cannot alias the same way --cpd-contrast-rgb does. It also can't be a plain
       literal across both themes: PaletteDark.Background (6,18,33) and
       PaletteLight.Background (247,243,234) are different colours, not a
       flip of the same one. Resolved via a `data-cpd-theme` attribute that
       ThemeProviderHost.razor keeps in sync with ThemeState — see the override
       a few lines down. Dark is the attribute-less default so pages that render
       no theme provider at all (a future regression of the MainLayout-only trap
       below) still get today's colour instead of an undefined one. */
    --cpd-dark-rgb: 6, 18, 33;
    --cpd-contrast: var(--mud-palette-text-primary, #ffffff);
    --cpd-contrast-rgb: var(--mud-palette-text-primary-rgb, 255, 255, 255);
    --cpd-accent: #d0d7e4;
    --cpd-error: var(--mud-palette-error, #f07a68);
    /* Component CSS composes this into rgba(). CSS cannot derive a channel
       triplet from a hex value, so it is spelled out here beside --cpd-error
       (and again in the light block) and must be kept in step with it. */
    --cpd-error-rgb: 240, 122, 104;
    --cpd-success: var(--mud-palette-success, #5cc489);
    /* Unlike --cpd-error/--cpd-success above, Warning was never given a
       deliberately-different cpStudio literal — PaletteDark.Warning IS the
       brand's amber-orange (#f0a542) already, so this aliases straight
       through like --cpd-gold does. Added because mud-overrides.css's
       .text-warning read var(--mud-palette-warning) directly with no
       fallback, the one rule in that section not to use a --cpd-* token. */
    --cpd-warning: var(--mud-palette-warning, #f0a542);
    --cpd-focus: rgba(var(--cpd-gold-rgb), 0.30);

    /* ---------- Typography ---------- */
    --cpd-font-sans: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    --cpd-font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;

    /* ---------- Surfaces (elevation on dark background) ---------- */
    --cpd-surface-0: var(--cpd-dark);
    --cpd-surface-1: rgba(var(--cpd-contrast-rgb), 0.02);
    --cpd-surface-2: rgba(var(--cpd-contrast-rgb), 0.05);
    --cpd-surface-3: rgba(var(--cpd-contrast-rgb), 0.08);
    --cpd-surface-hover: rgba(var(--cpd-contrast-rgb), 0.12);
    /* A table's own paper (PaletteDark/PaletteLight.Surface), OPAQUE: a DataTable's sticky row-actions
       column floats over the cells it covers and paints this so nothing scrolled under it shows
       through. A spelling of Mud's surface, like --cpd-dark of its background. */
    --cpd-surface-table: var(--mud-palette-surface, #0d1b2e);
    /* A hovered table row's gold wash (mud-overrides.css §4). Translucent, so a cell that must stay
       opaque (the sticky column) layers it over --cpd-surface-table rather than painting it alone. */
    --cpd-row-hover: rgba(var(--cpd-gold-rgb), 0.06);
    /* Curve Capture dialog surfaces. All three must be OPAQUE — they cannot be
       rgba(var(--cpd-contrast-rgb), …) overlays like --cpd-surface-1/2/3 above:
       --cpd-surface-dialog sits over live chart content and
       --cpd-surface-table-header is a `position: sticky` header scrolling over
       its own rows, so any alpha lets what is behind bleed through.

       They are also a genuine per-theme pair rather than a flip of one colour,
       so each gets an explicit light value below, like --cpd-dark-rgb and
       --cpd-surface-raised.

       What they actually are, against the page (#061221) — the previous comment
       described all three as progressively darker, which is not what the values
       do, and there is no "dialog shell" among them:
         -dialog       (10,21,38) LIGHTER — floating readout over the chart
                       (.curve-chart-readout; gold border + soft shadow). Despite
                       the name, no dialog shell uses this.
         -inset        ( 5,15,28) DARKER  — recessed chart well
                       (.capture-chart-box)
         -table-header (11,26,46) LIGHTER — sticky grid header band
                       (table.capture-grid th; carries --cpd-gold text) */
    --cpd-surface-dialog: #0a1526;
    --cpd-surface-inset: #050f1c;
    --cpd-surface-table-header: #0b1a2e;

    /* Opaque "floating above the page" surface, for the handful of places that
       cannot use the composed rgba() surfaces above because they need a real
       opaque colour: native <select> option popups (three files hardcoded
       #1b2631 / #0a1929 for exactly this).

       Needs both theme values spelled out for the same reason --cpd-dark-rgb
       does — it is not a flip of one colour. In dark mode it sits a step ABOVE
       --cpd-dark so a popup reads as raised off the page; in light mode the
       page is already ivory, so plain white is the raised analogue. */
    --cpd-surface-raised: #1b2631;

    /* Second stop of the shared "panel" gradient:
         linear-gradient(135deg, rgba(var(--cpd-dark-rgb), a), rgba(var(--cpd-panel-sheen-rgb), a))
       used by .history-table-container, .stoch-panel, .instrument-switch-card,
       .quote-chart, .recon-table-card and .recon-detail-card — 9 sites across
       the Market and Analytics pages.

       The first stop was tokenised when light mode went in; this one was left as
       a literal rgba(10, 25, 47), so every one of those panels rendered as a
       diagonal fade from the ivory page colour into navy in light mode. It reads
       as a thick dark frame around the content, and it is the first thing on the
       Market page.

       It is the page colour "lifted" — lighter than --cpd-dark in dark mode, so
       in light mode it lifts the other way, off ivory toward white. */
    --cpd-panel-sheen-rgb: 10, 25, 47;

    /* Keyword token for the `color-scheme` property — see the html,body rule. */
    --cpd-color-scheme: dark;

    /* Multiplier on the alpha of every muted TEXT colour built from
       --cpd-contrast-rgb. Applied to `color:` and `fill:` declarations only,
       never to background or border — those want to stay as faint as they are.

       An alpha overlay is not symmetric between the two themes. 0.4 white over
       #061221 measures 4.6:1; the same 0.4 of near-black over #f7f3ea measures
       only 2.5:1, because the light background starts much closer to the
       overlay's own luminance than the dark one does. So one alpha ladder tuned
       against dark mode silently under-delivers contrast at every rung in light
       mode — it left every 10px caption, slider tick and tile title on the
       trade-capture form washed out even after they started flipping colour
       correctly. 1.6 lifts the 0.4 rung to 0.64 (~4.6:1, matching what dark
       mode gets) and the 0.3 rung to 0.48. CSS clamps alpha at 1, so the top of
       the ladder (0.65 and up) is unaffected in both themes. */
    --cpd-text-alpha-scale: 1;

    /* ---------- Borders ---------- */
    --cpd-border-subtle: rgba(var(--cpd-contrast-rgb), 0.05);
    --cpd-border: rgba(var(--cpd-contrast-rgb), 0.1);
    --cpd-border-strong: rgba(var(--cpd-contrast-rgb), 0.2);

    /* ---------- Text levels ---------- */
    --cpd-text: var(--cpd-contrast);
    --cpd-text-muted: var(--cpd-accent);
    /* Scaled like the inline `color:` sites — this tier is only ever used as a
       text colour, so it belongs on the same ladder. */
    --cpd-text-subtle: rgba(var(--cpd-contrast-rgb), calc(0.4 * var(--cpd-text-alpha-scale)));

    /* ---------- Gold tints (consistent across components) ---------- */
    --cpd-gold-faint:  rgba(var(--cpd-gold-rgb), 0.04);
    --cpd-gold-soft:   rgba(var(--cpd-gold-rgb), 0.08);
    --cpd-gold-tint:   rgba(var(--cpd-gold-rgb), 0.15);
    --cpd-gold-strong: rgba(var(--cpd-gold-rgb), 0.25);
    --cpd-gold-border: rgba(var(--cpd-gold-rgb), 0.3);
    --cpd-gold-ring:   rgba(var(--cpd-gold-rgb), 0.1);

    /* ---------- Tile tints (docs/ux-shape.md entry 22) ----------
       One pair per TileTint family (CPStudio.Core/Page.fs; the family is the case name in kebab
       case, TileTints.Family): --cpd-tint-<family> is the open tile's background and its pane's
       header band, 8% of the edge over the surface; --cpd-tint-<family>-edge is solid, the open
       tile's and the pane's left border and the ribbon's colour. Low saturation, hues spread round
       the wheel clear of the status roles (green/olive, amber, crimson) and the gold accent:
       Details slate, Valuation blue, Risk violet, Xva rose, MarketData teal, Relations indigo,
       History grey. Body text (--cpd-text) on any tint over the page or a surface is >= 14.8:1 in
       both schemes; each edge is >= 4.3:1 against the page. Light values in the light block.
       TileTintTests fails when a family has no pair in either scheme. */
    --cpd-tint-details:          rgba(143, 163, 191, 0.08);
    --cpd-tint-details-edge:     #8fa3bf;
    --cpd-tint-valuation:        rgba(91, 155, 240, 0.08);
    --cpd-tint-valuation-edge:   #5b9bf0;
    --cpd-tint-risk:             rgba(167, 139, 250, 0.08);
    --cpd-tint-risk-edge:        #a78bfa;
    --cpd-tint-xva:              rgba(226, 127, 189, 0.08);
    --cpd-tint-xva-edge:         #e27fbd;
    --cpd-tint-market-data:      rgba(63, 191, 176, 0.08);
    --cpd-tint-market-data-edge: #3fbfb0;
    --cpd-tint-relations:        rgba(127, 138, 245, 0.08);
    --cpd-tint-relations-edge:   #7f8af5;
    --cpd-tint-history:          rgba(154, 160, 166, 0.08);
    --cpd-tint-history-edge:     #9aa0a6;

    /* ---------- The Assistant (docs/ux-shape.md entry 24) ----------
       The assistant's own colour, used for the assistant and nothing else: the pointer's ring and
       note, the panel's marker, and the "filled by the assistant" field marker. Never gold (gold is
       the primary action) and never a tile tint: a sky blue, hue 199, clear of Valuation's muted
       blue (#5b9bf0, hue 214) and MarketData's teal (#3fbfb0, hue 173) by saturation and hue.
       --cpd-assist-ink is the text on an assist fill (the note): 9.5:1 on #40c4ff here, 5.9:1
       (white on #0369a1) in light. The ring itself is 9.5:1 against the dark page and 5.4:1
       against the ivory one. --cpd-assist-panel-w is the panel's width as a third column (>= 1024)
       and as the drawer below it. Light values in the light block. */
    --cpd-assist:         #40c4ff;
    --cpd-assist-rgb:     64, 196, 255;
    --cpd-assist-ink:     #061221;
    --cpd-assist-panel-w: 360px;

    /* ---------- Chart series (docs/ux-shape.md entry 26) ----------
       The categorical slots a chart's series take, in this fixed order and never cycled: blue,
       orange, aqua. Validated as one set for colour-vision deficiency against each scheme's page
       (dataviz validator, 2026-10-19). Light orange (2.9:1) and aqua (2.5:1) are under 3:1 against
       the ivory page, so a chart using them names its series in a legend, differs them by more than
       colour (a dash, a line weight) and offers the same figures as a table. Light values in the
       light block. */
    --cpd-series-1: #3987e5;
    --cpd-series-2: #d95926;
    --cpd-series-3: #199e70;

    /* The Exposure Profile's expected initial margin, to collect and to post (docs/ux-shape.md
       entry 26, amended 2026-10-21): two slots beside the chart's EE, EPE and ENE (violet, green,
       red, XvaExposureChart.razor.css), which leave no room for the shared blue and orange — blue
       sits on violet and orange on red. Sky and yellow, validated with the chart's three as one set
       of five, all pairs (dataviz validate_palette.js, 2026-10-21):
         dark, surface #061221: normal-vision worst 17.9 (sky vs violet); CVD worst 6.5, the
           existing EPE vs ENE pair (WARN: the lines also differ by dash, legend and table); contrast
           all >= 3:1; lightness band FAIL for violet, green and red (existing) and this yellow
           (0.75), kept with the chart's pastel family it is read beside.
         light, surface #f7f3ea: every check PASS (normal-vision worst 17.2, CVD worst 8.4) except
           contrast, yellow 2.65:1 (WARN: relief by the legend, the dash and the Table view).
       Light values in the light block. */
    --cpd-series-im-collect: #0284c7;
    --cpd-series-im-post: #d1a800;

    /* ---------- Layout ----------
       The layout breakpoints this app is built around. Documentation only: a media
       query cannot read a custom property, so every media query in the repo still
       spells its own number out and this is the list to check them against.
         641px   mobile (hamburger nav, stacked .page) / desktop (flex row, fixed sidebar)
         1024px  reserved as the tablet/desktop boundary; several page-level queries
                 already sit on it (and on the older 768/992/1100 numbers they were
                 written with, which are NOT being renumbered onto this list)
         1366px  auto-compact sidebar: compact below, expanded at or above (1366 rather
                 than 1280 because a 13" panel commonly reports exactly 1280 CSS px - see
                 navMode.js). Density does NOT follow it: density is a user setting.
       The 640.98px / 1365.98px spellings elsewhere are those same boundaries
       written as `max-width` limits.

       The size classes those widths bound (docs/ux-shape.md entry 18), decided once in the
       skeleton (CPStudioBasePage, PageRenderer), never per page:
         compact   < 641       one pane, nav as a drawer, rail tiles as a strip above the body
         medium    641-1023    one pane, nav as an icon rail
         expanded  1024-1365   primary column plus the right rail
         large     >= 1366     as expanded, with the nav expanded
       Anything that sizes to its own box rather than the window uses a container query. */
    /* The one content width (entry 18), applied by CPStudioBasePage's Layout and centred with
       margin-inline: auto. Supporting pane and List pages share the first; Feed (Home) the second.
       No page writes a max-width of its own (LayoutShapeTests). */
    --cpd-content-max:      1600px;
    --cpd-content-max-feed: 1200px;
    /* Sidebar widths. The effective mode is a data-cpd-nav attribute on <html>, set by
       wwwroot/js/navMode.js before first paint; the shell geometry below and NavMenu.razor key
       their compact rules off it. */
    --cpd-sidebar-w:         250px;
    --cpd-sidebar-compact-w: 56px;

    /* ---------- Density ---------- */
    /* Density is one user setting with two values (docs/ux-shape.md entry 18): Compact, the
       default and these values, and Comfortable, html[data-cpd-density="comfortable"] (the block
       below the theme overrides). wwwroot/js/navMode.js sets the attribute before first paint from
       the stored preference cpd-density. Consumers use these tokens rather than the raw spacing
       scale so one attribute flips them all. */
    --cpd-page-pad:        var(--cpd-space-lg);
    --cpd-page-header-mb:  var(--cpd-space-xl);
    --cpd-tile-pad:        var(--cpd-space-md);
    --cpd-tile-gap:        0.75rem;
    --cpd-field-min:       180px;
    --cpd-field-max:       280px;
    --cpd-pane-rail-w:     240px;
    /* The one height of a control in a JSON editor's field (docs/ux-shape.md entry 15): text box,
       number box, select, date picker, the notional box, a checkbox's line. Fields in a row share
       their control line, so neighbours share a baseline. */
    --cpd-control-h:       1.75rem;

    /* ---------- Spacing scale ---------- */
    --cpd-space-xs:  0.25rem;
    --cpd-space-sm:  0.5rem;
    --cpd-space-md:  0.75rem;
    --cpd-space-lg:  1rem;
    --cpd-space-xl:  1.5rem;
    --cpd-space-2xl: 2rem;
    --cpd-space-3xl: 3rem;

    /* ---------- Type scale ----------
       Five steps, the ramp docs/frontend-spec.md §7.2 settled for the rewrite, so the two surfaces
       size text alike (docs/ux-shape.md §3). New CSS takes a size from here; the older literals
       migrate file by file. 0: captions, units, table headers. 1: table cells, dense controls.
       2: body, field values. 3: page and pane titles. 4: a tile's headline figure. */
    --cpd-text-0: 0.75rem;
    --cpd-text-1: 0.8125rem;
    --cpd-text-2: 0.875rem;
    --cpd-text-3: 1.125rem;
    --cpd-text-4: 1.5rem;

    /* ---------- Radii ---------- */
    --cpd-radius-sm: 4px;
    --cpd-radius-md: 6px;
    --cpd-radius-lg: 8px;
    --cpd-radius-xl: 12px;
    --cpd-radius-pill: 999px;

    /* ---------- Transitions ---------- */
    --cpd-transition-fast:  0.15s ease;
    --cpd-transition:       0.25s ease;
    --cpd-transition-slow:  0.3s ease;

    /* ---------- States ---------- */
    /* A control that cannot be used yet (docs/ux-shape.md §8): one dimming for every button, so a
       disabled primary never reads as the page's live action. */
    --cpd-disabled-opacity: 0.5;

    /* ---------- Shadows ---------- */
    --cpd-shadow-card: 0 8px 32px rgba(0, 0, 0, 0.3);
    --cpd-shadow-soft: 0 2px 8px rgba(0, 0, 0, 0.2);
}

/* Light-mode values for the two tokens that cannot express both themes in a
   single declaration — see the comments on their dark defaults above.
   `data-cpd-theme` is set on <html> by ThemeProviderHost.razor, kept in sync
   with ThemeState (both at first paint and on every toggle). An attribute
   selector beats a bare :root selector on specificity, so these win
   regardless of source order. */
:root[data-cpd-theme="light"] {
    /* PaletteLight.Background is #f7f3ea. */
    --cpd-dark-rgb: 247, 243, 234;

    /* The status pair's light fallbacks (PaletteLight.Success / Error) and the error triplet. */
    --cpd-success: var(--mud-palette-success, #2f7d4f);
    --cpd-error: var(--mud-palette-error, #b23a2a);
    --cpd-error-rgb: 178, 58, 42;

    /* See the dark default for why this needs both values written out. */
    --cpd-surface-raised: #ffffff;
    --cpd-color-scheme: light;

    /* Ivory lifted toward white, mirroring how the dark value lifts #061221.
       Deliberately subtle: the dark gradient is a barely-there sheen, not a
       visible two-tone, and it should stay that way here. */
    --cpd-panel-sheen-rgb: 255, 253, 247;

    /* Curve Capture surfaces. The dark set steps AWAY from the page in both
       directions (raised things lighter, the chart well darker); mirrored here,
       "raised" becomes lighter than ivory and "recessed" becomes darker. Note
       the dialog's own paper is PaletteLight.Surface (#ffffff), not the ivory
       page, so these are judged against white where they sit inside it.

         -dialog       white, over the #eee8dc chart well — reads as raised, and
                       it already carries a gold border and --cpd-shadow-soft.
         -inset        one warm step below the white dialog paper: a recessed
                       well. The chart's own marks stay legible on it (axis
                       labels 4.0:1, the darkest series 4.5:1).
         -table-header the ivory page colour, which is the one value that works
                       here. It has to differ from the white rows AND carry
                       --cpd-gold text, and in light mode that gold is
                       PaletteLight.Primary (#8a6a1f) — only 5.05:1 on white to
                       begin with, so every step of tint eats the margin: at
                       #ece4d4 the gold is down to 4.0:1. Ivory keeps it at
                       4.56:1 and still tints away from the rows; the band is
                       drawn by the 2px gold bottom border the rule already has,
                       which is what separates it in dark mode too. */
    --cpd-surface-dialog: #ffffff;
    --cpd-surface-inset: #eee8dc;
    --cpd-surface-table-header: #f7f3ea;
    --cpd-text-alpha-scale: 1.6;

    /* --cpd-accent feeds --cpd-text-muted, used on ~68 sites (landing copy,
       placeholders, wizard labels, chart and table annotations). Its dark
       default #d0d7e4 is a pale blue-grey that sits at roughly 1.3:1 against
       the ivory light background — effectively invisible. This is
       PaletteLight.TextSecondary.

       Overridden here rather than aliased to var(--mud-palette-text-secondary)
       at the definition, because that variable is rgba(255,255,255,0.65) in
       dark mode and would silently restyle all 68 sites in the theme that
       ships today. Keeping the dark default literal confines this fix to
       light mode. */
    --cpd-accent: rgba(13, 21, 34, 0.65);

    /* Tile tints, light (see the dark defaults). */
    --cpd-tint-details:          rgba(82, 99, 122, 0.08);
    --cpd-tint-details-edge:     #52637a;
    --cpd-tint-valuation:        rgba(47, 111, 196, 0.08);
    --cpd-tint-valuation-edge:   #2f6fc4;
    --cpd-tint-risk:             rgba(116, 81, 201, 0.08);
    --cpd-tint-risk-edge:        #7451c9;
    --cpd-tint-xva:              rgba(168, 68, 127, 0.08);
    --cpd-tint-xva-edge:         #a8447f;
    --cpd-tint-market-data:      rgba(31, 127, 117, 0.08);
    --cpd-tint-market-data-edge: #1f7f75;
    --cpd-tint-relations:        rgba(75, 85, 194, 0.08);
    --cpd-tint-relations-edge:   #4b55c2;
    --cpd-tint-history:          rgba(107, 112, 117, 0.08);
    --cpd-tint-history-edge:     #6b7075;

    /* Chart series, light (see the dark defaults). */
    --cpd-series-1: #2a78d6;
    --cpd-series-2: #eb6834;
    --cpd-series-3: #1baf7a;
    --cpd-series-im-collect: #0284c7;
    --cpd-series-im-post: #ca8a04;

    /* The Assistant, light (see the dark defaults). */
    --cpd-assist:         #0369a1;
    --cpd-assist-rgb:     3, 105, 161;
    --cpd-assist-ink:     #ffffff;
}

/* The Comfortable half of the Density tokens above (Compact is the :root default, so an absent
   attribute, or data-cpd-density="compact", reads as Compact). Only the tokens are redefined - no
   consumer needs a second rule - so a component picks up both densities by naming the token once.
   This is the ONE [data-cpd-density] selector in the app (LayoutShapeTests): a component never
   keys a rule off the attribute. Not nested in a media query: density is a user setting, and a
   size class changes layout, not density. */
:root[data-cpd-density="comfortable"] {
    --cpd-page-pad:        var(--cpd-space-2xl);
    --cpd-page-header-mb:  var(--cpd-space-3xl);
    --cpd-tile-pad:        var(--cpd-space-lg);
    --cpd-tile-gap:        1rem;
    --cpd-field-min:       220px;
    --cpd-field-max:       320px;
    --cpd-pane-rail-w:     280px;
    --cpd-control-h:       2rem;
}

/* One reduced-motion rule for the whole app: every --cpd-transition-* token goes to zero, so a
   component that names a token (rather than a literal duration) honours the preference for free.
   A new --cpd-transition-* token gets a line here in the same change. */
@media (prefers-reduced-motion: reduce) {
    :root {
        --cpd-transition-fast:  0s;
        --cpd-transition:       0s;
        --cpd-transition-slow:  0s;
    }
}

/* Two equal columns once this box is at least 900px wide, stacked below - by container query on
   the box itself, so the page knows nothing about it (docs/ux-shape.md entry 18, a two-leg editor).
   Usage: <div class="cpd-two-col"><div>…</div><div>…</div></div>. */
.cpd-two-col {
    container-type: inline-size;
    container-name: cpd-two-col;
    display: flex;
    flex-wrap: wrap;
    gap: var(--cpd-tile-gap);
}

.cpd-two-col > * {
    flex: 1 1 100%;
    min-width: 0;
}

@container cpd-two-col (min-width: 900px) {
    .cpd-two-col > * {
        flex: 1 1 0;
    }
}

/* A tile family's class (TileTints.CssClass), on a SummaryTile's and a DetailPane's root element:
   it points the two component-local properties at the family's pair, so SummaryTile.razor.css and
   DetailPane.razor.css each style a tint once, through var(--tile-tint) and var(--tile-tint-edge),
   rather than once per family. The ribbon layer reads --cpd-tint-<family>-edge from the tile. */
.tile-tint--details { --tile-tint: var(--cpd-tint-details); --tile-tint-edge: var(--cpd-tint-details-edge); }
.tile-tint--valuation { --tile-tint: var(--cpd-tint-valuation); --tile-tint-edge: var(--cpd-tint-valuation-edge); }
.tile-tint--risk { --tile-tint: var(--cpd-tint-risk); --tile-tint-edge: var(--cpd-tint-risk-edge); }
.tile-tint--xva { --tile-tint: var(--cpd-tint-xva); --tile-tint-edge: var(--cpd-tint-xva-edge); }
.tile-tint--market-data { --tile-tint: var(--cpd-tint-market-data); --tile-tint-edge: var(--cpd-tint-market-data-edge); }
.tile-tint--relations { --tile-tint: var(--cpd-tint-relations); --tile-tint-edge: var(--cpd-tint-relations-edge); }
.tile-tint--history { --tile-tint: var(--cpd-tint-history); --tile-tint-edge: var(--cpd-tint-history-edge); }

/* ---------- Global reset ----------------------------------- */
*{box-sizing:border-box;margin:0;padding:0}

/* Remove focus outline on headings set by Blazor's FocusOnNavigate */
h1:focus{outline:none}

html,
body{
    font-family: var(--cpd-font-sans);
    background: var(--cpd-surface-0);
    color: var(--cpd-text);
    /* Tells the UA which way to render the parts of native widgets that CSS
       cannot reach — <select>'s dropdown arrow and popup, date/number input
       spinners, scrollbars, autofill. Capture.razor.css pinned this to `dark`
       on two rules, which left those widget internals dark-on-ivory in light
       mode; both now read this token instead. It has to be a keyword in a
       variable because color-scheme takes no colour value to derive it from. */
    color-scheme: var(--cpd-color-scheme);
}

html, body { height: 100%; }
body { overflow-x: hidden; }

/* Every native <select>'s option list, app-wide. The browser draws the popup itself and paints it
   with the OS's own (light) background, while the option text inherits the page's text colour - so
   in dark mode every list read white on white, visible only on the highlighted row. Only colour can
   be styled there, and it must be OPAQUE (see --cpd-surface-raised). One rule covers every select. */
select option,
select optgroup {
    background-color: var(--cpd-surface-raised);
    color: var(--cpd-text);
}

/* Status colour, one home: the classes ToneMapping.ToCssClass emits for the Tone DU (good = ok,
   attention = warn, bad = bad, neutral = pending; docs/ux-shape.md section 7). Neutral inherits the
   element's own colour unless it sets --cpd-tone-neutral (a muted status line does). */
.tone-good {
    color: var(--cpd-success);
}

.tone-attention {
    color: var(--cpd-warning);
}

.tone-bad {
    color: var(--cpd-error);
}

.tone-neutral {
    color: var(--cpd-tone-neutral, inherit);
}

/* ---------- Pane charts, one home (docs/ux-shape.md entry 26) ----------
   The frame, axes and per-date hover readout every hand-drawn pane chart shares (XvaExposureChart,
   MarginProfileChart; their geometry is ChartAxes'). A chart's root carries .cpd-chart; its series'
   colours, dashes and legend stay in its own scoped stylesheet. The hit band is invisible but takes
   the pointer; the guide, dots and readout show only while their band is hovered or focused, and
   never take the pointer themselves, so the band under them keeps the hover. */
.cpd-chart {
    border: 1px solid var(--cpd-gold-border);
    border-radius: var(--cpd-radius-lg);
    background: rgba(var(--cpd-contrast-rgb), 0.03);
    padding: var(--cpd-space-sm);
    margin: var(--cpd-space-md) 0;
}

.cpd-chart svg {
    display: block;
    width: 100%;
    height: auto;
}

.cpd-chart .axis {
    stroke: rgba(var(--cpd-contrast-rgb), 0.25);
    stroke-width: 1;
}

.cpd-chart .grid {
    stroke: rgba(var(--cpd-contrast-rgb), 0.06);
    stroke-width: 1;
}

.cpd-chart .zero-line {
    stroke: rgba(var(--cpd-contrast-rgb), 0.35);
    stroke-width: 1;
}

.cpd-chart .axis-label {
    fill: rgba(var(--cpd-contrast-rgb), calc(0.5 * var(--cpd-text-alpha-scale)));
    font-size: var(--cpd-text-0);
    font-variant-numeric: tabular-nums;
}

.cpd-chart .chart-hover-band {
    fill: none;
    pointer-events: all;
    outline: none;
}

.cpd-chart .chart-hover-guide,
.cpd-chart .chart-hover-dot,
.cpd-chart .chart-readout {
    visibility: hidden;
    pointer-events: none;
}

.cpd-chart .chart-hover:hover .chart-hover-guide,
.cpd-chart .chart-hover:hover .chart-hover-dot,
.cpd-chart .chart-hover:hover .chart-readout,
.cpd-chart .chart-hover:focus-within .chart-hover-guide,
.cpd-chart .chart-hover:focus-within .chart-hover-dot,
.cpd-chart .chart-hover:focus-within .chart-readout {
    visibility: visible;
}

.cpd-chart .chart-hover-guide {
    stroke: rgba(var(--cpd-contrast-rgb), 0.35);
    stroke-width: 1;
}

/* A surface ring, so a dot reads over the line it sits on; its fill is its series'. */
.cpd-chart .chart-hover-dot {
    stroke: var(--cpd-dark);
    stroke-width: 1.5;
}

/* The readout box: a raised surface with a border, so it reads over the marks in both themes;
   figures in tabular digits so each column lines up. */
.cpd-chart .chart-readout-box {
    fill: var(--cpd-surface-raised);
    stroke: var(--cpd-border-strong);
    stroke-width: 1;
    rx: var(--cpd-radius-sm);
}

.cpd-chart .chart-readout text {
    font-size: var(--cpd-text-0);
    font-variant-numeric: tabular-nums;
}

.cpd-chart .chart-readout-date,
.cpd-chart .chart-readout-value {
    fill: var(--cpd-text);
}

.cpd-chart .chart-readout-name {
    fill: rgba(var(--cpd-contrast-rgb), calc(0.6 * var(--cpd-text-alpha-scale)));
}


/* remove default link colour / underlines everywhere */
a,a:hover,a:visited,a:focus,a:active{
    text-decoration:none;
    color:inherit;
}

/* ---------- Header / branding ------------------------------ */
.landing-header{
    position:fixed;top:0;left:0;width:100%;
    background:var(--cpd-surface-0);
    padding:1rem 0;
    z-index:100;
    border-bottom:1px solid var(--cpd-border-subtle);
}
.container{
    width:min(90%,1200px);
    margin-inline:auto;
    display:flex;
    align-items:center;
    justify-content:space-between;
}

/* brand mark */
.brand{
    font-size:1.6rem;font-weight:600;
    color:var(--cpd-contrast);
    transition:color .25s;
}
.brand:hover{color:#b6bdc9}
.brand .logo-accent{color:var(--cpd-gold)}
.brand:hover .logo-accent{color:var(--cpd-gold)}

/* primary nav */
.nav-links{display:flex;gap:3rem;list-style:none}
.nav-links a{font-weight:500;color:var(--cpd-contrast)}
.nav-links a:hover{color:var(--cpd-gold)}
@media(max-width:768px){.nav-links{gap:1.5rem}}

/* ---------- Buttons ---------------------------------------- */
.btn-link{color:var(--cpd-gold);transition:color .25s}
.btn-link:hover{color:var(--cpd-contrast)}

.btn-outline-gold,.btn-gold{
    padding:.55rem 1.35rem;
    border-radius: var(--cpd-radius-md);
    transition: background var(--cpd-transition), color var(--cpd-transition);
}

/* outline */
.btn-outline-gold{
    border:1px solid var(--cpd-gold);
    background:transparent;
    color:var(--cpd-gold);
}
.btn-outline-gold:hover{
    background:var(--cpd-gold);
    color:var(--cpd-dark);
}

a.btn-gold, .btn-gold {
    background: var(--cpd-gold) !important;
    color: var(--cpd-dark) !important;
    border: 1px solid var(--cpd-gold) !important;
    padding: .55rem 1.35rem;
    border-radius: var(--cpd-radius-md);
    transition: background var(--cpd-transition), color var(--cpd-transition);
}
a.btn-gold:hover, .btn-gold:hover {
    background: var(--cpd-dark) !important;
    color: var(--cpd-gold) !important;
}

/* focus ring */
.btn:focus,
.btn:active:focus,
.btn-link.nav-link:focus,
.form-control:focus,
.form-check-input:focus{
    box-shadow:0 0 0 .1rem var(--cpd-dark),
               0 0 0 .25rem var(--cpd-focus);
}

/* ---------- Forms ------------------------------------------ */
/* Box model only - Bootstrap isn't loaded (see mud-overrides.css's .cpd-field-label comment),
   so without this a plain <input class="form-control"> renders at the browser's native text-box
   size instead of filling its container like the surrounding .btn elements do. */
.form-control{
    display: block;
    width: 100%;
    padding: 0.375rem 0.75rem;
    font-size: 1rem;
    border-radius: var(--cpd-radius-md);
    background: var(--cpd-surface-2);
    border: 1px solid var(--cpd-border);
    color: var(--cpd-text);
}
    .form-control:focus {
        background: var(--cpd-surface-3);
        border-color: var(--cpd-gold);
        color: var(--cpd-text);
    }
.form-control::placeholder{color: var(--cpd-text-muted)}
.darker-border-checkbox.form-check-input{border-color: var(--cpd-text-muted)}

/* ---------- Validation colours ----------------------------- */
.valid.modified:not([type=checkbox]){outline:1px solid var(--cpd-success)}
.invalid{outline:1px solid var(--cpd-error)}
.validation-message{color:var(--cpd-error)}

/* ---------- Blazor error banner ---------------------------- */
.blazor-error-boundary{
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding:1rem 1rem 1rem 3.7rem;
    color: var(--cpd-text);
    border-radius: var(--cpd-radius-md);
}
.blazor-error-boundary::after{content:"An error has occurred."}

.hero{min-height:calc(100vh - 4.75rem);display:flex;align-items:center;position:relative;overflow:hidden}
.hero-inner{
    display:grid;grid-template-columns:1fr 1fr;
    align-items:center;gap:4rem;
}
@media(max-width:992px){
    .hero-inner{grid-template-columns:1fr;gap:3rem}
    .visual{justify-self:center}
}
@media(max-width:768px){
    .copy h1{font-size:2.8rem}
    .copy p {font-size:1.1rem}
}

.logo-accent { color: var(--cpd-gold); }


/* Menu Tree */
.bb-sidebar2-top-row.navbar {
    display: none !important;
}

.cp-nav-item {
     background-color: var(--cpd-surface-0) !important;
}
.nav-container {
    background: var(--cpd-surface-1);
}

.modal-content {
    background-color: var(--cpd-surface-0) !important;
    color: var(--cpd-text) !important;
    border: 1px solid var(--cpd-gold-border) !important;
    border-radius: var(--cpd-radius-lg) !important;
}

.modal-header {
    background: rgba(var(--cpd-contrast-rgb), 0.03) !important;
    border-bottom: 1px solid rgba(var(--cpd-gold-rgb), 0.2) !important;
    color: var(--cpd-text) !important;
}

.modal-header .modal-title {
    font-weight: 600;
    font-size: 1rem;
}

.modal-header .btn-close {
    filter: invert(1) !important;
}

.modal-body {
    background-color: transparent !important;
    color: var(--cpd-text) !important;
    font-size: 0.95rem;
}

.modal-footer {
    background: rgba(var(--cpd-contrast-rgb), 0.03) !important;
    border-top: 1px solid rgba(var(--cpd-gold-rgb), 0.2) !important;
}

.modal-footer .btn-secondary {
    background-color: var(--cpd-surface-3) !important;
    border: 1px solid var(--cpd-border-strong) !important;
    color: var(--cpd-text) !important;
}

.modal-footer .btn-secondary:hover {
    background-color: var(--cpd-surface-hover) !important;
}

.form-text {
    color: var(--cpd-text-muted) !important;
}

.alert-danger {
    background-color: #5a1d1d !important;
    border-color: #842029 !important;
    color: #ea868f !important;
}

.alert-info {
    background-color: var(--cpd-gold-soft) !important;
    border-color: var(--cpd-gold-border) !important;
    color: var(--cpd-gold) !important;
}

.alert-success {
    background-color: rgba(39, 174, 96, 0.1) !important;
    border-color: rgba(39, 174, 96, 0.3) !important;
    color: var(--cpd-success) !important;
}

.bb-sidebar2 .nav-link {
    display: flex !important;
    align-items: flex-start !important;
    gap: 0.4rem !important;
    padding: 0.55rem 0.5rem !important;
    border-radius: var(--cpd-radius-sm);
    transition: background var(--cpd-transition-fast), color var(--cpd-transition-fast);
}

.bb-sidebar2 .nav-link:hover {
    background: rgba(var(--cpd-gold-rgb), 0.12);
    color: var(--cpd-gold) !important;
}

.bb-sidebar2 .nav-link.active {
    background: rgba(var(--cpd-gold-rgb), 0.18) !important;
    color: var(--cpd-gold) !important;
}

.bb-sidebar2 .nav-link-icon,
.bb-sidebar2 .nav-link-arrow {
    flex: 0 0 auto !important;
    margin: 0 !important;
    padding: 0 !important;
    display: inline-flex !important;
    align-items: center !important;
    opacity: 0.85;
}

.bb-sidebar2 .nav-link-text,
.bb-sidebar2 .nav-link-text.ms-2 {
    flex: 1 1 0% !important;
    min-width: 0 !important;
    margin: 0 !important;
    padding: 0 !important;
    display: block !important;
    word-wrap: break-word !important;
    overflow-wrap: break-word !important;
    line-height: 1.4;
}

.bb-sidebar2 .nav-link-icon i,
.bb-sidebar2 .nav-link-arrow i {
    margin: 0 !important;
    padding: 0 !important;
}

.bb-sidebar2-content {
    padding: 0.35rem !important;
}

.bb-sidebar2 .nav-item {
    padding: 0.1rem 0.2rem !important;
    margin: 0 !important;
}

.bb-sidebar2 .nav-item .nav-item {
    margin-left: 1rem !important;
}

/* Shared block styling (Trade-section style cards).
   Authoritative copy lives in sandbox-page-base.css; keep this file free of
   duplicate rules. */

/* Ensure collapse animation works for ConventionDetails */
.object-child-area {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--cpd-transition-slow);
}

.object-child-area.show {
    grid-template-rows: 1fr;
}

.object-child-area-inner {
    overflow: hidden;
}

/* Convention custom schedule */
.convention-details-container {
    padding-top: 0.35rem;
}

.convention-display-grid {
    display: grid;
    gap: 0.85rem 1rem;
}

.convention-field {
    padding-bottom: 0.15rem;
}

.convention-schedule {
    grid-column: 1 / -1;
    padding-top: 0.25rem;
}

.convention-schedule-grid {
    display: grid;
    grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) auto;
    gap: var(--cpd-space-sm);
    align-items: center;
}

.convention-schedule-header {
    color: var(--cpd-text);
    font-weight: 600;
    font-size: 0.85rem;
}

.convention-schedule-actions {
    margin-top: var(--cpd-space-sm);
    display: flex;
    flex-wrap: wrap;
    gap: var(--cpd-space-sm);
}

.convention-schedule-import {
    margin-top: var(--cpd-space-md);
}

.convention-schedule-paste {
    width: 100%;
    min-height: 8rem;
    margin-top: 0.35rem;
    font-family: var(--cpd-font-mono);
}

.convention-schedule-help {
    margin-top: 0.4rem;
    color: var(--cpd-text);
    font-size: 0.8rem;
    opacity: 0.85;
}

.convention-schedule-error {
    margin-top: var(--cpd-space-sm);
    color: #f28b82;
    font-size: 0.85rem;
}

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0; left: 0; width: 100%; z-index: 1000;
    background: #b32121;
    color: var(--cpd-text);
    padding: .6rem 1.25rem .7rem 3.7rem;
}
#blazor-error-ui .dismiss {
    position: absolute; right: .75rem; top: .5rem; cursor: pointer;
}
#blazor-error-ui .blazor-error-details {
    margin-top: .35rem;
}
#blazor-error-ui .blazor-error-details summary {
    cursor: pointer;
    text-decoration: underline;
}
#blazor-error-ui .blazor-error-details .token-banner-row {
    margin: .35rem 0;
}
#blazor-error-ui #blazor-error-detail {
    display: block;
    width: 100%;
    height: 14rem;
    resize: vertical;
    background: rgba(0, 0, 0, 0.35);
    color: var(--cpd-text);
    border: 1px solid rgba(var(--cpd-contrast-rgb), 0.35);
    border-radius: 4px;
    padding: .5rem;
    font-family: var(--cpd-font-mono);
    font-size: .78rem;
    white-space: pre;
}

/* Shared "requirement row" grammar for the Market/Editors' `Inline` markup (StochasticEditor,
   CreditEditor, ...) — used from both MarketDataRequirements (trade capture's Market Data pane)
   and MarketView, so it lives in the global stylesheet rather than a razor.css scoped to either
   host. `market-row-content` grows/shrinks with whatever each editor's own link/label/input looks
   like; `market-row-actions` clusters the row's model-options toggle and Set/Save button flush
   right. Because every row's actions cluster is pinned to the same right edge regardless of how
   wide its content is, each row's Set button lands in the same visual column as every other row's —
   this is what "Set" not lining up across rows (the Stochastic row in particular, which used to be
   a bare left-hugging flex row in a different control order) was missing. */
.market-row-inline {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    width: 100%;
}

.market-row-content {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
}

.market-row-actions {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

/* StochasticEditor's Inline compact field (a JsonField with `is-text`, wrapping a plain <input>) sits
   at the bottom of a flex-shrink chain — market-row-content (flex:1 1 auto, min-width:0) >
   .json-field.is-text (a greedy field, min-width: min(100%, its --ch)) > .repr-overlay-wrapper
   (min-width:0) — whose floor is the field's declared width, not its content's, so it shrinks to
   whatever the grid's Value column happens to offer rather than to its own content's size. Measured
   live against the HW1F default (`hw1f 0.05 0.01 act/365`, the exact string the xVA demo README has
   the presenter read out loud): clientWidth 150px vs a needed scrollWidth of 185px — 35px of
   clipped text. Floor it at 185px + ~10px of cross-browser/rounding slack so that default never
   clips; this also widens the shared Value column enough for every other row in the grid (same
   trade-off CreditEditor made for its numeric fields below). HW2F's compact grammar (`hw2f <a1>
   <a2> <sigma1> <sigma2> <rho> <daycount>`) is meaningfully longer than HW1F's and will still clip
   here — that's out of scope for this fix (HW2F has no seeded compact default to demo), but
   revisit this floor if that changes. */
.stochastic-compact-field .json-field.is-text {
    min-width: 195px;
}

/* ---------- Top-bar selects (ReportingCurrencyBar, EnvironmentBar) ----------
   One look for every picker in the shell, so the currency and the environment read as one row of
   controls. */
/* The native <select> is styled to read as MudBlazor's dense outlined field, so the top bar looks
   like one row of controls rather than one Mud control beside a browser default. Why it is native
   at all is in ReportingCurrencyBar.razor's comment: a popover-backed control in the shell is invisible on every
   WebAssembly page.

   The option list itself is drawn by the browser and cannot be styled beyond colour, which is why
   --cpd-surface-raised exists (app.css names the three other files that hardcoded a raised surface
   for exactly this): an option popup needs an OPAQUE background, and the composed
   rgba(var(--cpd-contrast-rgb), …) surfaces would let the page show through. */
.topbar-select {
    position: relative;
    display: flex;
    align-items: center;
}

/* Sits on the border like Mud's floating label, with a slab of the top bar's own background behind
   it so the border appears to break around the text. --cpd-surface-0 rather than transparent
   because the border runs underneath.

   Raised above the field: MudBlazor's reset makes every <button> position: relative, so the
   workspace picker's button (EnvironmentBar) is positioned like the label and, coming after it,
   painted over it - its border ran through "Workspace" where the currency's native <select>, not
   positioned, left a gap (user test 2026-10-20: "the workspace is sitting on the line"). */
.topbar-select-label {
    position: absolute;
    z-index: 1;
    top: -7px;
    left: 10px;
    padding: 0 4px;
    background: var(--cpd-surface-0);
    font-size: 11px;
    line-height: 1;
    color: var(--cpd-text-muted);
    pointer-events: none;

    /* Absolutely placed inside a narrow box, so it would otherwise shrink to the box and wrap onto
       the value ("Reporting currency" did, at 110px). */
    white-space: nowrap;
}

.topbar-select-field {
    width: 100%;
    height: 40px;
    padding: 0 28px 0 12px;
    border: 1px solid var(--cpd-border-strong);
    border-radius: var(--cpd-radius-lg);
    background-color: transparent;
    color: var(--cpd-text);
    font-family: inherit;
    font-size: 14px;
    cursor: pointer;

    /* The chevron, drawn rather than imported: an inline data: URI keeps it a background image with
       no extra request and no icon font, and currentColor is not available inside one, so the stroke
       is the muted text colour spelled out. */
    appearance: none;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23d0d7e4' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 6px center;
    background-size: 16px 16px;
    transition: border-color 0.15s;
}

.topbar-select-field:hover {
    border-color: var(--cpd-text);
}

.topbar-select-field:focus-visible {
    outline: none;
    border-color: var(--cpd-gold);
    box-shadow: 0 0 0 3px var(--cpd-gold-ring);
}

/* Working anywhere but production is the state a user must not miss, so the environment picker
   carries the warning role while it names another environment - colour plus the word itself, never
   colour alone. */
.topbar-select-field.topbar-select-field--elsewhere {
    border-color: var(--cpd-warning);
    color: var(--cpd-warning);
    font-weight: 600;
}

/* ---------- Page pending (MainLayout) ----------
   The page region's placeholder while a page has not drawn an element of its own yet. Hidden as soon
   as the region has any element besides the placeholder itself. Fades in after 400ms so a page that
   renders quickly never flashes it. */
.cpd-shell-content > .cpd-page-pending {
    display: flex;
    align-items: center;
    gap: var(--cpd-space-sm);
    padding: var(--cpd-page-pad);
    color: rgba(var(--cpd-contrast-rgb), 0.65);
    opacity: 0;
    animation: cpd-page-pending-in 200ms ease-out 400ms forwards;
}

.cpd-shell-content:has(> :not(.cpd-page-pending)) > .cpd-page-pending {
    display: none;
}

.cpd-page-pending-bar {
    width: 2.5rem;
    height: 3px;
    border-radius: var(--cpd-radius-sm);
    background: linear-gradient(90deg, transparent, var(--cpd-gold), transparent);
    background-size: 200% 100%;
    animation: cpd-page-pending-sweep 1.2s linear infinite;
}

@keyframes cpd-page-pending-in {
    to { opacity: 1; }
}

@keyframes cpd-page-pending-sweep {
    from { background-position: 100% 0; }
    to { background-position: -100% 0; }
}

/* ---------- Shell geometry (MainLayout, App.razor's boot skeleton) ----------
   The one home of the signed-in shell's layout: the .cpd-shell row (the rail, then the main column),
   the 3.5rem top bar and its grid, the content area and its gutters. MainLayout lays the browser's
   shell out in it, and App.razor's static boot skeleton carries the same classes, so nothing moves
   when the real shell replaces the skeleton. Global rather than MainLayout.razor.css because the
   skeleton is server markup that a scoped rule cannot reach, and because the bar's children are
   other components' elements (ClaimsComponent's .claims-line, TopBarHost's two groups). */
.cpd-shell { display: flex; flex-direction: column; min-height: 100vh; }
.cpd-shell-main { flex: 1; }

.cpd-shell-rail {
    width: var(--cpd-sidebar-w);
    background: var(--cpd-surface-0);
}
@media (max-width: 640.98px) {
    .cpd-shell-rail { width: 100%; height: auto; }
}

/* The top bar: three columns, so the session group (environment, date, currency, Run) is centred on
   the bar, not merely pushed right of the name (docs/ux-shape.md §5; the 2026-10-09 user test found
   the date, currency and Run "far away" at the top right). The name keeps the left, Display and the
   bell the right.

   The children are ClaimsComponent's .claims-line and TopBarHost's two groups (the skeleton's blocks
   carry the same two group classes). TopBarHost renders into this header with no element of its own
   around them, so .topbar-session and .topbar-actions are grid items here - which is why the grid
   lives on the bar and not inside TopBarHost. What is inside each group is TopBarHost.razor.css's.

   The side columns are 1fr each, so the middle is truly centred whenever there is room. Their floors
   differ on purpose: the name ellipsises down to nothing (minmax(0, …)), but the actions never shrink
   below their own width (minmax(max-content, …)) - when the bar is cramped the session group gives up
   exact centring before Display or the bell is pushed off the bar.

   The bar is also the "topbar" container: TopBarHost.razor.css collapses the session group to icons
   by the bar's own width, which a window width cannot tell (the sidebar's width is the user's
   choice). */
.cpd-shell-topbar {
    container: topbar / inline-size;
    height: 3.5rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(max-content, 1fr);
    align-items: center;
    column-gap: var(--cpd-space-md);
    padding: 0 var(--cpd-space-xl);
    background: var(--cpd-surface-0);
    border-bottom: 1px solid rgba(var(--cpd-contrast-rgb), 0.08);
}

.cpd-shell-topbar .claims-line {
    grid-column: 1;
    justify-self: start;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
    color: var(--cpd-text);
}

.cpd-shell-topbar .topbar-session {
    grid-column: 2;
}

.cpd-shell-topbar .topbar-actions {
    grid-column: 3;
    justify-self: end;
}

/* Phones: no room for three columns in one row. The name and the actions share the first row, and
   the session group takes a row of its own under them (it wraps there - TopBarHost.razor.css). */
@media (max-width: 640.98px) {
    .cpd-shell-topbar {
        height: auto;
        grid-template-columns: minmax(0, 1fr) max-content;
        row-gap: var(--cpd-space-lg);
        padding: var(--cpd-space-sm) var(--cpd-space-lg);
    }

    .cpd-shell-topbar .topbar-actions {
        grid-column: 2;
        grid-row: 1;
    }

    .cpd-shell-topbar .topbar-session {
        grid-column: 1 / -1;
        grid-row: 2;
        justify-self: center;
    }
}

@media (min-width: 641px) {
    .cpd-shell {
        flex-direction: row;
        height: 100vh;
        overflow: hidden;
    }
    .cpd-shell-rail {
        width: var(--cpd-sidebar-w);
        flex-shrink: 0;
        height: 100vh;
        overflow: hidden;
    }

    /* Compact rail (html[data-cpd-nav="compact"], see navMode.js). The rail stays at the compact
       width in the flex row; when it expands its children widen to --cpd-sidebar-w and overflow OVER
       the page rather than pushing it, so the content never shifts.

       Expanded means hovered, OR focus somewhere inside it - see the matching rules in NavMenu.razor's
       style block. :focus-within is what lets a keyboard user open the rail at all.

       z-index 1050 puts the overlay above the page's own stacking (the trade topbar at 5, sticky
       table headers at 10) while staying below MudBlazor's chrome - drawer/appbar at 1100 and
       popovers at 1200 - so an open dialog or popover still paints over it. */
    html[data-cpd-nav="compact"] .cpd-shell-rail {
        width: var(--cpd-sidebar-compact-w);
        position: relative;
        z-index: 1050;
    }
    html[data-cpd-nav="compact"] .cpd-shell-rail:is(:hover, :focus-within) {
        overflow: visible;
    }
    .cpd-shell-main {
        flex: 1;
        min-width: 0;
        display: flex;
        flex-direction: column;
        min-height: 0;
        height: 100vh;
    }
    .cpd-shell-topbar { flex-shrink: 0; z-index: 1; }
    .cpd-shell-content { flex: 1; min-width: 0; overflow: auto; }

    /* The footer (SiteFooter) keeps its own height instead of being squeezed out by the content's
       flex: 1 in this 100vh column. */
    .cpd-shell-main .site-footer { flex-shrink: 0; }
    .cpd-shell-topbar,
    .cpd-shell-content { padding-left: var(--cpd-space-2xl) !important; padding-right: var(--cpd-space-xl) !important; }
}

/* ---------- Boot skeleton (App.razor) ----------
   The first paint of a signed-in page. The router does not prerender (docs/ux-shape.md entry 14), so
   until the WebAssembly runtime has started and MainLayout has drawn, App.razor's static skeleton
   stands in for the shell: the rail, the top bar and the download progress, laid out by the shell
   geometry above. Gone the moment MainLayout's .cpd-app root exists. */
body:has(.cpd-app) .cpd-boot-skeleton {
    display: none;
}

/* The download progress: a determinate bar and a percentage, both read from the two custom
   properties blazor.web.js writes on <html> during the resource download (its
   onDownloadResourceProgress: --blazor-load-percentage, e.g. 42.5%, and
   --blazor-load-percentage-text, e.g. "42%" as a CSS string). No script of ours is involved. Fades in
   after 400ms, like the page placeholder, so a boot from the cache never flashes it.

   Once the download reaches 100% the runtime is still starting, which can take a second or more;
   the label then crossfades to "Starting…". CSS cannot compare a value, so the two labels share
   one grid cell and each takes its opacity from the percentage itself: clamp() turns
   "below 100%" into 0 or 1 without a script. Opacity takes a percentage, so the arithmetic needs no
   unit division.

   A boot that fails shows #blazor-error-ui (blazor.web.js sets its style to display:block); the
   status then gets out of the way and the skeleton stops pulsing, so the banner is the one thing on
   screen that moves rather than an endless "Starting…". */
.cpd-boot-status {
    display: flex;
    align-items: center;
    gap: var(--cpd-space-sm);
    padding: var(--cpd-page-pad);
    color: rgba(var(--cpd-contrast-rgb), 0.65);
    opacity: 0;
    animation: cpd-page-pending-in 200ms ease-out 400ms forwards;
}

.cpd-boot-bar {
    display: block;
    inline-size: min(12rem, 40vw);
    block-size: 3px;
    border-radius: var(--cpd-radius-sm);
    background: rgba(var(--cpd-contrast-rgb), 0.12);
    overflow: hidden;
}

.cpd-boot-bar-fill {
    display: block;
    block-size: 100%;
    inline-size: var(--blazor-load-percentage, 0%);
    background: var(--cpd-gold);
    transition: inline-size var(--cpd-transition-fast);
}

.cpd-boot-text {
    display: grid;
}

.cpd-boot-text > * {
    grid-area: 1 / 1;
}

.cpd-boot-label {
    opacity: clamp(0%, (100% - var(--blazor-load-percentage, 0%)) * 1000, 100%);
}

.cpd-boot-percent {
    margin-inline-start: var(--cpd-space-xs);
    font-variant-numeric: tabular-nums;
}

.cpd-boot-percent::before {
    content: var(--blazor-load-percentage-text, "");
}

.cpd-boot-starting {
    opacity: clamp(0%, (var(--blazor-load-percentage, 0%) - 99.9%) * 1000, 100%);
}

.cpd-boot-starting::before {
    content: "Starting…";
}

body:has(#blazor-error-ui[style*="block"]) .cpd-boot-status {
    display: none;
}

.cpd-skeleton-block {
    display: block;
    border-radius: var(--cpd-radius-sm);
    background: rgba(var(--cpd-contrast-rgb), 0.08);
    animation: cpd-skeleton-pulse 1.6s ease-in-out infinite;
}

/* NavMenu's .brand-bar and .navbar-brand, spelled again: the wordmark is static and known, so it is
   real text from the first paint rather than a block. */
.cpd-skeleton-brand {
    display: flex;
    align-items: center;
    block-size: 3.5rem;
    padding: 0 var(--cpd-space-lg);
    border-bottom: 1px solid var(--cpd-border-subtle);
    font-size: 1.25rem;
    font-weight: 600;
    white-space: nowrap;
    color: var(--cpd-text);
}

.cpd-skeleton-rows {
    display: flex;
    flex-direction: column;
    gap: var(--cpd-space-sm);
    padding: var(--cpd-space-sm) 0;
    background: var(--cpd-dark);
}

/* One dense MudNavLink: 4px/16px padding, a 24px icon, the label 12px after it. */
.cpd-skeleton-row {
    display: flex;
    align-items: center;
    gap: var(--cpd-space-md);
    padding: var(--cpd-space-xs) var(--cpd-space-lg);
}

.cpd-skeleton-icon {
    flex: 0 0 auto;
    inline-size: 1.5rem;
    block-size: 1.5rem;
}

.cpd-skeleton-text {
    block-size: var(--cpd-space-md);
    inline-size: 60%;
}

.cpd-skeleton-row:nth-child(3n + 2) .cpd-skeleton-text { inline-size: 75%; }
.cpd-skeleton-row:nth-child(3n) .cpd-skeleton-text { inline-size: 45%; }

/* The bar's controls in TopBarHost's order, in two halves that stand in for its two groups and carry
   their classes, so the shell geometry places them where .topbar-session (centred) and
   .topbar-actions (right) will be. The
   widths are the ones TopBarHost.razor.css gives the date and currency pickers; Display and the bell
   are icon buttons. */
.cpd-skeleton-session,
.cpd-skeleton-actions {
    display: flex;
    align-items: center;
    gap: var(--cpd-space-md);
}

.cpd-skeleton-date {
    inline-size: 160px;
    block-size: var(--cpd-space-2xl);
}

.cpd-skeleton-currency {
    inline-size: 128px;
    block-size: var(--cpd-space-2xl);
}

/* The run control (RunControlBar: the Run split button) holds its place while the runtime starts, so
   the date and currency do not shift left and back. */
.cpd-skeleton-run {
    inline-size: 128px;
    block-size: var(--cpd-space-2xl);
}

.cpd-skeleton-button {
    inline-size: var(--cpd-space-xl);
    block-size: var(--cpd-space-xl);
    border-radius: var(--cpd-radius-pill);
}

/* Below 1024 the session group's date and currency narrow (TopBarHost.razor.css); so do their
   blocks. */
@media (max-width: 1023.98px) {
    .cpd-skeleton-date { inline-size: 144px; }
    .cpd-skeleton-currency { inline-size: 112px; }
}

/* Where the bar is too narrow for the session group, the date and currency become icon-sized
   (TopBarHost.razor.css, which explains the bound); so do their blocks. The skeleton has no
   environment block, so only the narrower bound applies to it. */
@media (min-width: 641px) {
    @container topbar (max-width: 599.98px) {
        .cpd-skeleton-date,
        .cpd-skeleton-currency { inline-size: calc(var(--cpd-space-2xl) + var(--cpd-space-sm)); }
    }
}

/* Mobile: the real menu is closed behind the hamburger, so only the brand bar stands in. */
@media (max-width: 640.98px) {
    .cpd-skeleton-rows { display: none; }
}

/* The compact rail, collapsed: NavMenu shows "cp" centred and icons only. */
@media (min-width: 641px) {
    html[data-cpd-nav="compact"] .cpd-skeleton-brand {
        justify-content: center;
        padding: 0;
    }
    html[data-cpd-nav="compact"] .cpd-skeleton-brand .logo-accent,
    html[data-cpd-nav="compact"] .cpd-skeleton-text {
        display: none;
    }
    html[data-cpd-nav="compact"] .cpd-skeleton-row {
        justify-content: center;
        padding-inline: 0;
    }
}

@keyframes cpd-skeleton-pulse {
    50% { opacity: 0.5; }
}

body:has(#blazor-error-ui[style*="block"]) .cpd-skeleton-block {
    animation: none;
}

@media (prefers-reduced-motion: reduce) {
    .cpd-page-pending-bar {
        animation: none;
        background: var(--cpd-gold);
    }

    .cpd-boot-bar-fill {
        transition: none;
    }

    .cpd-skeleton-block {
        animation: none;
    }
}

/* ControlView's OpenPane: a link in a tile's text that opens another tile's pane. It has no href (the
   router would take "#tile" for the app root), so it is given a link's look here. */
.page-open-pane {
    color: var(--cpd-gold);
    cursor: pointer;
}

.page-open-pane:hover,
.page-open-pane:focus-visible {
    text-decoration: underline;
}

/* The term input's calendar (TermInput, docs/ux-shape.md entry 19, 2026-10-13): days that are not
   business days shaded, the day a rolled tenor's arithmetic put it on ringed, the anchor underlined.
   Global because MudDatePicker renders the days, which a scoped rule cannot reach. */
.term-input-calendar .mud-day.term-day-off:not(.mud-selected) {
    background: var(--cpd-surface-3);
    color: var(--cpd-text-muted);
}

.term-input-calendar .mud-day.term-day-before {
    outline: 1.5px dashed var(--cpd-gold);
    outline-offset: -2px;
}

.term-input-calendar .mud-day.term-day-anchor {
    text-decoration: underline;
}

/* ---------- Assistant panel beside a dialog (docs/ux-shape.md entry 24, amended 2026-10-15) ----------
   From 1024 wide the open panel is the page's third column. A MudDialog's container (and so its
   overlay) stops at the panel's edge instead of covering it: the dialog centres over the page and the
   panel stays usable beside it, so the reader can ask the Assistant to fill the dialog they have open.
   Global, because MudDialog renders into MudDialogProvider, which no component's scoped CSS reaches.
   Below 1024 the panel is a drawer over the page, and a dialog still covers it. */
@media (min-width: 1024px) {
    body:has(.assistant-panel) .mud-dialog-container,
    body:has(.assistant-panel) .mud-dialog-container .mud-overlay-dialog {
        width: auto;
        right: var(--cpd-assist-panel-w);
    }
}

/* ---------- Assistant pointer (docs/ux-shape.md entry 24, "The pointer") ----------
   wwwroot/js/assistantSpotlight.js draws a ring and an optional note into MainLayout's one overlay
   element, positioned in viewport coordinates and moved with the element it rings. Global, because
   the script creates them and scoped CSS cannot reach what a script creates. The overlay never takes
   a click: only the note does. Above the page's own stacking and the compact nav (1050), below the
   Assistant's panel when it is a drawer over the page (1080, below 1024) - a ringed part the drawer
   covers stays under it rather than the ring and note floating over the panel - and below MudBlazor's
   drawer, popovers and dialogs (1100+), so a dialog the reader opens covers it. */
.cpd-assist-overlay {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 1070;
}

/* A ring on a part inside a dialog (assistantSpotlight.js adds the class while it rings one): above the
   dialog's container, so the ring is not hidden behind the dialog it points into; below the dialog's
   own popovers (a select's list, MudBlazor's dialog + 3). A part on the page keeps 1070, under the
   drawer. */
.cpd-assist-overlay.cpd-assist-overlay--over-dialog {
    z-index: calc(var(--mud-zindex-dialog) + 1);
}

.cpd-assist-ring {
    position: fixed;
    border: 2px solid var(--cpd-assist);
    border-radius: var(--cpd-radius-md);
    box-shadow: 0 0 0 4px rgba(var(--cpd-assist-rgb), 0.25);
    animation: cpd-assist-pulse 1.6s ease-in-out 3;
}

@keyframes cpd-assist-pulse {
    50% { box-shadow: 0 0 0 8px rgba(var(--cpd-assist-rgb), 0.08); }
}

.cpd-assist-note {
    position: fixed;
    display: flex;
    align-items: flex-start;
    gap: var(--cpd-space-sm);
    max-width: min(320px, calc(100vw - 2 * var(--cpd-space-sm)));
    padding: var(--cpd-space-sm) var(--cpd-space-md);
    border-radius: var(--cpd-radius-md);
    background: var(--cpd-assist);
    color: var(--cpd-assist-ink);
    font-size: var(--cpd-text-2);
    line-height: 1.4;
    box-shadow: var(--cpd-shadow-soft);
    pointer-events: auto;
}

.cpd-assist-note-close {
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: inherit;
    font-size: var(--cpd-text-3);
    line-height: 1;
    cursor: pointer;
    padding: 0 var(--cpd-space-xs);
    border-radius: var(--cpd-radius-sm);
}

.cpd-assist-note-close:focus-visible {
    outline: 2px solid var(--cpd-assist-ink);
    outline-offset: 1px;
}

@media (prefers-reduced-motion: reduce) {
    .cpd-assist-ring {
        animation: none;
    }
}

/* ---------- A field the Assistant filled (docs/ux-shape.md entry 24, amended 2026-10-14) ----------
   ControlView adds .assist-filled to a field the Assistant filled until the user edits it or saves,
   with "Filled by the Assistant" as the field's helper text: the edge is the assistant colour and the
   words say it, so the state is never colour alone. Global, because the class sits on MudBlazor's own
   input root, which scoped CSS cannot reach reliably. The edge rule names MudBlazor's own outlined-input
   classes, so it outranks their border; focus and an error keep their own colour. */
.assist-filled .mud-input.mud-input-outlined:not(:focus-within):not(.mud-input-error) .mud-input-outlined-border {
    border-color: var(--cpd-assist);
    border-width: 2px;
}

.assist-filled .mud-input-helper-text,
.assist-filled-note {
    color: var(--cpd-assist);
}

.assist-filled-note {
    margin-top: var(--cpd-space-xs);
    font-size: var(--cpd-text-1);
}
