/* WinkPG Developer Portal - docs site theme.
 *
 * Ported from the dx.winkpg.com design direction: a Stripe-shaped documentation shell with a thin
 * header, a persistent left sidebar, a measured content column, and a next-steps footer closing
 * every page.
 *
 * Brand colours are NOT declared here. They arrive as --dxp-brand-* custom properties from
 * /css/brand-tokens.css, which the host renders per instance. This file consumes them and supplies
 * a fallback in every var() so a missing token file degrades to the platform palette rather than to
 * unstyled text. Everything else is expressed against the neutral ramp below, which is what makes
 * an instance's rebrand a three-value change.
 *
 * Both colour schemes are first-class. A reader's system preference is the starting point, and the
 * header toggle overrides it by writing data-theme on the html element (see wwwroot/js/theme.js).
 * That is why the dark ramp is declared twice below: once for the explicit choice and once for the
 * system preference, which no single selector can express. The two blocks must stay in step, and
 * ThemeTokenBlockParityTests fails the build when they drift.
 */

/* ── Fonts ───────────────────────────────────────────────────────────── */

/* Self-hosted rather than loaded from a font CDN, because this host sets font-src 'self'. Both
 * families are variable fonts, so one file per family covers the whole weight axis: requesting
 * individual weights from Google returns the same file each time, and shipping it once is the
 * difference between 58KB and about 150KB of duplicated payload.
 *
 * Latin subset only, with the matching unicode-range, so a page that happens to contain Cyrillic or
 * Greek falls through to the system stack rather than rendering tofu. */

@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 200 800;
    font-display: swap;
    src: url('../fonts/plus-jakarta-sans-variable.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
        U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 100 800;
    font-display: swap;
    src: url('../fonts/jetbrains-mono-variable.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
        U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ── Tokens ──────────────────────────────────────────────────────────── */

:root {
    color-scheme: light dark;

    --dxp-bg: #ffffff;
    --dxp-bg-subtle: #F3F3F3;
    --dxp-bg-raised: #ffffff;
    --dxp-bg-code: #16181d;

    --dxp-fg: #212121;
    --dxp-fg-muted: #5C629E;
    --dxp-fg-faint: #6B7285;
    --dxp-fg-code: #e8eaf0;

    --dxp-border: #E4E8F3;
    --dxp-border-strong: #C9CFE3;

    --dxp-accent: var(--dxp-brand-primary, #3D3C8A);
    --dxp-accent-contrast: #ffffff;
    --dxp-accent-subtle: color-mix(in srgb, var(--dxp-accent) 10%, transparent);
    --dxp-highlight: var(--dxp-brand-secondary, #DA0A4F);

    /* The eyebrow colour, darkened from the instance's tertiary rather than used raw. The dx teal
     * scores 2.6:1 against white and the platform default is no better, and this token colours small
     * uppercase text, so taken literally it would fail AA everywhere it appears. Darkening the brand
     * value keeps a rebranded instance's hue instead of pinning one accessible teal for everyone.
     * The dark ramp uses it raw, where it already clears AA comfortably. */
    --dxp-teal: color-mix(in srgb, var(--dxp-brand-tertiary, #51ACA0) 65%, #000000);

    --dxp-method-get: #1a7f5a;
    --dxp-method-post: #2b5fd9;
    --dxp-method-put: #9a6400;
    --dxp-method-patch: #7a4bbd;
    --dxp-method-delete: #c0322b;

    /* The code-sample chips, one colour per language, each recognisably that language's own. These
     * are the only tokens here that are not the instance's brand and must not become it: a reader
     * scanning for Python is looking for Python's blue, and rebranding it would make the row four
     * shades of one hue. They are drawn from each project's published colour and then moved to clear
     * AA against the ramp they sit on, which is why the light values are not the raw brand hexes: the
     * TypeScript blue lands at 4.5:1 on white, close enough to the line that a nudge is cheaper than
     * an argument. The dark ramp lightens all four, where the raw values run from marginal (Python,
     * 3.1:1) to failing outright (the .NET purple, 2.4:1). */
    --dxp-lang-curl: #4a5568;
    --dxp-lang-powershell: #01579b;
    --dxp-lang-typescript: #2b6cb0;
    --dxp-lang-csharp: #512bd4;
    --dxp-lang-python: #3776ab;

    /* The request log's highlighted bodies, one colour per kind of JSON run. Chosen against
     * --dxp-bg-subtle, which is what a .dxp-code block paints, rather than against the page. */
    --dxp-json-key: #0b5394;
    --dxp-json-string: #a3374b;
    --dxp-json-number: #0a6b4a;
    --dxp-json-keyword: #6a3ab2;
    --dxp-json-punct: #6B7285;

    --dxp-radius: 12px;
    --dxp-radius-sm: 8px;
    --dxp-radius-lg: 16px;
    /* The shell the header, body and footer all share. These three were three independent literals
     * that had to agree by hand: a container left behind renders a header whose brand no longer sits
     * above the sidebar, and nothing reports it, because no test in this repository loads the
     * stylesheet. One token now drives all three, and DocsShellWidthTokenTests fails the build if a
     * literal comes back.
     *
     * The value is deliberately not the viewport. Generous side margins are part of how the docs
     * read, so the shell grows modestly and the reclaimed width goes to the content column, which
     * was clamped well inside the shell it already had. */
    --dxp-shell-width: 96rem;
    --dxp-shell-gutter: 1.5rem;
    --dxp-body-gap: 2.5rem;

    --dxp-sidebar-width: 20rem;

    /* Breathing room inside the sidebar's scroll container, so the things that paint outside their
     * own border box there (the search field's focus ring, the nav links' leftward background
     * bleed) are not clipped by it. The sidebar cancels this with a negative margin, so it costs no
     * layout width and --dxp-sidebar-width above stays the number every other rule reasons about.
     * See the .dxp-sidebar rule for why a scrolling container clips on both axes. */
    --dxp-sidebar-gutter: 0.75rem;

    /* Derived, not chosen: whatever the row leaves after the gutters, the sidebar and the gap is
     * exactly what the main column may occupy. Picking an independent number is how width ends up
     * stranded inside a shell that is already wide enough. */
    --dxp-content-width: calc(
        var(--dxp-shell-width) - (2 * var(--dxp-shell-gutter)) - var(--dxp-sidebar-width) -
            var(--dxp-body-gap)
    );

    /* A readable line length for running prose. The content column is sized for tables, card grids
     * and code blocks; paragraphs set across the whole of it would be tiring to read.
     *
     * Widened from 46rem: at that value a caption paragraph stopped roughly two thirds of the way
     * across the column, and against the full-width code block directly above it the short line
     * read as a broken container rather than a chosen measure. 60rem still holds the line short of
     * the full column, so the cap keeps doing its job where prose runs for paragraphs at a time. */
    --dxp-measure: 60rem;

    --dxp-header-height: 3.75rem;

    --dxp-font: 'Plus Jakarta Sans', system-ui, -apple-system, "Segoe UI", Roboto,
        "Helvetica Neue", sans-serif;
    --dxp-font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
        monospace;
}

/* The dark ramp, for a reader who chose it. The accent is lightened from whatever the instance
 * brands with rather than replaced by a fixed hex: the dx indigo scores about 2:1 against this
 * background and fails AA outright, and hard-coding a lighter purple here would silently drop a
 * rebranded instance's colour in one of the two schemes. The contrast pair flips with it, because
 * white on the lightened accent is worse than the dark background is. */
:root[data-theme="dark"] {
    color-scheme: dark;

    --dxp-bg: #101216;
    --dxp-bg-subtle: #171a20;
    --dxp-bg-raised: #171a20;
    --dxp-bg-code: #0b0d11;

    --dxp-fg: #e8eaf0;
    --dxp-fg-muted: #9aa1b4;
    --dxp-fg-faint: #737b8f;

    --dxp-border: #262a33;
    --dxp-border-strong: #363c48;

    --dxp-accent: color-mix(in srgb, var(--dxp-brand-primary, #3D3C8A) 55%, #ffffff);
    --dxp-accent-contrast: #101216;
    --dxp-accent-subtle: color-mix(in srgb, var(--dxp-accent) 22%, transparent);
    --dxp-teal: var(--dxp-brand-tertiary, #51ACA0);

    --dxp-method-get: #48c79a;
    --dxp-method-post: #6f9cff;
    --dxp-method-put: #d8a33a;
    --dxp-method-patch: #b190ea;
    --dxp-method-delete: #ef7a72;

    --dxp-lang-curl: #9fa9be;
    --dxp-lang-powershell: #6ea8ff;
    --dxp-lang-typescript: #4f9ce8;
    --dxp-lang-csharp: #9a7bf0;
    --dxp-lang-python: #5a9fd4;

    --dxp-json-key: #86c8f0;
    --dxp-json-string: #e39a86;
    --dxp-json-number: #9fd3a8;
    --dxp-json-keyword: #bda2f0;
    --dxp-json-punct: #9aa1b4;
}

/* The same ramp for a reader who chose nothing and whose system says dark. Declared separately
 * because a media query and an attribute cannot be combined into one selector, and excluded for an
 * explicit light choice so the toggle can override the system rather than lose to it. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;

        --dxp-bg: #101216;
        --dxp-bg-subtle: #171a20;
        --dxp-bg-raised: #171a20;
        --dxp-bg-code: #0b0d11;

        --dxp-fg: #e8eaf0;
        --dxp-fg-muted: #9aa1b4;
        --dxp-fg-faint: #737b8f;

        --dxp-border: #262a33;
        --dxp-border-strong: #363c48;

        --dxp-accent: color-mix(in srgb, var(--dxp-brand-primary, #3D3C8A) 55%, #ffffff);
        --dxp-accent-contrast: #101216;
        --dxp-accent-subtle: color-mix(in srgb, var(--dxp-accent) 22%, transparent);
        --dxp-teal: var(--dxp-brand-tertiary, #51ACA0);

        --dxp-method-get: #48c79a;
        --dxp-method-post: #6f9cff;
        --dxp-method-put: #d8a33a;
        --dxp-method-patch: #b190ea;
        --dxp-method-delete: #ef7a72;

        --dxp-lang-curl: #9fa9be;
        --dxp-lang-powershell: #6ea8ff;
        --dxp-lang-typescript: #4f9ce8;
        --dxp-lang-csharp: #9a7bf0;
        --dxp-lang-python: #5a9fd4;

        --dxp-json-key: #86c8f0;
        --dxp-json-string: #e39a86;
        --dxp-json-number: #9fd3a8;
        --dxp-json-keyword: #bda2f0;
        --dxp-json-punct: #9aa1b4;
    }
}

/* An explicit light choice, which only has to undo the system's dark rendering of form controls
 * and scrollbars: every token above already holds. */
:root[data-theme="light"] {
    color-scheme: light;
}

/* ── Visibility ──────────────────────────────────────────────────────── */

/* The docs pages keep both branches of a two-state region mounted and hide one, per the platform's
 * DOM-stability rule. They toggle THIS CLASS rather than an inline style="display:none": the host
 * enforces style-src 'self' with no 'unsafe-inline', and style-src covers inline style attributes as
 * well as <style> blocks (style-src-attr falls back to it), so an inline toggle is dropped by the
 * browser and every element meant to be hidden renders visible. Nothing server-side notices, and
 * bUnit reads the rendered attribute rather than the computed style.
 *
 * !important so a hidden element stays hidden regardless of what a display utility on the same
 * element would otherwise set.
 */
.dxp-hidden {
    display: none !important;
}

/* Present to a screen reader, absent to everyone else. Not interchangeable with dxp-hidden: a
 * display:none element is out of the accessibility tree entirely, so an aria-live region hidden that
 * way announces nothing and the failure is completely silent. The search typeahead's result count is
 * the one thing that reaches this state today.
 *
 * The 1px clipped box rather than width:0: a zero-sized element is dropped by some screen readers
 * for the same reason display:none is. */
.dxp-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── Base ────────────────────────────────────────────────────────────── */

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--dxp-bg);
    color: var(--dxp-fg);
    font-family: var(--dxp-font);
    font-size: 16px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
}

a {
    color: var(--dxp-accent);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

a:focus-visible,
button:focus-visible,
input:focus-visible {
    outline: 2px solid var(--dxp-accent);
    outline-offset: 2px;
    border-radius: var(--dxp-radius-sm);
}

/* The router focuses the page heading on every navigation (FocusOnNavigate in Routes.razor), which is
 * what makes a screen reader announce the new page. Blazor marks the heading focusable with
 * tabindex="-1" in order to do it, so the browser paints its default focus ring around the title and
 * leaves it there until the reader clicks or tabs elsewhere: a stray box around the page title on
 * every page. Suppress the ring, keep the focus move.
 *
 * This is not a lost focus indicator. A negative tabindex is skipped by the tab order, so no reader
 * can land on the heading by keyboard and be left with nothing to see. Interactive controls keep the
 * accent outline above, which is the rule that governs the elements a reader can actually reach.
 *
 * Do not delete this as dead CSS: it looks unreachable precisely because nothing else here focuses a
 * heading. */
h1:focus,
h1:focus-visible {
    outline: none;
}

code {
    font-family: var(--dxp-font-mono);
    font-size: 0.875em;
}

h1, h2, h3 {
    line-height: 1.25;
    margin: 0;
    font-weight: 650;
    letter-spacing: -0.01em;
}

/* ── Shell ───────────────────────────────────────────────────────────── */

.dxp-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

.dxp-body {
    display: flex;
    align-items: flex-start;
    flex: 1 1 auto;
    width: 100%;
    max-width: var(--dxp-shell-width);
    margin: 0 auto;
    padding: 0 var(--dxp-shell-gutter);
    gap: var(--dxp-body-gap);
}

.dxp-main {
    flex: 1 1 auto;
    min-width: 0;
    max-width: var(--dxp-content-width);
    padding: 2.5rem 0 4rem;
}

/* ── Header ──────────────────────────────────────────────────────────── */

.dxp-header {
    position: sticky;
    top: 0;
    z-index: 10;
    background: color-mix(in srgb, var(--dxp-bg) 92%, transparent);
    backdrop-filter: saturate(180%) blur(8px);
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    height: var(--dxp-header-height);
    max-width: var(--dxp-shell-width);
    margin: 0 auto;
    padding: 0 var(--dxp-shell-gutter);
}

.dxp-header__brand {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--dxp-fg);
    font-weight: 650;
    font-size: 1.05rem;
}

.dxp-header__brand:hover {
    text-decoration: none;
}

.dxp-header__logo {
    height: 1.5rem;
    width: auto;
}

.dxp-header__suffix {
    color: var(--dxp-fg-faint);
    font-weight: 500;
}

.dxp-header__nav {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    flex-wrap: wrap;
}

/* The disclosure that reveals the nav below the shell's narrow breakpoint, and nothing at all above
 * it: the links sit inline on the bar wherever there is room for them, so the wide layout must not
 * pay a control for a state it never enters. The rest of it, including why the reveal is a checkbox,
 * is in the "Narrow viewports" block that switches it on and in the comment in DocsHeader.razor.
 *
 * The state input is display: none here and only here, which is a deliberate departure from the
 * rule that a visually-hidden control must never be display: none. That rule exists because
 * display: none takes an element out of the accessibility tree AND out of the tab order, and the
 * usual mistake is doing it to a control somebody still needs to reach. Above this breakpoint
 * nobody does: the panel it toggles is laid out inline whatever the checkbox says, so leaving it
 * merely visually hidden would put a tab stop announcing "Show site navigation" on every desktop
 * page of the site, in front of a control that changes nothing. The narrow block puts it back, and
 * dxp-visually-hidden is what makes it reachable and announced there. */
.dxp-header__menu-state,
.dxp-header__menu-toggle {
    display: none;
}

.dxp-header__link {
    color: var(--dxp-fg-muted);
    font-size: 0.9375rem;
    font-weight: 500;
}

.dxp-header__link:hover {
    color: var(--dxp-fg);
    text-decoration: none;
}

.dxp-header__link--cta {
    padding: 0.375rem 0.875rem;
    border-radius: 999px;
    background: var(--dxp-accent);
    color: var(--dxp-accent-contrast);
}

.dxp-header__link--cta:hover {
    color: var(--dxp-accent-contrast);
    filter: brightness(1.08);
}

/* The account affordance (Sign in, Sign out). Outlined rather than filled on purpose: it needs to
 * read as a distinct destination next to the plain nav links, but an instance that publishes a
 * support address already spends the one filled accent pill on it, and two competing pills in a
 * six-item nav reads as two primary actions. */
.dxp-header__link--account {
    padding: 0.375rem 0.875rem;
    border: 1px solid var(--dxp-border);
    border-radius: 999px;
    color: var(--dxp-fg);
}

.dxp-header__link--account:hover {
    border-color: var(--dxp-accent);
    color: var(--dxp-accent);
}

/* ── Theme toggle ────────────────────────────────────────────────────── */

/* Hidden until the theme script has run. The button does nothing without JavaScript, and a control
 * that visibly does nothing is worse than no control: the reader cannot tell whether they misread
 * the icon or the site is broken. wwwroot/js/theme.js adds dxp-js to the html element, so the
 * toggle appears only where it works. */
.dxp-theme-toggle {
    display: none;
}

:root.dxp-js .dxp-theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.125rem;
    height: 2.125rem;
    padding: 0;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg-muted);
    cursor: pointer;
}

.dxp-theme-toggle:hover {
    color: var(--dxp-fg);
    border-color: var(--dxp-border-strong);
}

.dxp-theme-toggle svg {
    width: 1rem;
    height: 1rem;
    display: block;
}

/* Both icons stay mounted and one is hidden, per the platform's DOM-stability rule, and they hide
 * with a class because this host's style-src 'self' drops an inline style attribute. Which one
 * shows is the scheme in force, so it needs the same three-way split as the token blocks: the moon
 * offers dark and shows while light is in force, the sun offers light. */
.dxp-theme-toggle__sun {
    display: none;
}

:root[data-theme="dark"] .dxp-theme-toggle__sun {
    display: block;
}

:root[data-theme="dark"] .dxp-theme-toggle__moon {
    display: none;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .dxp-theme-toggle__sun {
        display: block;
    }

    :root:not([data-theme="light"]) .dxp-theme-toggle__moon {
        display: none;
    }
}

/* ── Sidebar ─────────────────────────────────────────────────────────── */

/* The horizontal padding, and the negative margin that cancels it, are load-bearing rather than
 * decorative spacing. This is a scrolling container, and setting overflow-y alone is enough to make
 * it clip on the other axis too: CSS computes overflow-x from visible to auto whenever the other
 * value is not visible, so there is no such thing as "scrolls vertically, overflows horizontally".
 *
 * Two things inside deliberately paint outside their own border box, and with no padding both are
 * cut off at the left content edge, which a left-to-right scroll container can never scroll past.
 * The search field is width: 100%, so the 4px accent focus ring (2px outline plus 2px offset, from
 * the base rule near the top of this file) has nowhere to go and a focused field shows three sides
 * of a ring. The nav links carry a negative left margin so their hover and current-page background
 * bleeds leftward past the label, and that bleed is shaved off.
 *
 * --dxp-sidebar-gutter is what each side gets back. It is added to the width and taken off again as
 * a negative margin, so the flex item still occupies exactly --dxp-sidebar-width, the content edge
 * lands where it did before, and --dxp-content-width (a calc over the same token) is untouched.
 * Everything here is border-box, so the widened width absorbs the padding rather than adding to it.
 *
 * Raising the gutter is safe; lowering it below the ring's 4px reintroduces the clipped focus ring,
 * which no test other than SidebarScrollGutterStylesheetTests can see. */
.dxp-sidebar {
    position: sticky;
    top: var(--dxp-header-height);
    flex: 0 0 calc(var(--dxp-sidebar-width) + (2 * var(--dxp-sidebar-gutter)));
    width: calc(var(--dxp-sidebar-width) + (2 * var(--dxp-sidebar-gutter)));
    margin-inline: calc(-1 * var(--dxp-sidebar-gutter));
    max-height: calc(100vh - var(--dxp-header-height));
    overflow-y: auto;
    padding: 2rem var(--dxp-sidebar-gutter) 3rem;
}

/* A margin alone stops separating groups once the nav is long (Get started, Blueprints, the guide
 * groups, then one section per API resource): the eye reads a single run of links. The rule is on
 * the sibling rather than on every section so the first group does not open under a stray line, and
 * the space is split either side of it so the divider sits between the two groups rather than
 * crowding the heading below it. --dxp-border is the palette's lightest line and is themed for both
 * schemes, which is what keeps this a hairline separator rather than a horizontal rule. */
/* The disclosure that reveals the link tree below the shell's narrow breakpoint. Absent above it,
 * where the tree is simply the sidebar and has nothing to disclose. Switched on, and explained, in
 * the "Narrow viewports" block; the state input is display: none above the breakpoint for the
 * reason .dxp-header__menu-state is, which is written out there. */
.dxp-sidebar__nav-state,
.dxp-sidebar__nav-toggle {
    display: none;
}

.dxp-sidebar__section + .dxp-sidebar__section {
    margin-top: 1.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--dxp-border);
}

.dxp-sidebar__heading {
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--dxp-teal);
    margin: 0 0 0.5rem;
}

/* The heading row of a group, which is now a summary rather than a bare h2: every section is a
 * details element, and the reader opens the one they want instead of scrolling past all of them.
 *
 * Padded and pulled back by the same 0.625rem the links use, so its hover background bleeds leftward
 * to the same edge theirs does and the heading text still starts on the sidebar's content edge. Both
 * that bleed and the focus ring below are paid for by --dxp-sidebar-gutter, which is the only reason
 * this scroll container does not shave them off; see the .dxp-sidebar rule above.
 *
 * list-style and the WebKit pseudo-element both have to go, for the reason .dxp-split-button__toggle
 * states: a summary draws a disclosure triangle by default and the two vendors hide it by different
 * mechanisms, so left alone the caret below renders beside a second, differently drawn arrow. */
.dxp-sidebar__summary {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.625rem;
    margin-left: -0.625rem;
    border-radius: var(--dxp-radius-sm);
    cursor: pointer;
    list-style: none;
}

.dxp-sidebar__summary::-webkit-details-marker {
    display: none;
}

.dxp-sidebar__summary:hover {
    background: var(--dxp-bg-subtle);
}

/* A summary is focusable but is none of the three elements the base focus rule near the top of this
 * file names, so it needs the ring restated rather than inherited. Same width and offset, because
 * the gutter that keeps it on screen is sized against that rule and this element sits in the same
 * clipping container. */
.dxp-sidebar__summary:focus-visible {
    outline: 2px solid var(--dxp-accent);
    outline-offset: 2px;
}

/* The heading gives up its bottom margin inside the summary and the open state gives it back below,
 * so a closed group is a single row with nothing hanging off it and an open one keeps exactly the
 * spacing between heading and links the sidebar had before this change. */
.dxp-sidebar__summary .dxp-sidebar__heading {
    margin-bottom: 0;
}

.dxp-sidebar__section[open] > .dxp-sidebar__summary {
    margin-bottom: 0.5rem;
}

/* Pushed to the far end of the row rather than sitting beside the heading, so the carets line up
 * down the sidebar whatever a group is called. It reports the state the summary already announces,
 * for the reader who can see it rather than hear it, and it is keyed on the details element's own
 * open attribute so the browser drives it and no script has to keep a class in step. */
.dxp-sidebar__caret {
    flex: 0 0 auto;
    margin-left: auto;
    width: 0.875rem;
    height: 0.875rem;
    color: var(--dxp-fg-faint);
}

.dxp-sidebar__section[open] > .dxp-sidebar__summary .dxp-sidebar__caret {
    transform: rotate(90deg);
}

.dxp-sidebar__list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.dxp-sidebar__link {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    padding: 0.3125rem 0.625rem;
    margin-left: -0.625rem;
    border-radius: var(--dxp-radius-sm);
    color: var(--dxp-fg-muted);
    font-size: 0.9rem;
    line-height: 1.4;
}

.dxp-sidebar__link:hover {
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg);
    text-decoration: none;
}

.dxp-sidebar__link--current {
    background: var(--dxp-accent-subtle);
    color: var(--dxp-accent);
    font-weight: 600;
}

/* The verb is the main scanning aid in a list this long, so it renders as the same chip the content
 * pane uses (HttpMethodBadge, .dxp-method--sm) rather than as its own text treatment. This element
 * is only the wrapper the markup toggles dxp-hidden on for a link with no verb: it carries no colour
 * of its own, which is what makes "the sidebar and the page cannot disagree about what colour a
 * DELETE is" structural rather than a convention two rule sets have to keep in step.
 *
 * The link is align-items: baseline, so the chip's verb text sits on the same baseline as the label
 * text, including when a long label wraps (baseline alignment uses the first line). */
.dxp-sidebar__method {
    flex: 0 0 auto;
}

.dxp-sidebar__label {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* ── Search ──────────────────────────────────────────────────────────── */

/* A GET form, not a control with a handler: the docs surface is statically rendered and buys no
   circuit, so search has to work with the address bar alone. The submit button is visible rather
   than relying on the Enter key, because a form with one field and no visible control is a form some
   readers never discover they can submit. */

/* Block rather than flex, with the row inside it doing the laying out. The form is the positioning
   context the suggestion panel hangs off, and it holds a label in the page variant, so it cannot
   also be the row. */
.dxp-search {
    position: relative;
    margin-bottom: 1.5rem;
}

.dxp-search__icon {
    position: absolute;
    left: 0.625rem;
    font-size: 0.8rem;
    opacity: 0.55;
    pointer-events: none;
    /* Sits above the field rather than beside it, so the icon does not consume a flex track and
       leave the input narrower than the sidebar. */
    z-index: 1;
}

/* The field's own positioning context, holding the input and the slash-key hint drawn on top of it.
   It exists so the hint is placed against the input's right-hand edge rather than the row's: the row
   also holds the submit button, so an offset measured from there is really the button's rendered
   width written out as a number, and every change to that button's label, padding or font moves the
   hint. The magnifier keeps the row as its context: it is anchored to the left edge, which the
   button cannot move, and this wrapper is the row's first in-flow item, so the two edges coincide.

   Flex rather than block so the input is not laid out as inline content, which would leave descender
   space below it and drop the field a couple of pixels out of line with the button beside it. */
.dxp-search__field {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
}

/* Both horizontal insets reserve room for something overlaid on the field rather than beside it:
 * the magnifier on the left, the slash-key hint on the right. Without the right one the placeholder
 * and any typed query run underneath the hint and are then cut off by the field's own edge, which
 * is what the sidebar's "Search the documentati…" looked like. The page variant below resets both,
 * because it renders neither overlay. */
.dxp-search__input {
    width: 100%;
    min-width: 0;
    padding: 0.4375rem 1.75rem 0.4375rem 2rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg);
    font: inherit;
    font-size: 0.875rem;
}

.dxp-search__submit {
    flex: 0 0 auto;
    padding: 0.4375rem 0.75rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg);
    font: inherit;
    font-size: 0.875rem;
    cursor: pointer;
}

.dxp-search__submit:hover {
    border-color: var(--dxp-accent);
    color: var(--dxp-accent);
}

/* The results page carries its own copy of the form, stacked and labelled. On a narrow viewport the
   sidebar drops below the content, so a reader refining a search that found nothing would otherwise
   have to scroll past every explanation to reach the only control that can help. */

.dxp-search--page {
    display: block;
    margin-bottom: 2rem;
}

.dxp-search__label {
    display: block;
    margin-bottom: 0.375rem;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
    font-weight: 600;
}

/* The row is the positioning context for the magnifier, which the sidebar variant overlays on the
   field. The form cannot be, because the page variant puts a label above the row and the icon would
   then sit against the label instead of the input. */
.dxp-search__row {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.375rem;
}

.dxp-search--page .dxp-search__input {
    padding-left: 0.625rem;
    padding-right: 0.625rem;
}

/* ── Search typeahead ────────────────────────────────────────────────── */

/* Everything below is the progressive enhancement in wwwroot/js/docs-search.js. The panel is empty
   and carries dxp-hidden until the script has something to put in it, so a reader with no
   JavaScript sees the plain GET form and nothing else. */

/* The keyboard hint, hidden until the script has marked the document, exactly like the theme
   toggle. A hint naming a key that does nothing is worse than no hint: a reader who tries it gets a
   slash typed into the page and concludes the site is broken. */
.dxp-search__hint {
    display: none;
}

:root.dxp-js .dxp-search__hint {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    min-width: 1.375rem;
    height: 1.375rem;
    padding: 0 0.3125rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg);
    color: var(--dxp-fg-muted);
    font-family: var(--dxp-font-mono);
    font-size: 0.6875rem;
    line-height: 1;
    /* Overlays the right-hand end of the field rather than taking a flex track, so the input keeps
       the full width of the sidebar. pointer-events off so a click lands on the field under it,
       which is what a reader aiming at the hint means.

       Positioned inside .dxp-search__field, so this inset is measured from the input's own right
       edge and nothing to the right of the field can move it. The inset and this rule's min-width
       together spend exactly the right-hand inset the field reserves above, which is what keeps the
       placeholder and any typed query clear of the hint. */
    position: absolute;
    top: 50%;
    right: 0.375rem;
    transform: translateY(-50%);
    pointer-events: none;
}

.dxp-search__panel {
    position: absolute;
    z-index: 20;
    top: calc(100% + 0.25rem);
    left: 0;
    right: 0;
    max-height: 24rem;
    overflow-y: auto;
    padding: 0.25rem;
    border: 1px solid var(--dxp-border-strong);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-raised);
    box-shadow: 0 0.75rem 1.75rem rgba(0, 0, 0, 0.18);
}

.dxp-search__group + .dxp-search__group {
    margin-top: 0.25rem;
    padding-top: 0.25rem;
    border-top: 1px solid var(--dxp-border);
}

.dxp-search__group-title {
    padding: 0.3125rem 0.5rem 0.1875rem;
    color: var(--dxp-fg-muted);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.dxp-search__option {
    display: block;
    padding: 0.375rem 0.5rem;
    border-radius: var(--dxp-radius-sm);
    color: inherit;
    text-decoration: none;
}

/* Hover and keyboard selection look the same on purpose: there is one selected suggestion, and a
   reader who has been arrowing down and then moves the mouse should not see two. */
.dxp-search__option:hover,
.dxp-search__option--active {
    background: var(--dxp-bg-subtle);
    color: inherit;
}

.dxp-search__option-title {
    display: block;
    font-size: 0.8125rem;
    font-weight: 600;
}

.dxp-search__option-meta {
    display: block;
    margin-top: 0.0625rem;
    color: var(--dxp-fg-muted);
    font-size: 0.6875rem;
}

.dxp-search__option--all {
    margin-top: 0.25rem;
    padding-top: 0.4375rem;
    border-top: 1px solid var(--dxp-border);
    border-radius: 0 0 var(--dxp-radius-sm) var(--dxp-radius-sm);
    color: var(--dxp-accent);
    font-size: 0.75rem;
    font-weight: 600;
}

/* The matched run inside a suggestion. Restated rather than left to the browser default, which is
   black on yellow and unreadable against the dark ramp. */
.dxp-search__match {
    background: var(--dxp-accent-subtle);
    color: inherit;
    border-radius: 0.125rem;
    padding: 0 0.0625rem;
}

.dxp-search__empty {
    padding: 0.625rem 0.5rem;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
}

/* ── Search results ──────────────────────────────────────────────────── */

.dxp-results {
    margin: 0;
    padding: 0;
    list-style: none;
}

.dxp-results__item + .dxp-results__item {
    margin-top: 0.5rem;
}

.dxp-results__link {
    display: block;
    padding: 0.75rem 0.875rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    color: inherit;
    text-decoration: none;
}

.dxp-results__link:hover {
    border-color: var(--dxp-accent);
}

.dxp-results__title {
    display: block;
    color: var(--dxp-accent);
    font-weight: 600;
    /* An operation id, an error code and a route are all long unbroken tokens, and the results column
       is narrower than the reference pages these come from. */
    overflow-wrap: break-word;
}

.dxp-results__meta {
    display: block;
    margin-top: 0.125rem;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
    overflow-wrap: break-word;
}

.dxp-results__excerpt {
    display: block;
    margin-top: 0.375rem;
    font-size: 0.875rem;
    overflow-wrap: break-word;
}

/* ── Hero and sections ───────────────────────────────────────────────── */

.dxp-hero {
    padding-bottom: 2.5rem;
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-hero__title {
    font-size: clamp(1.875rem, 1.4rem + 1.8vw, 2.75rem);
    margin-bottom: 0.75rem;
}

.dxp-hero__lead {
    margin: 0 0 1.5rem;
    font-size: 1.0625rem;
    color: var(--dxp-fg-muted);
    max-width: var(--dxp-measure);
}

.dxp-hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.dxp-hero__meta {
    margin: 1.25rem 0 0;
    font-size: 0.875rem;
    color: var(--dxp-fg-faint);
}

.dxp-section {
    padding-top: 2.5rem;
}

/* A section carrying an id is a fragment target: a write redirected here so the developer lands on
 * the controls rather than at the top of the page. The offset is the same one the operation lists
 * and the docs headings use, because the thing that would cover it is the same sticky header.
 *
 * Keyed on the presence of an id rather than on a modifier class, so a section that becomes a
 * fragment target cannot be linked to before somebody remembers to give it the offset: the symptom
 * is a heading under the header, which reads as landing on the wrong place rather than as a missing
 * class. */
.dxp-section[id] {
    scroll-margin-top: calc(var(--dxp-header-height) + 1rem);
}

/* A section that groups sections rather than carrying content of its own: it exists to be a fragment
 * target and to hold the notice a write reports into, and the sections inside it supply the title and
 * the rhythm. Without this the padding above applies twice and the group sits a whole gap further
 * down the page than its first child did before it was wrapped.
 *
 * A modifier rather than a plain div, because the offset above is deliberately keyed on the id and
 * not on a class: a wrapper that opted out of being a section would have to remember the offset
 * separately, and forgetting it lands the browser under the sticky header with nothing failing. */
.dxp-section--grouping {
    padding-top: 0;
}

.dxp-section__title {
    font-size: 1.25rem;
    margin-bottom: 0.5rem;
}

/* A section that closes a page with reference material rather than content the reader came for: a
 * legend, a glossary, a note on how to read what is above it. Same section rhythm, quieter title, so
 * it does not compete with the section it explains. */
.dxp-section__title--minor {
    font-size: 1.0625rem;
    color: var(--dxp-fg-muted);
}

.dxp-section__lead {
    margin: 0 0 1.25rem;
    color: var(--dxp-fg-muted);
    max-width: var(--dxp-measure);
}

.dxp-article__header {
    padding-bottom: 1.5rem;
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-article__header h1 {
    font-size: clamp(1.625rem, 1.3rem + 1.2vw, 2.125rem);
    margin-bottom: 0.75rem;
}

.dxp-article__lead {
    margin: 1rem 0 0;
    color: var(--dxp-fg-muted);
    max-width: var(--dxp-measure);
}

.dxp-article__meta {
    margin: 0.75rem 0 0;
    font-size: 0.875rem;
    color: var(--dxp-fg-faint);
}

.dxp-breadcrumb {
    display: flex;
    gap: 0.5rem;
    margin: 0 0 0.75rem;
    font-size: 0.8125rem;
    color: var(--dxp-fg-faint);
}

.dxp-breadcrumb__separator {
    color: var(--dxp-border-strong);
}

.dxp-muted {
    color: var(--dxp-fg-muted);
}

/* A secondary sentence under something else in the same cell: the rotation-overlap deadline and the
 * legacy-format nudge on an API key row.
 *
 * display:block rather than a paragraph, because it sits inside a table cell alongside an inline
 * chip and must start on its own line without introducing block-level margins into the row. It is
 * combined with .dxp-hidden when the notice does not apply, and .dxp-hidden's display:none carries
 * !important precisely so it wins over this. */
.dxp-note {
    display: block;
    margin-top: 0.25rem;
    font-size: 0.8125rem;
    line-height: 1.4;
}

/* ── Buttons ─────────────────────────────────────────────────────────── */

.dxp-button {
    display: inline-flex;
    align-items: center;
    padding: 0.5625rem 1.125rem;
    border-radius: var(--dxp-radius-sm);
    font-size: 0.9375rem;
    font-weight: 600;
    border: 1px solid transparent;
}

.dxp-button:hover {
    text-decoration: none;
}

.dxp-button--primary {
    background: var(--dxp-accent);
    color: var(--dxp-accent-contrast);
}

.dxp-button--primary:hover {
    color: var(--dxp-accent-contrast);
    filter: brightness(1.08);
}

.dxp-button--ghost {
    border-color: var(--dxp-border-strong);
    color: var(--dxp-fg);
}

.dxp-button--ghost:hover {
    background: var(--dxp-bg-subtle);
}

/* A disabled button has to look disabled. The variants above set their own background and colour, so
 * without this rule the user agent's greying is overridden and a disabled primary button is visually
 * identical to a live one: every interactive page raises a busy flag that disables its buttons while
 * a call is in flight, and the whole of that state was invisible. Dimmed by opacity so both variants
 * read the same way, and below the :hover rules so the hover effects lose at equal specificity: a
 * pointer resting on the button it just pressed would otherwise keep the pressed look alive. */
.dxp-button:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    filter: none;
}

.dxp-button--ghost:disabled {
    background: transparent;
}

/* ── Row actions ──────────────────────────────────────────────────────────
 *
 * The per-row controls in the scenario composer: move a step, remove a step, remove a capture. Icons
 * rather than words, because three word-labelled buttons per row read as heavier than the row they
 * act on, and a composed scenario is mostly rows.
 *
 * The tooltip is the native `title` attribute, which is the only tooltip this host can serve: it
 * enforces script-src 'self' with no nonce, so nothing a library would ship can load. A title is
 * never the accessible name either, so every one of these buttons carries its own aria-label as
 * well: a title is announced inconsistently by screen readers and is unreachable by touch.
 */

.dxp-row-actions {
    display: flex;
    gap: 0.25rem;
}

/* Square, so the control's width is decided by the glyph rather than by how long the word it
 * replaced was. Both rules override .dxp-button's own at equal specificity, so they have to stay
 * below it in this file. line-height is what stops the inherited body leading making the box a
 * rectangle a little taller than it is wide. */
.dxp-icon-button {
    justify-content: center;
    padding: 0.5625rem;
    min-width: 2.25rem;
    line-height: 1;
}

/* ── Page actions ────────────────────────────────────────────────────── */

/* The action row the docs shell puts above every page that publishes a Markdown variant. Anchored
 * at the top of the content column rather than fixed, because the shell stacks below 60rem and a
 * fixed control would then sit over the page it belongs to.
 *
 * flex-wrap is what keeps the promise at the narrow layout: the content column loses its max-width
 * there, and a row that cannot wrap would push a second action off the edge the day one is added. */
.dxp-page-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.5rem;
    margin: 0 0 1.5rem;
}

/* ── The split button ────────────────────────────────────────────────── */

/* One action visible, the rest behind a caret. What this buys is a row that stays a fixed width as
 * the site grows machine-readable affordances: it held five loose buttons before, all weighted the
 * same, above every page heading.
 *
 * A native details/summary disclosure, which is the only kind available here: the docs surface
 * declares no render mode, so there is no circuit to raise an event on, and script-src 'self' rules
 * out anything a library would ship. The browser owns the open state, so the menu works for a reader
 * whose JavaScript never arrived, and nothing has to be re-bound after an enhanced navigation. */
.dxp-split-button {
    display: inline-flex;
}

/* The two halves read as one control: the seam is a single shared border rather than two, so the
 * pair is the width of one button plus a caret rather than of two buttons with a gap. */
.dxp-split-button__action {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

/* Positioned, because the menu below is absolute and anchors to this element. Anchoring to
 * .dxp-split-button instead would right-align the menu to the primary action's right edge, which is
 * the same edge only while the caret is the last thing in the row. */
.dxp-split-button__disclosure {
    position: relative;
    display: flex;
}

/* list-style and the WebKit pseudo-element both have to go: a summary renders a disclosure triangle
 * by default, and the two vendors hide it by different mechanisms. Left alone, the caret sits beside
 * a second, differently drawn arrow. */
.dxp-split-button__toggle {
    justify-content: center;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
    margin-left: -1px;
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    cursor: pointer;
    list-style: none;
}

.dxp-split-button__toggle::-webkit-details-marker {
    display: none;
}

.dxp-split-button__caret {
    width: 1rem;
    height: 1rem;
}

/* The caret reports the state the summary already announces, for the reader who can see it rather
 * than hear it. Keyed on the details element's own open attribute, so the browser drives it and no
 * script has to keep a class in step. */
.dxp-split-button__disclosure[open] .dxp-split-button__caret {
    transform: rotate(180deg);
}

/* ── The action menu ─────────────────────────────────────────────────── */

/* Absolute rather than in flow, so opening the menu overlays the page instead of pushing the heading
 * down the screen. Right-aligned because the row is, and a left-aligned panel would hang off the
 * content column at the narrow layout.
 *
 * Reuses the search typeahead's panel treatment (border, radius, raised background, shadow) rather
 * than introducing tokens of its own: the dark ramp is declared twice in this file, so every new
 * token is a second edit somebody has to remember. */
.dxp-menu {
    position: absolute;
    z-index: 20;
    top: calc(100% + 0.25rem);
    right: 0;
    min-width: 13rem;
    padding: 0.25rem;
    border: 1px solid var(--dxp-border-strong);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-raised);
    box-shadow: 0 0.75rem 1.75rem rgba(0, 0, 0, 0.18);
}

/* Deliberately not .dxp-button: an item in a menu is a row, not a bordered control, and the shared
 * button rules would draw a box inside a box for every entry. The reset covers both tags the menu
 * holds, since a button brings a background, a border and a centred font the anchors do not.
 *
 * This rule has to stay above .dxp-copy-markdown, whose display: none it is overridden by at equal
 * specificity. CopyMarkdownActionTests pins the ordering. */
.dxp-menu__item {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 0.4375rem 0.625rem;
    border: 0;
    border-radius: var(--dxp-radius-sm);
    background: none;
    color: var(--dxp-fg);
    font-family: inherit;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.4;
    text-align: left;
    white-space: nowrap;
    cursor: pointer;
}

.dxp-menu__item:hover {
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg);
    text-decoration: none;
}

/* The one item in the menu that needs a script, and therefore the one that has to be hidden until
 * there is a script. The View as Markdown link outside the menu already serves a reader with no
 * JavaScript, and a button that visibly does nothing is worse than no button: the reader cannot tell
 * whether they misread it or the site is broken.
 *
 * Keyed on dxp-copy-md rather than on the shared dxp-js the theme toggle and the search hint use, and
 * that is the whole safety of the gate. theme.js sets dxp-js from the head, so a page where only
 * docs-copy-markdown.js failed to load would carry it and would reveal a button nothing can drive.
 * That file sets dxp-copy-md itself, last, after its listener is registered.
 *
 * This rule has to stay below both .dxp-button and .dxp-menu__item, whose display it overrides at
 * equal specificity, so source order is the whole of the discrimination. CopyMarkdownActionTests pins
 * the ordering against both, because a rule hoisted above either stops hiding anything and nothing
 * else notices. .dxp-button is still one of them: the button carried those classes before it moved
 * into the menu, and a tidy-up that put them back would reintroduce the same collision. */
.dxp-copy-markdown {
    display: none;
}

:root.dxp-copy-md .dxp-copy-markdown {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}

/* The outcome, on the control the reader just pressed. Never the only report of it: the live region
 * beside the button carries the same result in words, because a colour announces nothing. Both reuse
 * the method-verb ramp rather than introducing tokens of their own, for the reason the status page
 * gives: the dark ramp is declared twice in this file and every new token is a second edit somebody
 * has to remember.
 *
 * Colour alone now that the control is a menu row: it carries no border of its own to recolour, and
 * the script sets these on the item rather than on anything the menu draws. */
.dxp-copy-markdown--copied {
    color: var(--dxp-method-get);
}

.dxp-copy-markdown--failed {
    color: var(--dxp-method-delete);
}

/* ── Copy a snippet that is already on the page ──────────────────────── */

/* Hidden until portal-copy-snippet.js has registered its listener, exactly like the Markdown copier
 * above and keyed on its own class for the same reason: theme.js sets the shared dxp-js from the
 * head, so a page where only this file failed to load would carry it and would show a button nothing
 * can drive. The snippet itself is always rendered, so a developer who never receives that file
 * selects the command by hand rather than losing anything.
 *
 * The readiness class is dxp-copy-snip and the button's is dxp-copy-snippet, and they must stay
 * different words. The gate is written on the html element, so a readiness class spelled the same as
 * the button's would match the display: none rule below and hide the whole document.
 *
 * This rule has to stay below .dxp-button, whose display it overrides at equal specificity, so
 * source order is the whole of the discrimination. PortalCopySnippetStyleTests pins the ordering,
 * because a rule hoisted above it stops hiding anything and nothing else notices. */
.dxp-copy-snippet {
    display: none;
}

:root.dxp-copy-snip .dxp-copy-snippet {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}

/* The outcome, on the control the developer just pressed. Never the only report of it: the live
 * region beside the button carries the same result in words, because a colour announces nothing.
 * Both reuse the method-verb ramp rather than introducing tokens of their own, for the reason the
 * Markdown copier gives. */
.dxp-copy-snippet--copied {
    border-color: var(--dxp-method-get);
    color: var(--dxp-method-get);
}

.dxp-copy-snippet--failed {
    border-color: var(--dxp-method-delete);
    color: var(--dxp-method-delete);
}

/* The heredoc runs to several lines and must keep every one of them: a pre inside the code body
 * would otherwise inherit the block's own margins and sit unevenly inside it. */
.dxp-code__body pre {
    margin: 0;
    white-space: pre;
}

/* ── Resource cards ──────────────────────────────────────────────────── */

.dxp-cards {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: 0.875rem;
}

.dxp-card {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    height: 100%;
    padding: 1rem 1.125rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-lg);
    background: var(--dxp-bg-raised);
    color: var(--dxp-fg);
}

/* Scoped to the anchor form on purpose. A hover affordance is a promise that pressing the card does
 * something, and not every card is a link: a client library card holds a facts list and its own
 * action, so an unscoped rule would light the whole card up under the cursor and lead nowhere. */
a.dxp-card:hover {
    border-color: var(--dxp-accent);
    text-decoration: none;
}

/* A facts list inside a card is not the same object as a facts list on a page.
 *
 * The standalone list is spaced to be read after running prose and sized for a full content column;
 * inside a 19rem card track its 9rem minimum term column leaves the value column too narrow to hold
 * a package id on one line, and its top margin opens a gap wider than the card's own padding. Both
 * numbers are relative to the shared rule rather than replacing the reasoning behind it: the row gap
 * still exceeds the leading inside a value, which is what keeps two wrapped rows from merging into
 * one block of text. */
.dxp-card .dxp-facts {
    grid-template-columns: minmax(4.5rem, max-content) 1fr;
    gap: 0.625rem 1rem;
    margin-top: 0.75rem;
}

/* The card's one action, pushed to the foot so cards carrying different amounts of prose still line
 * their links up. Deliberately not .dxp-card__meta, which is muted: that class is for a trailing
 * detail nobody presses, and this is a link. */
.dxp-card__action {
    margin-top: auto;
    padding-top: 0.75rem;
    font-size: 0.8125rem;
    font-weight: 600;
}

/* The name and its optional chip share a line, and the chip drops below the name rather than
 * squeezing it when the card is narrow. */
.dxp-card__heading {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.375rem;
}

/* overflow-wrap is the backstop rather than the mechanism: resource names are humanized before they
 * reach here, so a name normally wraps between its words. This is what catches the one that cannot,
 * a single unbroken token longer than the track. break-word rather than anywhere on purpose:
 * anywhere collapses the element's min-content width, which lets the grid track shrink around a
 * mid-word break even when there was room to keep the word whole. */
.dxp-card__title {
    font-weight: 600;
    overflow-wrap: break-word;
    min-width: 0;
}

/* The card's leading mark, where one is rendered.
 *
 * Sized against the title rather than in absolute units, because the two blueprint catalogs render
 * that title as different elements (a heading on the runner's list, a span inside the anchor on the
 * documentation one) and the mark has to track whichever it sits in. It states the accent rather
 * than inheriting the title's colour: a mark in the body colour is a smudge beside the words rather
 * than something a reader picks a card out by.
 *
 * aria-hidden at every call site. What the mark stands for is the card's category, which the group
 * heading above it already names in words. */
.dxp-card__icon {
    margin-right: 0.4375rem;
    color: var(--dxp-accent);
    font-size: 0.875em;
}

.dxp-card__description {
    font-size: 0.8125rem;
    color: var(--dxp-fg-muted);
    overflow-wrap: break-word;
}

.dxp-card__meta {
    font-size: 0.8125rem;
    color: var(--dxp-fg-faint);
    margin-top: auto;
    padding-top: 0.25rem;
}

/* A card for a flow this merchant's account does not reach.
 *
 * Muted rather than removed, and still a link. The public documentation site publishes the whole
 * blueprint catalog to anyone, so hiding the card would protect nothing a crawler can already read
 * and would strand a developer who followed a link from those pages. What the card stops being is
 * the obvious next thing to press.
 *
 * The colour is not the whole of the state, deliberately: .dxp-card__unavailable below says it in
 * words under the chips, because a reader who cannot see the muting gets nothing from it. */
.dxp-card--not-runnable {
    border-style: dashed;
    opacity: 0.75;
}

a.dxp-card--not-runnable:hover {
    border-color: var(--dxp-border-strong);
}

/* The sentence under a not-runnable card's chips.
 *
 * Mounted on every card and carrying dxp-hidden on most, per this host's DOM-stability rule, so its
 * text is empty rather than merely hidden when there is nothing to say: a display:none element still
 * carries its words into the accessibility tree. */
.dxp-card__unavailable {
    font-size: 0.8125rem;
    color: var(--dxp-fg-muted);
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--dxp-border);
}

/* ── Use-case cards ──────────────────────────────────────────────────────
 *
 * The landing page's router. Structurally a .dxp-card, but never an anchor: the card's substance is
 * three separate destinations, so the whole-card hover affordance would be a promise it cannot keep.
 * That is why the base .dxp-card hover rule is scoped to a.dxp-card; see the note there.
 *
 * The card is taller than a resource card because it holds a sentence and three rows, so the grid
 * track it sits in has to be the wide one (.dxp-cards--wide) for the link labels to stay on one line.
 */

.dxp-use-case {
    gap: 0.375rem;
}

/* The link rows, pushed to the foot so cards carrying different amounts of prose line their first
 * link up with each other. Same reasoning as .dxp-card__action, which this replaces for a card with
 * more than one action. */
.dxp-use-case__links {
    list-style: none;
    margin: auto 0 0;
    padding: 0.625rem 0 0;
    border-top: 1px solid var(--dxp-border);
    display: flex;
    flex-direction: column;
    gap: 0.4375rem;
}

/* The role chip and its link share a line, and the link wraps under the chip rather than squeezing it
 * when the track is narrow. align-items: baseline would drop the chip onto the link's first line and
 * leave it hanging beside a two-line label, so the chip aligns to the top of the row instead. */
.dxp-use-case__link {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0.4375rem;
    font-size: 0.8125rem;
    line-height: 1.35;
}

/* The chip states the kind of document rather than a status, so it keeps the neutral chip's colours
 * and only fixes its width. A fixed minimum is what turns five differently-worded roles into a
 * column the eye can scan down, the same trick .dxp-method--sm plays on the operation lists. The
 * width fits "Decision guide", the longest role. */
.dxp-chip--use-case-role {
    min-width: 5.75rem;
    text-align: center;
    flex: none;
}

/* ── Resource groups ─────────────────────────────────────────────────────
 *
 * One heading per category on the landing page. The heading is a sibling of the card grid rather
 * than part of it, so a group with no heading (the fallback an instance running an older build
 * renders) collapses to just its cards.
 */

.dxp-resource-group + .dxp-resource-group {
    padding-top: 1.75rem;
}

.dxp-resource-group__title {
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--dxp-fg-faint);
    margin: 0 0 0.75rem;
}

/* Guide cards and resource cards carry a full sentence rather than a bare count, so they need a
 * wider track. The modifier sits on the grid rather than on the card because the track size is what
 * has to change: a wider card inside a 13rem track would simply overflow it. */
.dxp-cards--wide {
    grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
}

/* ── Operation lists ─────────────────────────────────────────────────── */

.dxp-tag {
    padding-top: 2.5rem;
    scroll-margin-top: calc(var(--dxp-header-height) + 1rem);
}

.dxp-tag__title {
    font-size: 1.25rem;
    margin-bottom: 0.75rem;
}

/* The category reads as an eyebrow over the resource name, in the same treatment the landing page
 * gives its category headings, so a reader who arrives from a card sees the grouping they left. */
.dxp-tag__category {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--dxp-fg-faint);
    margin: 0 0 0.25rem;
}

/* The description sits between the name and the operation list, so the title keeps only a small gap
 * beneath it when the description is present. */
.dxp-tag__title:has(+ .dxp-tag__description:not(.dxp-hidden)) {
    margin-bottom: 0.375rem;
}

.dxp-tag__description {
    color: var(--dxp-fg-muted);
    margin: 0 0 0.875rem;
    max-width: 48rem;
}

.dxp-operations {
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius);
    overflow: hidden;
}

.dxp-operations__item + .dxp-operations__item {
    border-top: 1px solid var(--dxp-border);
}

.dxp-operation {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.875rem 1.125rem;
    color: var(--dxp-fg);
}

.dxp-operation:hover {
    background: var(--dxp-bg-subtle);
    text-decoration: none;
}

.dxp-operation__line {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    flex-wrap: wrap;
}

.dxp-operation__path {
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
    overflow-wrap: anywhere;
}

.dxp-operation__summary {
    font-size: 0.9375rem;
}

.dxp-operation__requirement {
    font-size: 0.8125rem;
    color: var(--dxp-fg-faint);
}

.dxp-endpoint {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    flex-wrap: wrap;
    margin: 0;
    padding: 0.75rem 1rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
}

.dxp-endpoint__path {
    font-family: var(--dxp-font-mono);
    overflow-wrap: anywhere;
}

.dxp-requirement {
    margin: 0.75rem 0 0;
    font-size: 0.875rem;
    color: var(--dxp-fg-muted);
}

/* ── Method and status badges ────────────────────────────────────────── */

.dxp-method {
    display: inline-block;
    min-width: 3.5rem;
    padding: 0.125rem 0.4375rem;
    border-radius: 4px;
    background: var(--dxp-bg-subtle);
    border: 1px solid var(--dxp-border-strong);
    color: var(--dxp-fg-muted);
    font-family: var(--dxp-font-mono);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-align: center;
}

.dxp-method--get { color: var(--dxp-method-get); border-color: currentColor; }
.dxp-method--post { color: var(--dxp-method-post); border-color: currentColor; }
.dxp-method--put { color: var(--dxp-method-put); border-color: currentColor; }
.dxp-method--patch { color: var(--dxp-method-patch); border-color: currentColor; }
.dxp-method--delete { color: var(--dxp-method-delete); border-color: currentColor; }

/* Compact variant, for the sidebar, where the chip sits beside 0.9rem body text in a 20rem column
 * rather than heading an operation. Size only: it sets no colour, so the verb modifiers above still
 * decide what a GET looks like and the two surfaces stay in step by construction. min-width fits
 * DELETE, the longest verb, so the chips still form a column the eye can scan down. */
.dxp-method--sm {
    min-width: 2.875rem;
    padding: 0.0625rem 0.3125rem;
    font-size: 0.5625rem;
}

.dxp-status {
    display: inline-block;
    padding: 0.0625rem 0.4375rem;
    border-radius: 4px;
    border: 1px solid currentColor;
    font-family: var(--dxp-font-mono);
    font-size: 0.75rem;
    font-weight: 700;
}

.dxp-status--2xx { color: var(--dxp-method-get); }
.dxp-status--3xx { color: var(--dxp-method-put); }
.dxp-status--4xx { color: var(--dxp-method-put); }
.dxp-status--5xx { color: var(--dxp-method-delete); }
.dxp-status--other { color: var(--dxp-fg-muted); }

/* ── Chips ───────────────────────────────────────────────────────────── */

.dxp-chips {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.25rem;
}

.dxp-chip {
    display: inline-block;
    padding: 0.0625rem 0.4375rem;
    border-radius: 999px;
    background: var(--dxp-bg-subtle);
    border: 1px solid var(--dxp-border);
    color: var(--dxp-fg-faint);
    font-size: 0.6875rem;
    white-space: nowrap;
}

.dxp-chip--required {
    color: var(--dxp-highlight);
    border-color: currentColor;
}

/* A settled payload contract, and an event type registered but not yet publishing. Both read as
 * status rather than as decoration, so each takes a colour of its own rather than sharing the
 * neutral chip: the difference between "you can build against this" and "this will not fire yet" is
 * the single most consequential thing on the event reference. */
.dxp-chip--documented {
    color: var(--dxp-method-get);
    border-color: currentColor;
}

.dxp-chip--pending {
    color: var(--dxp-method-put);
    border-color: currentColor;
}

.dxp-chip--deprecated {
    color: var(--dxp-method-put);
    border-color: currentColor;
}

/* Resource chips. A variant this stylesheet has no rule for keeps the neutral base chip, so a token
 * added upstream renders as a plain chip here rather than as an unstyled one or none at all. Both
 * tokens take a colour from the shared method palette, which is themed in light and dark already. */
.dxp-chip--preview {
    color: var(--dxp-method-post);
    border-color: currentColor;
}

.dxp-chip--new {
    color: var(--dxp-method-get);
    border-color: currentColor;
}

.dxp-chip--approved {
    color: var(--dxp-method-get);
    border-color: currentColor;
}

.dxp-chip--partial {
    color: var(--dxp-method-put);
    border-color: currentColor;
}

.dxp-chip--refused {
    color: var(--dxp-method-delete);
    border-color: currentColor;
}

/* A blueprint step whose call worked and whose payment did not. Not the red a failed request takes,
 * because a decline is an outcome a developer is there to observe rather than a request to debug, and
 * not the amber of "Your turn" either, which asks for something a decline does not. The PATCH purple
 * is the one palette colour no run state uses yet. No new token, for the reason .dxp-step__result
 * gives. */
.dxp-chip--declined {
    color: var(--dxp-method-patch);
    border-color: currentColor;
}

/* A chip's leading mark.
 *
 * It states no colour, deliberately: the glyph inherits whichever colour the chip's own variant set,
 * so one declaration decides both and a variant cannot leave its mark behind in the neutral grey.
 * Sized relative to the chip rather than in absolute units, because it is a mark rather than a
 * second word and has to stay below the label at whatever size the label ends up.
 *
 * Every call site carries aria-hidden. The label beside it already says what it says, and a screen
 * reader announcing a private-use codepoint says nothing at all. */
.dxp-chip__icon {
    margin-right: 0.3125rem;
    font-size: 0.9em;
}

/* ── Blueprint catalog chips ─────────────────────────────────────────────
 *
 * The one place on this host a chip carries a tint rather than a border colour alone, and the
 * exception is what the catalog cards are for. A card is a title, a summary and its chips, so with
 * the neutral chip a grid of them reads as one block of grey and nothing on a card separates two
 * quite different things: a guided integration the runner performs end to end, and one that stops
 * partway and hands the rest back to the developer.
 *
 * The tint is a tenth of the token over the card's own background, which is faint on purpose. It is
 * there to tell one chip from the next at a glance, not to compete with the title above it.
 *
 * Each variant names its token twice rather than mixing `currentColor`. That would read better and
 * is not reliable: `currentColor` inside `color-mix` resolves inconsistently across the engines that
 * reach this host, and the failure is a chip whose tint is simply absent with nothing to report it.
 * Both halves take a `--dxp-*` token rather than a literal, which is what keeps the light and dark
 * ramps in step: every one of these is declared in both.
 */

/* The step and API-call counts: the structural fact about a blueprint, in the instance's own brand. */
.dxp-chip--blueprint-meta {
    color: var(--dxp-accent);
    border-color: color-mix(in srgb, var(--dxp-accent) 30%, transparent);
    background: color-mix(in srgb, var(--dxp-accent) 10%, transparent);
}

/* Every step runs here, so the developer presses one button and reads the answer. */
.dxp-chip--blueprint-runs-all {
    color: var(--dxp-method-get);
    border-color: color-mix(in srgb, var(--dxp-method-get) 30%, transparent);
    background: color-mix(in srgb, var(--dxp-method-get) 10%, transparent);
}

/* Some steps are the developer's own: a redirect in a payer's browser, a webhook arriving at their
 * service. Deliberately not the amber `--dxp-method-put` that `.dxp-chip--partial` takes, which
 * reads as a warning. A blueprint that hands work back is not a degraded one. */
.dxp-chip--blueprint-runs-some {
    color: var(--dxp-teal);
    border-color: color-mix(in srgb, var(--dxp-teal) 30%, transparent);
    background: color-mix(in srgb, var(--dxp-teal) 10%, transparent);
}

/* What a blueprint touches, on the documentation catalog. The quietest of the four and carrying no
 * mark of its own, because a card holds up to three of them: they qualify the chip above rather
 * than competing with it. */
.dxp-chip--blueprint-product {
    color: var(--dxp-fg-muted);
    border-color: color-mix(in srgb, var(--dxp-fg-muted) 25%, transparent);
    background: color-mix(in srgb, var(--dxp-fg-muted) 6%, transparent);
}

/* ── Reference tables ────────────────────────────────────────────────────
 *
 * The scroll container is a separate element rather than `display: block` on
 * the table itself (which is what .dxp-fields does). A table forced to block
 * loses its row/column semantics for assistive technology, and the testing
 * page's tables are wide enough to need real header association. Keeping the
 * table a table and scrolling the wrapper preserves both.
 */

.dxp-table-scroll {
    overflow-x: auto;
    margin: 0.75rem 0 0;
}

.dxp-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}

.dxp-table th,
.dxp-table td {
    padding: 0.4375rem 0.75rem 0.4375rem 0;
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-table thead th {
    color: var(--dxp-fg-faint);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    white-space: nowrap;
    border-bottom-color: var(--dxp-border-strong);
}

.dxp-table tbody th {
    font-weight: 600;
    white-space: nowrap;
}

.dxp-table code {
    white-space: nowrap;
}

.dxp-table__note {
    display: block;
    margin-top: 0.25rem;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
}

.dxp-chip--vendored {
    color: var(--dxp-fg-muted);
}

/* ── Error reference ─────────────────────────────────────────────────── */

/* A worked response body. Scrolls inside itself rather than widening the page: several of the
   example bodies are wider than a phone. */
.dxp-code {
    margin: 0.75rem 0 0;
    padding: 0.875rem 1rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
    line-height: 1.5;
    overflow-x: auto;
}

.dxp-code code {
    background: none;
    border: 0;
    padding: 0;
    font-size: inherit;
    white-space: pre;
}

/* Module jump list above the code tables. Wraps rather than scrolling, because the number of
   modules is a property of the instance and can be large. */
.dxp-jump {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin: 1rem 0 0;
}

.dxp-jump__link {
    padding: 0.1875rem 0.5625rem;
    border: 1px solid var(--dxp-border);
    border-radius: 999px;
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg-muted);
    font-size: 0.75rem;
    white-space: nowrap;
}

.dxp-jump__link:hover {
    border-color: var(--dxp-accent);
    color: var(--dxp-accent);
    text-decoration: none;
}

/* ── Field tables ────────────────────────────────────────────────────── */

.dxp-fields {
    width: 100%;
    border-collapse: collapse;
    margin: 0.75rem 0 0;
    font-size: 0.9rem;
    display: block;
    overflow-x: auto;
}

.dxp-fields th,
.dxp-fields td {
    text-align: left;
    vertical-align: top;
    padding: 0.5rem 0.75rem 0.5rem 0;
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-fields th {
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--dxp-fg-faint);
}

.dxp-fields__name {
    white-space: nowrap;
}

.dxp-fields__type {
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
    color: var(--dxp-fg-muted);
}

.dxp-fields__description {
    color: var(--dxp-fg-muted);
}

/* A table row carrying an id is a fragment target: the portal overview's top error codes card links
 * straight at a row of the error catalog, and the decline and testing references anchor their rows
 * the same way. The offset is the one .dxp-section[id] carries, for the same reason: without it the
 * browser scrolls the row to the top edge of the viewport, the sticky header paints over it, and the
 * first row a reader can see is a different code from the one they clicked.
 *
 * Keyed on the presence of an id, as .dxp-section[id] is, so a row that becomes a fragment target
 * cannot be linked to before somebody remembers to give it the offset. */
.dxp-fields tr[id],
.dxp-table tr[id] {
    scroll-margin-top: calc(var(--dxp-header-height) + 1rem);
}

/* The row the link landed on, while it is the fragment target. A catalog group runs to dozens of
 * rows of near-identical codes, so the offset alone lands the reader in the right place without
 * telling them which row it was.
 *
 * Painted on the cells rather than the row, because a row's background under border-collapse is
 * drawn inconsistently across engines. --dxp-accent-subtle is a translucent mix of the accent, so
 * the tint follows the instance's brand and each theme's own accent. Stylesheet only: the page is
 * statically rendered and the host's script-src is 'self', so there is no script to add a class.
 * The cost is that an enhanced navigation never sets :target (it records the address with
 * history.pushState), so a link that should land on a marked row carries data-enhance-nav="false",
 * as the top error codes card's link does. */
.dxp-fields tr[id]:target > *,
.dxp-table tr[id]:target > * {
    background: var(--dxp-accent-subtle);
}

.dxp-response + .dxp-response {
    margin-top: 1.75rem;
}

.dxp-response__title {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    font-size: 1rem;
    font-weight: 600;
}

.dxp-plain-list {
    margin: 0.75rem 0 0;
    padding-left: 1.125rem;
    color: var(--dxp-fg-muted);
}

/* One glossary term: its heading, its definition, and the optional link to the page that treats it
 * in full.
 *
 * A block with its own id rather than a bare heading and paragraph, because search results and
 * cross-links address the term rather than the group it sits in, and an anchor on the heading alone
 * scrolls the definition off the top on a short viewport. The scroll margin matches what
 * .dxp-section[id] carries for the same reason it carries it: the docs shell has a sticky header, and
 * an anchor landing under it looks like a link that did nothing.
 *
 * The left rule is what makes a run of terms read as a list without markers. Definitions are full
 * sentences, so a marker would sit beside prose rather than beside a label, and the rule does the
 * same grouping work without competing with the heading. */
.dxp-term {
    margin-top: 1.5rem;
    padding-left: 1rem;
    border-left: 2px solid var(--dxp-border);
    scroll-margin-top: calc(var(--dxp-header-height) + 1rem);
}

.dxp-term__definition {
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-muted);
}

.dxp-term__link {
    margin: 0.5rem 0 0;
    font-size: 0.875rem;
}

/* The blueprint runner's and the scenario editor's run log.
 *
 * Deliberately not .dxp-plain-list, which only indents and leaves the browser's own marker in place.
 * Every line here already opens with a state chip and a numbered step title, so a marker beside them is
 * a third piece of leading furniture and, on an ordered list, a number that disagrees with the step
 * number in the title. list-style: none is the whole reason this rule exists rather than reusing the
 * one above; the zero padding is what makes the chip start at the section's own left edge, so a
 * reader's eye runs down one column of states.
 *
 * Rows rather than a bulleted paragraph: an entry is a chip, a title and a sentence, and without the
 * row gap a two-line message runs into the next entry's chip with nothing separating the two. */
.dxp-run-log {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin: 0.75rem 0 0;
    padding-left: 0;
    list-style: none;
    color: var(--dxp-fg-muted);
}

.dxp-run-log > li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
}

/* ── Notices ─────────────────────────────────────────────────────────── */

.dxp-notice {
    margin-top: 2rem;
    padding: 1.25rem 1.375rem;
    border: 1px solid var(--dxp-border-strong);
    border-left: 3px solid var(--dxp-highlight);
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-subtle);
}

.dxp-notice__title {
    font-size: 1.0625rem;
    margin-bottom: 0.5rem;
}

.dxp-notice p {
    margin: 0.5rem 0 0;
    color: var(--dxp-fg-muted);
}

/* "That guide is part of the product documentation" is information, not a fault: the address was
 * right and the guide exists, it just lives in the application. The neutral accent says so, where
 * the default highlight would read as an error. */
.dxp-notice--not-published {
    border-left-color: var(--dxp-accent);
}

/* The one page every refused sign-in lands on. It says the same thing whatever the reason, so it
 * gets the neutral accent rather than the fault highlight: for the caller this is a statement about
 * their access, not a report that something broke. */
.dxp-notice--denied {
    border-left-color: var(--dxp-accent);
}

/* "Nothing here yet" on a surface that is working correctly. A merchant with no webhook traffic has
 * hit no fault, so the neutral accent again, and the empty state is a plain paragraph rather than a
 * titled notice: a heading would give an absence more weight than the content it replaces. */
.dxp-notice--empty {
    border-left-color: var(--dxp-accent);
}

/* A write the developer asked for and did not get: a sample the endpoint refused, an add the rules
 * rejected, an operation that could not run. The fault highlight, because unlike a denied notice
 * this is a report that something broke rather than a statement about the caller's access.
 *
 * It restates the base rule's colour rather than relying on it. The outcome notice picks exactly one
 * of this and .dxp-notice--empty on every render, so a failure that carried no modifier at all would
 * be one edit to the base away from looking like a success, and nothing would fail. */
.dxp-notice--fault {
    border-left-color: var(--dxp-highlight);
}

/* The reference on this page is real and may be a few minutes behind, which is a statement about
 * the copy rather than a fault the reader has hit: the neutral accent, for the reason
 * .dxp-notice--unavailable takes it. It restates the colour rather than inheriting the base rule's
 * highlight, and that is the whole point of the rule existing: without it a stale banner would carry
 * the fault colour and read as "this page is broken" on a page whose every endpoint is accurate. */
.dxp-notice--stale {
    border-left-color: var(--dxp-accent);
}

/* ── Credentials call to action ──────────────────────────────────────── */

/* Closes every quickstart. Deliberately not a .dxp-notice: a notice reports a state the reader
 * cannot act on, and this panel exists to be acted on, so it carries a full border and a button
 * rather than the notices' left rule and muted paragraph. */
.dxp-cta {
    margin-top: 2.5rem;
    padding: 1.5rem;
    border: 1px solid var(--dxp-border-strong);
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-subtle);
}

.dxp-cta__title {
    font-size: 1.0625rem;
    margin: 0 0 0.5rem;
}

.dxp-cta__body,
.dxp-cta__fallback {
    margin: 0;
    color: var(--dxp-fg-muted);
}

.dxp-cta__actions {
    margin: 1rem 0 0;
}

/* ── Authenticated portal ────────────────────────────────────────────── */

/* The portal shell has no sidebar, so its content column centres in the body instead of sitting
 * beside one. */
.dxp-main--portal {
    margin: 0 auto;
}

/* Facts about the current session: a two-column list of term and value. Grid rather than float,
 * so a long value wraps under its own column instead of under the term.
 *
 * The size and the row gap are stated here rather than inherited, and both were wrong by omission.
 * Values took the 16px body size and sat 0.5rem apart, which holds only while every value is a
 * single line: one line is separated from the next row by its own leading, so the small gap reads
 * as a gap. A value that wraps has internal line spacing larger than the gap between rows, and the
 * rows then merge into one block of text with no boundary a reader can see. The list is also set a
 * step down from body copy for the reason a table is: it is reference material scanned by term,
 * not prose read in order. */
.dxp-facts {
    display: grid;
    grid-template-columns: minmax(9rem, max-content) 1fr;
    gap: 1rem 2rem;
    margin: 1.25rem 0 0;
    font-size: 0.875rem;
    line-height: 1.65;
}

.dxp-facts__term {
    color: var(--dxp-fg-muted);
    font-weight: 600;
}

.dxp-facts__value {
    margin: 0;
    font-family: var(--dxp-font-mono);
    word-break: break-word;
}

/* The prose variant, for a list whose values are sentences rather than identifiers.
 *
 * Monospace is right for every other consumer of this list: an id, a timestamp, an endpoint, a
 * duration, all values a reader compares character by character. It is wrong for a paragraph, which
 * it renders as something that looks like a payload rather than something to read, and it carries
 * no measure, so the sentence sets across the whole content column. The body face comes back, the
 * value is capped at the same reading measure the rest of the running prose uses, and the emphasis
 * inverts: the term becomes the foreground label and the explanation is muted, which is the
 * hierarchy a reader scanning for the row that applies to them actually wants.
 *
 * word-break is reset with it. `break-word` may split mid-word, which is what an unbroken id needs
 * and what a sentence must never do; `overflow-wrap` breaks only where nothing else will fit. */
.dxp-facts--prose .dxp-facts__term {
    color: var(--dxp-fg);
}

/* The mark on a prose fact's term.
 *
 * A fixed inline width is the load-bearing part rather than the colour: the glyphs differ in width
 * (a key is narrow, a flask is not), so without it the terms below one another start at different
 * offsets and the column stops reading as a column. The colour is the eyebrow token, which is the
 * one accent on this host already chosen to clear AA as small text in both ramps.
 *
 * Only the prose variant renders one. Every other fact list on this host holds field names against
 * identifiers, and a glyph per field there is noise. */
.dxp-facts__icon {
    display: inline-block;
    width: 1em;
    margin-right: 0.5rem;
    color: var(--dxp-teal);
    font-size: 0.875em;
    text-align: center;
}

.dxp-facts--prose .dxp-facts__value {
    max-width: var(--dxp-measure);
    color: var(--dxp-fg-muted);
    font-family: var(--dxp-font);
    word-break: normal;
    overflow-wrap: break-word;
}

@media (max-width: 40rem) {
    /* One column, so the term sits directly above its own value and a row gap would only push the
     * two halves of one row apart. The separation moves onto the value's bottom margin, which is
     * the only place that can express "between rows" once the grid has stopped expressing it. */
    .dxp-facts {
        grid-template-columns: 1fr;
        gap: 0.25rem;
    }

    .dxp-facts__value {
        margin-bottom: 1rem;
    }
}

/* ── Portal navigation ───────────────────────────────────────────────── */

/* The bar under the header on every authenticated page. It is a full-bleed strip with an inner
 * container, matching .dxp-header__inner, so the rule and the background run the width of the
 * viewport while the entries line up with the wordmark above them and the content below.
 *
 * Deliberately not sticky, unlike the header and the docs sidebar. It wraps to as many rows as the
 * entries need (see below), and a sticky element whose height depends on the viewport width would
 * eat a third of a phone screen on every page. The header above it stays sticky, and it carries the
 * link back here. */
.dxp-portal-nav {
    background: var(--dxp-bg-subtle);
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-portal-nav__inner {
    max-width: var(--dxp-shell-width);
    margin: 0 auto;
    padding: 0 var(--dxp-shell-gutter);
}

/* Wrapping rather than a horizontal scroller, which is the other way a ten-entry bar is usually
 * built. A scroller keeps one row at any width by putting the later entries off-screen behind a
 * gesture, and the entries this navigation exists for are the ones nobody could find: the endpoint
 * list and the request log, which sit at the end. Wrapping costs a second row on a phone and leaves
 * every entry visible without scrolling sideways. */
.dxp-portal-nav__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 0.25rem;
    list-style: none;
    margin: 0;
    padding: 0;
}

.dxp-portal-nav__item {
    display: flex;
}

/* The active entry is marked with an underline as well as a colour, so the mark survives a reader
 * who cannot separate the accent from the muted foreground. The border is on every entry and
 * transparent until it is the current one: a border applied only to the current entry would make
 * that entry a pixel taller than its neighbours and shift the row. */
.dxp-portal-nav__link {
    display: inline-flex;
    align-items: center;
    padding: 0.75rem 0.625rem;
    border-bottom: 2px solid transparent;
    color: var(--dxp-fg-muted);
    font-size: 0.9rem;
    font-weight: 500;
    line-height: 1.3;
    white-space: nowrap;
}

.dxp-portal-nav__link:hover {
    color: var(--dxp-fg);
    border-bottom-color: var(--dxp-border-strong);
    text-decoration: none;
}

.dxp-portal-nav__link--current {
    color: var(--dxp-accent);
    border-bottom-color: var(--dxp-accent);
    font-weight: 650;
}

@media (max-width: 40rem) {
    /* Tighter on a narrow viewport, because the same ten entries now wrap onto three or four rows
     * and the bar is competing with the page for the fold. The entries stay full-size targets
     * vertically; only the horizontal padding and the type come down. */
    .dxp-portal-nav__link {
        padding: 0.625rem 0.5rem;
        font-size: 0.8125rem;
    }
}

/* ── Merchant switcher ───────────────────────────────────────────────── */

/* The strip under the portal navigation on a reseller session: which merchant the session is acting
 * for, and the control that moves it. Rendered by PortalMerchantSwitcher in PortalLayout and nowhere
 * else; a merchant session renders none of it.
 *
 * The same full-bleed strip and inner container as .dxp-portal-nav, so the control lines up with the
 * entries above it and the content below. Not sticky, for the reason the navigation gives. */
.dxp-merchant-switcher {
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-merchant-switcher__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    max-width: var(--dxp-shell-width);
    margin: 0 auto;
    padding: 0.5rem var(--dxp-shell-gutter);
}

/* One row of label, select and button, wrapping rather than overflowing on a phone: a long merchant
 * name is the thing most likely to push the button off the edge. */
.dxp-merchant-switcher__form,
.dxp-merchant-switcher__current {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
}

.dxp-merchant-switcher__label {
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
    font-weight: 600;
}

/* .dxp-filter__control sets the look; this only lets a long name shrink instead of widening the row
 * past the viewport. */
.dxp-merchant-switcher__control {
    max-width: 100%;
}

.dxp-merchant-switcher__submit {
    padding: 0.4375rem 0.875rem;
    font-size: 0.875rem;
}

.dxp-merchant-switcher__name {
    color: var(--dxp-fg);
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* The outcome notice sits in the same row and drops to its own when the row is full. The notice's
 * page-level margin and padding would otherwise make a strip meant to be one line tall several lines
 * tall for as long as the notice shows. */
.dxp-merchant-switcher__inner .dxp-notice {
    margin: 0;
    padding: 0.4375rem 0.875rem;
    font-size: 0.875rem;
}

/* ── Webhook delivery log ────────────────────────────────────────────── */

/* The narrowing controls on the delivery list. A plain GET form rather than anything interactive:
 * the authenticated area can hold interactive islands, but this page declares no render mode, so
 * submitting navigates to the same page with the narrowing in the query string. That also makes any
 * view of the log a link a developer can share or bookmark. */
.dxp-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
    margin: 1.25rem 0 0;
}

.dxp-filter__field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.dxp-filter__label {
    color: var(--dxp-fg-faint);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

/* A sentence under a field's label, explaining the field rather than naming it: what the schema says
 * a Try-It parameter means, and the note saying the console will not send one.
 *
 * A paragraph, so without this it arrives with the user agent's 1em margins on both sides. Inside
 * .dxp-filter__field, which is a flex column that already spaces its own children, those margins put
 * the sentence a full line away from both the label above it and the control below it, and the field
 * ends up taller than the space its content needs.
 *
 * A size of its own rather than the inherited 16px, so it reads as help beside a 0.875rem control the
 * way .dxp-field-error reads as a fault. The colour is not stated here: .dxp-muted is what says this
 * is secondary text, and a hint that took a colour too would silently override a caller that wanted a
 * different one. */
.dxp-filter__hint {
    margin: 0;
    font-size: 0.8125rem;
    line-height: 1.5;
}

.dxp-filter__control {
    min-width: 12rem;
    padding: 0.4375rem 0.625rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg);
    font: inherit;
    font-size: 0.875rem;
}

/* A boolean parameter's row: the checkbox and the value it will send, on one line.
 *
 * It exists because .dxp-filter__control is the wrong rule for a checkbox and there is no way to make
 * it the right one. That rule is a text field: a 12rem min-width, padding, a border and a fill. A
 * native checkbox given it is a lone box, and .dxp-filter__field is a column, so it lands on a row of
 * its own under the description with nothing beside it saying what it is or which way it is set.
 *
 * The row is the flex context the field's own column layout does not provide, and it is what carries
 * .dxp-hidden, so the box and its value can never be in different states. */
.dxp-filter__check {
    display: flex;
    align-items: center;
    gap: 0.4375rem;
}

/* No width, height or padding: those belong to the browser's own checkbox metrics, which track the
 * platform and the reader's zoom. Only the margin is reset, because a user-agent checkbox carries one
 * that would put it out of line with the value beside it, and only the colour is stated, because the
 * default accent is a blue chosen for neither ramp. */
.dxp-filter__checkbox {
    margin: 0;
    accent-color: var(--dxp-accent);
    cursor: pointer;
}

.dxp-filter__checkbox:disabled {
    cursor: default;
}

/* Monospace and the muted foreground, so it reads as the literal that goes on the wire rather than as
 * a caption naming the control. The field's own label is what names it. */
.dxp-filter__check-value {
    color: var(--dxp-fg-muted);
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
}

/* The event-type chooser on the add-an-endpoint form: one checkbox per type a merchant may subscribe
 * to, under one native disclosure per resource. See WebhookEventTypeChecklist for why it is a
 * checkbox group and not a multi-select, and why the browser owns the open state.
 *
 * A fieldset, so a screen reader reads the legend before every box under it and the group is one
 * question rather than several unrelated ones. Its default border, padding and min-inline-size are
 * reset for the reason .dxp-authmode resets them: a fieldset's min-inline-size is min-content, which
 * stops it shrinking inside the flex row .dxp-filter lays its fields out on.
 *
 * flex-basis 100% because it is a group and not a field: the name and URL inputs sit beside each
 * other on that row, and a list of events wedged in beside them would wrap every entry. */
.dxp-checkgroup {
    flex: 1 1 100%;
    margin: 0;
    padding: 0;
    border: 0;
    min-inline-size: 0;
}

/* Says the thing the control it replaced could not: that more than one may be chosen. The legend
 * names the group and this states how it behaves, so it is a paragraph rather than part of the
 * legend, which a screen reader would otherwise repeat before every box. */
.dxp-checkgroup__hint {
    margin: 0.25rem 0 0.5rem;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
}

/* The groups, as one bordered panel rather than loose disclosures, so the chooser reads as a single
 * control sitting under its legend instead of as a run of headings the form happens to end with. */
.dxp-checkgroup__groups {
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    overflow: hidden;
}

.dxp-checkgroup__group + .dxp-checkgroup__group {
    border-top: 1px solid var(--dxp-border);
}

/* The heading row of a resource, in the treatment .dxp-sidebar__summary gives the same element: the
 * user-agent marker is dropped in both spellings and the caret below reports the state instead, so
 * the disclosures on this form and the ones in the documentation sidebar behave alike.
 *
 * list-style on its own is enough on current Gecko and Blink; the ::-webkit-details-marker rule is
 * what covers WebKit, which still draws the triangle regardless. */
.dxp-checkgroup__summary {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    cursor: pointer;
    list-style: none;
}

.dxp-checkgroup__summary::-webkit-details-marker {
    display: none;
}

.dxp-checkgroup__summary:hover {
    background: var(--dxp-bg-subtle);
}

/* A summary is focusable and is none of the three elements the base focus rule near the top of this
 * file names, so the ring is restated rather than inherited. Inset rather than offset, because this
 * one sits inside a panel that clips its own overflow to keep the corner radius. */
.dxp-checkgroup__summary:focus-visible {
    outline: 2px solid var(--dxp-accent);
    outline-offset: -2px;
}

.dxp-checkgroup__group-name {
    color: var(--dxp-fg);
    font-size: 0.875rem;
    font-weight: 600;
}

/* How much is behind a closed group. Muted and set in the tabular figures the rest of this site gives
 * a count, so a column of them does not jitter as the numbers change width. */
.dxp-checkgroup__group-count {
    color: var(--dxp-fg-muted);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
}

/* Pushed to the far end of the row so the carets line up down the panel whatever a resource is
 * called, and keyed on the details element's own open attribute so the browser drives it and no
 * script has to keep a class in step. */
.dxp-checkgroup__caret {
    flex: 0 0 auto;
    margin-left: auto;
    width: 0.875rem;
    height: 0.875rem;
    color: var(--dxp-fg-faint);
}

.dxp-checkgroup__group[open] > .dxp-checkgroup__summary .dxp-checkgroup__caret {
    transform: rotate(90deg);
}

/* Columns that fill the width available rather than a fixed count, because the number of events under
 * a resource is not something this stylesheet can know.
 *
 * The track minimum is min(100%, 18rem) and not a bare 18rem: a bare one is wider than the container
 * on a narrow viewport, and a track wider than the container is exactly the overflow this rule is
 * sized to prevent.
 *
 * There is deliberately no scroll ceiling here. The flat list this replaces needed one and paid for
 * it with a scrolling panel inside a form, which hides its own content and traps a wheel gesture that
 * was meant for the page. A closed group occupies one row, so the disclosures bound the height by
 * themselves and the page scrolls as a page. */
.dxp-checkgroup__options {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
    gap: 0.75rem 1.25rem;
    padding: 0.25rem 0.75rem 0.875rem;
}

/* The whole cell is the label, so the box and every line of text beside it are click targets.
 * .dxp-filter__checkbox supplies the box itself.
 *
 * Two callers now: the webhook event-type chooser this was written for, and the API key scope picker,
 * which is the same shape (a box, a display name, the literal that goes on the wire, and a sentence
 * saying what it covers) and had been setting all four in the uppercase treatment that names a field.
 * The rows below carry no assumption about which panel encloses them.
 *
 * A two-column grid rather than a flex row: the text stacks, so the box has to align to the first
 * line of it rather than to the middle of however many lines a name and a description came to.
 *
 * min-width: 0 is the load-bearing declaration in this rule and the reason the section used to scroll
 * sideways. A grid item's min-width defaults to auto, which is its min-content width, so the longest
 * event type in a group set the track's minimum and pushed the whole container past its box. The
 * overflow-wrap on the type below is the other half: together they let a long literal wrap inside its
 * column instead of widening it. */
.dxp-checkgroup__option {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: 0.5rem;
    min-width: 0;
    color: var(--dxp-fg);
    font-size: 0.875rem;
    cursor: pointer;
}

/* Nudged down onto the first line of the text beside it. A checkbox and a line of text do not share a
 * baseline on their own, and align-items: start alone puts the box against the line's top rather than
 * against its centre. */
.dxp-checkgroup__box {
    margin-top: 0.1875rem;
}

.dxp-checkgroup__text {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    min-width: 0;
}

.dxp-checkgroup__name {
    color: var(--dxp-fg);
}

/* Monospace and muted, in the treatment .dxp-filter__check-value gives a literal: this is the string
 * that gets persisted and dispatched against, and the display name above it is the prose.
 *
 * overflow-wrap: anywhere rather than break-word, because only anywhere is taken into account when
 * the browser computes the element's min-content width. An event type carries no spaces, so under
 * break-word the intrinsic minimum stays the width of the whole literal and the grid track is sized
 * to it whatever min-width says. */
.dxp-checkgroup__type {
    color: var(--dxp-fg-muted);
    font-family: var(--dxp-font-mono);
    font-size: 0.75rem;
    overflow-wrap: anywhere;
}

/* What the event means, when the registry describes it. The page fetched this for as long as the
 * control has existed and rendered it nowhere, so a developer choosing between forty event types had
 * nothing but the name to go on. Capped at the reading measure the rest of the site uses for prose,
 * because a column sized for a literal is wider than a sentence wants to be. */
.dxp-checkgroup__description {
    max-width: var(--dxp-measure);
    color: var(--dxp-fg-muted);
    font-size: 0.75rem;
    line-height: 1.45;
}

/* Why a row is disabled, on the row itself. The API key scope picker is what reaches this: a scope
 * the developer's own account cannot exercise is dimmed with its reason rather than omitted, because
 * a picker that silently dropped a scope leaves somebody wondering whether the platform has it.
 *
 * Set apart from the description above it by weight and colour rather than by position, since it is
 * the line that explains why the box beside it will not tick.
 *
 * The neutral accent and not the highlight, for the reason .dxp-notice--denied takes the accent: to
 * the developer reading it this is a statement about their own access rather than a report that
 * something broke, and the highlight is this site's fault colour.
 *
 * Combined with .dxp-hidden on every row that is available, whose display: none !important wins. */
.dxp-checkgroup__unavailable {
    max-width: var(--dxp-measure);
    color: var(--dxp-accent);
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.45;
}

/* ── The general-purpose disclosure ──────────────────────────────────── */

/* One optional field, or one block of reference prose, collapsed to a row that says what is behind
 * it. The API keys page is what this was written for: its create section ran roughly a screen of
 * optional controls and setup guidance before the first key table, so a developer who arrived to
 * revoke a key read the whole setup guide on the way past.
 *
 * .dxp-checkgroup__group is the same idea bound to one control, and this is the version any section
 * can use. The two are kept apart rather than merged: that one is a row inside a bordered panel of
 * sibling groups and owns none of its own border, and this one is a standalone panel. Merging them
 * would mean a modifier on every use of both.
 *
 * A native details/summary, which is the only kind available on this host: the portal declares no
 * render mode on these pages, so there is no circuit to raise an event on, and script-src is 'self'
 * with nothing served that would implement a scripted toggle. The browser owning the open state is
 * what makes the disclosure work with JavaScript turned off, keeps it working across an enhanced
 * navigation with nothing to re-bind, and keeps the open state off the server entirely.
 *
 * No display declaration, deliberately. .dxp-hidden is display: none !important and would win in any
 * case, but a details element's own default is what this wants, and flex here applies only when the
 * disclosure is a child of a flex row such as .dxp-filter. */
.dxp-disclosure {
    flex: 1 1 100%;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    overflow: hidden;
}

/* Outside a .dxp-filter row the flex basis above is inert and nothing supplies the gap that row
 * provides, so a disclosure standing on its own states its own top margin. Same value as
 * .dxp-filter's, so the guidance under the create form sits at the rhythm the form above it set. */
.dxp-disclosure--standalone {
    margin: 1.25rem 0 0;
}

/* The row itself, in the treatment .dxp-checkgroup__summary and .dxp-sidebar__summary give the same
 * element, so every disclosure on this site behaves alike.
 *
 * list-style and the WebKit pseudo-element both have to go: a summary draws a disclosure triangle by
 * default and the two vendors hide it by different mechanisms. Left alone, the caret this design
 * draws sits beside a second, differently drawn arrow. */
.dxp-disclosure__summary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem 0.5rem;
    padding: 0.5rem 0.75rem;
    cursor: pointer;
    list-style: none;
}

.dxp-disclosure__summary::-webkit-details-marker {
    display: none;
}

.dxp-disclosure__summary:hover {
    background: var(--dxp-bg-subtle);
}

/* A summary is focusable and is none of the three elements the base focus rule near the top of this
 * file names, so the ring is restated rather than inherited. Inset rather than offset, because the
 * panel clips its own overflow to keep the corner radius and would shave an outset ring off. */
.dxp-disclosure__summary:focus-visible {
    outline: 2px solid var(--dxp-accent);
    outline-offset: -2px;
}

.dxp-disclosure__label {
    color: var(--dxp-fg);
    font-size: 0.875rem;
    font-weight: 600;
}

/* What is behind the row, for a reader deciding whether to open it. This is the half that makes a
 * collapsed section honest rather than merely short: "Scopes" alone hides the fact that leaving it
 * closed is a decision, and the hint is what says which decision it is. */
.dxp-disclosure__hint {
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
}

/* Pushed to the far end so the carets line up down a column of disclosures whatever they are called,
 * and keyed on the details element's own open attribute so the browser drives it and no script has
 * to keep a class in step.
 *
 * margin-left: auto and not justify-content: space-between, because the summary wraps: at a narrow
 * width the hint drops to a second line, and space-between would strand the caret beside the label
 * on the first one. */
.dxp-disclosure__caret {
    flex: 0 0 auto;
    margin-left: auto;
    width: 0.875rem;
    height: 0.875rem;
    color: var(--dxp-fg-faint);
}

.dxp-disclosure[open] > .dxp-disclosure__summary .dxp-disclosure__caret {
    transform: rotate(90deg);
}

/* The panel behind the row. Its top border is what separates it from the summary; the element's own
 * border is on .dxp-disclosure and would otherwise draw nothing between the two. */
.dxp-disclosure__body {
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
    padding: 0.75rem;
    border-top: 1px solid var(--dxp-border);
}

/* A body that is also a fieldset, which brings three user-agent defaults the body rule above does not
 * clear on its own.
 *
 * The three sides are cleared one at a time rather than with a border shorthand, because this rule
 * sits below .dxp-disclosure__body in the file and a shorthand would take the separating top border
 * with them, leaving the panel and its summary running together.
 *
 * min-inline-size is cleared for the reason .dxp-checkgroup clears it: a fieldset's default is
 * min-content, so the longest scope description would stop the panel shrinking and push the page
 * sideways at a narrow width. Nothing about that shows up in a render test.
 *
 * margin is reset because a user-agent fieldset carries an inline one, which would leave the panel's
 * own border standing away from what it encloses. */
.dxp-disclosure__fieldset {
    margin: 0;
    border-right: 0;
    border-bottom: 0;
    border-left: 0;
    min-inline-size: 0;
}

/* Prose inside a disclosure: the notice above a picker, or the sentence under a field explaining
 * what an empty value means. Capped at the reading measure because the panel is as wide as the form
 * and a paragraph run to that width is wider than a sentence wants to be. */
.dxp-disclosure__note {
    max-width: var(--dxp-measure);
    margin: 0;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
    line-height: 1.5;
}

/* A run of checkbox rows inside a panel, in columns that fill the width available. Same intrinsic
 * sizing as .dxp-checkgroup__options and for the same reasons: the track minimum is capped at the
 * container's own width, because a bare 18rem is wider than the panel on a narrow viewport and a
 * track wider than its container is the overflow this bounds.
 *
 * It carries no padding of its own, which is the whole reason it is not .dxp-checkgroup__options:
 * that rule pads because it sits directly inside a bordered group, and here .dxp-disclosure__body has
 * already paid for the inset. */
.dxp-disclosure__options {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
    gap: 0.75rem 1.25rem;
}

/* .dxp-filter__control is sized for a field sitting in a wrapping row and carries a 12rem minimum. In
 * a disclosure body the control has the panel to itself, so it takes the width rather than leaving
 * two thirds of the panel empty beside it. */
.dxp-disclosure__textarea {
    width: 100%;
    resize: vertical;
}

/* ── Try-It console ──────────────────────────────────────────────────── */

/* How the console authenticates: the portal's own short-lived credential, or a key the developer
 * pastes. A native radio group inside a fieldset, so the legend names the choice for a screen reader
 * and arrow keys move between the options without any script; this host serves no inline script and
 * would have nothing to implement a custom control with.
 *
 * The browser's own radio is left visible rather than hidden behind a chip, unlike the documentation
 * snippet switcher. That control picks a view of the same page; this one picks which credential a
 * real request is sent with, and a control that looks like a toggle invites a glance where this one
 * wants a read.
 *
 * The fieldset's default border, padding and min-inline-size are reset for the reason .dxp-langs
 * resets them: min-inline-size on a fieldset is min-content by default, which stops a flex child
 * shrinking. */
.dxp-authmode {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem 1.25rem;
    margin: 1.25rem 0 0;
    padding: 0;
    border: 0;
    min-inline-size: 0;
}

.dxp-authmode > .dxp-filter__label {
    flex-basis: 100%;
    margin-bottom: 0.25rem;
    padding: 0;
}

.dxp-authmode__choice {
    display: inline-flex;
    align-items: center;
    gap: 0.4375rem;
    color: var(--dxp-fg);
    font-size: 0.875rem;
    cursor: pointer;
}

/* A disabled option still renders (both branches stay mounted per the DOM-stability rule), so it
 * says so rather than looking pressable. */
.dxp-authmode__choice:has(> input:disabled) {
    color: var(--dxp-fg-faint);
    cursor: default;
}

/* The key field, sized so the rejection message under it cannot resize the row.
 *
 * .dxp-filter__field is a flex column with no basis of its own, so it is as wide as its widest
 * child. The input alone settles at .dxp-filter__control's 12rem; a two-sentence .dxp-field-error
 * would take that to its own 32rem cap and push the host's buttons onto a second line, so the field
 * would change width on the developer the moment they typed something wrong.
 *
 * A basis fixes the column and lets the sentence wrap inside it, and min-width: 0 lets it shrink
 * below that basis on a narrow viewport rather than overflowing the content column. The message is
 * still free to be long: that is the paragraph's business, not the row's. */
.dxp-authmode__key {
    flex: 0 1 20rem;
    min-width: 0;
}

/* The console's operation picker: a field over a panel of matches grouped by resource.
 *
 * The panel itself reuses the documentation search typeahead's rules (.dxp-search__panel, __group,
 * __group-title, __empty), so the two controls on this site that offer a filtered list look like one
 * control. Only what genuinely differs is restated here, which is the option: a search suggestion is
 * an anchor and an option here is a button, and a button needs its own reset before the shared
 * .dxp-search__option padding and radius mean anything.
 *
 * position: relative is what the panel's absolute placement resolves against. Without it the panel
 * escapes to the nearest positioned ancestor and lands somewhere else on the page entirely, which is
 * a layout failure no render test can see. */
.dxp-combobox {
    position: relative;
    /* Takes the filter row rather than sitting at .dxp-filter__control's 12rem, which was sized for a
     * status dropdown. The content here is a path template plus a summary, so a narrow field would
     * make every option wrap. */
    flex: 1 1 28rem;
    max-width: 44rem;
}

.dxp-combobox__input {
    width: 100%;
}

/* Wider than the field when the field is narrow, because a path template is longer than the control
 * a reader typed it into. Capped so it cannot escape the content column on a wide viewport. */
.dxp-combobox__panel {
    min-width: 100%;
    max-width: 44rem;
}

/* The button reset. .dxp-search__option supplies the padding, radius and colour; a button supplies a
 * centred, inherited-font, bordered box on top of that unless every one of these is cleared. */
.dxp-combobox__option {
    width: 100%;
    border: 0;
    background: none;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.dxp-combobox__option:disabled {
    cursor: default;
    opacity: 0.55;
}

/* The verb, in the same monospace treatment the rest of the site gives a method so the picker and the
 * reference read alike. Fixed width so the paths line up into a column a reader can scan, and
 * inline-block so that width is honoured. */
.dxp-combobox__method {
    display: inline-block;
    min-width: 3.75rem;
    color: var(--dxp-fg-muted);
    font-family: var(--dxp-font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
}

.dxp-combobox__path {
    font-family: var(--dxp-font-mono);
    /* anywhere rather than break-word: a path template has no spaces, so a long one would otherwise
     * push the panel wider than its own max-width instead of wrapping inside it. */
    overflow-wrap: anywhere;
}

/* The console's parameter form.
 *
 * .dxp-filter is a wrapping flex row, and for the narrowing controls it was written for that is the
 * right shape: two or three one-word labels over one-line controls, each as wide as it needs to be and
 * no wider. The delivery log, the add-a-key form, the add-an-endpoint form, the scenario composer and
 * the blueprint runner all want exactly that, which is why this is a modifier and not a change to the
 * row itself.
 *
 * A console parameter is not that. It carries the schema's own sentence about itself above its
 * control, every parameter's sentence is a different length, and an operation publishes a dozen of
 * them at once: GET /api/customers alone declares fourteen. On a wrapping row each field then takes
 * its own intrinsic width and its own height, so no two controls start at the same x offset and no two
 * sit at the same y, and the form reads as a scatter of inputs rather than as a form.
 *
 * A grid instead: equal columns that fill the width, so the controls line up into columns whatever
 * their labels are.
 *
 * The floor is 18rem rather than something narrower because a description is prose: at 16rem the
 * content column fits four tracks and most sentences wrap to two or three lines, which reads as a
 * denser scatter than the row it replaced. Three tracks of 18rem is one line for most parameters.
 *
 * min() on that floor so it cannot exceed the container. A bare 18rem is wider than the content
 * column on a narrow viewport, and a track wider than its container is a horizontal scrollbar on the
 * whole page. Same sizing as .dxp-disclosure__options, for the same reason. */
.dxp-filter--params {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
    gap: 1.25rem 1.5rem;
    /* Every cell to its row's height, which is what the bottom-alignment below has to work against.
     * Stated rather than left to the default because .dxp-filter sets align-items: flex-end for the
     * row it is, and that value carries into the grid this modifier makes of it: fields would size to
     * their own content and sit on the row's baseline, which is the layout being fixed. */
    align-items: stretch;
}

/* A grid item's automatic minimum size is its content, so a field holding a long unbroken parameter
 * name or a wide select would push its own track past the 1fr share it was given and unbalance every
 * other column on the row.
 *
 * The gap is a little wider than the row's 0.25rem because the description sits between the label and
 * the control here, and three lines at one spacing read as one block rather than as a label, a
 * sentence and a field. */
.dxp-filter--params > .dxp-filter__field {
    min-width: 0;
    gap: 0.375rem;
}

/* The control takes its cell rather than sitting at the 12rem .dxp-filter__control was sized for,
 * which in a 1fr track would leave most of the column empty beside it. min-width because that 12rem
 * would otherwise be a floor the track cannot go under, which is the same overflow the min() above
 * avoids.
 *
 * margin-top is what lines the controls up across a row. The field is a flex column stretched to the
 * row's height, so an auto top margin absorbs whatever slack the description above it left over: a
 * one-line description and a three-line one put their controls on the same line. */
.dxp-filter--params .dxp-filter__control {
    width: 100%;
    min-width: 0;
    margin-top: auto;
}

/* The boolean's row takes the same alignment, and takes it rather than the checkbox inside it,
 * because the row is the child of the field and the checkbox is not. It is a separate rule because
 * only one kind of control is visible at a time and the other five are display:none, where a margin
 * means nothing: whichever kind this field renders is the one that takes the slack. */
.dxp-filter--params .dxp-filter__check {
    margin-top: auto;
}

/* The message under one input on the console's parameter form.
 *
 * It uses the refusal colour rather than the muted one because it is the only thing on that form
 * telling a developer why Send did nothing, and it sits next to a description in .dxp-muted: two
 * greys stacked would read as two pieces of help text rather than as help and a fault.
 *
 * Like every other conditional element on this host it is always mounted and hidden with
 * .dxp-hidden, so there is no :empty rule here and there must not be one: an empty error paragraph
 * is the normal state, and collapsing it by content rather than by class would put a second,
 * invisible visibility mechanism next to the one the tests assert on.
 *
 * The colour is the one .dxp-chip--refused and the delete method badge already use, so "this went
 * wrong" reads the same everywhere on the site and the token stays defined in both themes. */
.dxp-field-error {
    max-width: 32rem;
    margin: 0.25rem 0 0;
    color: var(--dxp-method-delete);
    font-size: 0.8125rem;
}

/* The console's request body editor. A plain textarea: this host has script-src closed, so a code
 * editor here would mean either a bundled script it will not load or a server round trip per
 * keystroke over the circuit. Monospace and a tab-friendly size is most of what a JSON payload
 * needs, and the validation a real editor would give is server-side and exact instead. */
.dxp-code-editor {
    width: 100%;
    min-width: 20rem;
    padding: 0.625rem 0.75rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg);
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
    line-height: 1.6;
    resize: vertical;
}

/* The body field spans the form rather than sitting in the flex row with the short inputs, which
 * would otherwise give a twelve-row editor a twelve-rem column. */
.dxp-filter__field:has(> .dxp-code-editor) {
    flex: 1 1 100%;
}

/* An example chip is a button rather than a link, so it needs the affordances a .dxp-chip has no
 * reason to carry: a pointer, a border that reacts, and a disabled state that reads as one while a
 * request is in flight. */
button.dxp-chip {
    cursor: pointer;
    font: inherit;
    font-size: 0.75rem;
}

button.dxp-chip:hover:not(:disabled) {
    border-color: var(--dxp-accent);
    color: var(--dxp-accent);
}

button.dxp-chip:disabled {
    cursor: default;
    opacity: 0.55;
}

/* A timestamp is monospace so a column of them lines up digit for digit, which is most of the value
 * of reading a delivery log at all. Every one is UTC and says so in the value itself: this host
 * resolves no viewer timezone, so a bare local-looking time would be a lie. */
.dxp-timestamp {
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
    white-space: nowrap;
}

/* Paging is a link, not a control: the "older" affordance carries the store's continuation token.
 * Absent on the last page rather than disabled, because there is no page to go to. */
.dxp-pager {
    display: flex;
    gap: 0.75rem;
    margin: 1.25rem 0 0;
}

/* ── Guide prose ─────────────────────────────────────────────────────── */

/* Rendered guide markdown. Everything under here is authored content, so the selectors are element
 * selectors scoped to the wrapper rather than classes: the renderer emits plain HTML and no class
 * ever reaches it.
 *
 * Headings carry an explicit size ramp with real steps and asymmetric spacing (the gap above a
 * heading exceeds the gap below it, so it binds to the copy it introduces rather than floating
 * between two blocks). The in-app guide surface learned this the hard way: leaving the levels on
 * the framework default put h2 and h3 within about ten percent of each other, and on a long guide a
 * subsection became indistinguishable from the section containing it. */
.dxp-guide {
    margin-top: 2rem;
}

.dxp-guide > :first-child {
    margin-top: 0;
}

.dxp-guide h2,
.dxp-guide h3,
.dxp-guide h4 {
    line-height: 1.25;
    font-weight: 650;
    letter-spacing: -0.01em;
}

.dxp-guide h2 {
    font-size: 1.5rem;
    margin: 3rem 0 0.875rem;
    padding-top: 1.75rem;
    border-top: 1px solid var(--dxp-border);
}

.dxp-guide h3 {
    font-size: 1.1875rem;
    margin: 2.25rem 0 0.625rem;
}

.dxp-guide h4 {
    font-size: 1rem;
    margin: 1.75rem 0 0.5rem;
    color: var(--dxp-fg-muted);
}

/* Running prose stops at the measure while everything around it keeps the full column. A guide is
 * mostly paragraphs and lists, so a wider column would otherwise buy longer lines rather than the
 * wider tables, code blocks and card grids it was widened for. */
.dxp-guide p {
    margin: 0 0 1rem;
    max-width: var(--dxp-measure);
}

.dxp-guide ul,
.dxp-guide ol {
    margin: 0 0 1rem;
    padding-left: 1.375rem;
    max-width: var(--dxp-measure);
}

.dxp-guide li + li {
    margin-top: 0.375rem;
}

.dxp-guide blockquote {
    margin: 0 0 1rem;
    max-width: var(--dxp-measure);
    padding: 0.75rem 1.125rem;
    border-left: 3px solid var(--dxp-accent);
    border-radius: 0 var(--dxp-radius-sm) var(--dxp-radius-sm) 0;
    background: var(--dxp-bg-subtle);
    color: var(--dxp-fg-muted);
}

.dxp-guide blockquote > :last-child {
    margin-bottom: 0;
}

.dxp-guide code {
    padding: 0.0625rem 0.3125rem;
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    border: 1px solid var(--dxp-border);
}

/* A code block scrolls inside its own box. The page body must never scroll horizontally, and a
 * request payload is exactly the kind of content that would otherwise force it to. */
.dxp-guide pre {
    margin: 0 0 1.25rem;
    padding: 1rem 1.125rem;
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-code);
    color: var(--dxp-fg-code);
    overflow-x: auto;
}

.dxp-guide pre code {
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font-size: 0.8125rem;
    line-height: 1.6;
}

/* Same reasoning as the code block: a wide comparison table scrolls in its own box. */
.dxp-guide table {
    display: block;
    width: 100%;
    margin: 0 0 1.25rem;
    border-collapse: collapse;
    overflow-x: auto;
    font-size: 0.9rem;
}

.dxp-guide th,
.dxp-guide td {
    text-align: left;
    vertical-align: top;
    padding: 0.5rem 0.75rem 0.5rem 0;
    border-bottom: 1px solid var(--dxp-border);
}

.dxp-guide th {
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--dxp-fg-faint);
}

.dxp-guide hr {
    margin: 2.5rem 0;
    border: 0;
    border-top: 1px solid var(--dxp-border);
}

.dxp-guide img {
    max-width: 100%;
    height: auto;
}

/* ── Next steps ──────────────────────────────────────────────────────── */

.dxp-next-steps {
    margin-top: 3.5rem;
    padding-top: 2rem;
    border-top: 1px solid var(--dxp-border);
}

.dxp-next-steps__title {
    font-size: 1.125rem;
    margin-bottom: 1rem;
}

.dxp-next-steps__grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 0.875rem;
}

.dxp-next-steps__card {
    display: flex;
    flex-direction: column;
    gap: 0.3125rem;
    height: 100%;
    padding: 1rem 1.125rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-lg);
    background: var(--dxp-bg-raised);
    color: var(--dxp-fg);
}

.dxp-next-steps__card:hover {
    border-color: var(--dxp-accent);
    text-decoration: none;
}

.dxp-next-steps__card-title {
    font-weight: 600;
    color: var(--dxp-accent);
}

.dxp-next-steps__card-text {
    font-size: 0.875rem;
    color: var(--dxp-fg-muted);
}

.dxp-next-steps__card-external {
    font-size: 0.75rem;
    color: var(--dxp-fg-faint);
}

/* ── Footer ──────────────────────────────────────────────────────────── */

.dxp-footer {
    border-top: 1px solid var(--dxp-border);
    background: var(--dxp-bg-subtle);
}

.dxp-footer__inner {
    display: flex;
    flex-wrap: wrap;
    gap: 2.5rem;
    max-width: var(--dxp-shell-width);
    margin: 0 auto;
    padding: 2.5rem var(--dxp-shell-gutter);
}

.dxp-footer__brand {
    flex: 1 1 18rem;
    min-width: 0;
}

.dxp-footer__wordmark {
    font-weight: 650;
}

.dxp-footer__tagline {
    margin: 0.375rem 0 0;
    font-size: 0.875rem;
    color: var(--dxp-fg-muted);
    max-width: 26rem;
}

.dxp-footer__links {
    flex: 0 0 auto;
}

.dxp-footer__heading {
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--dxp-fg-faint);
    margin: 0 0 0.5rem;
}

.dxp-footer__list {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: 0.9rem;
}

.dxp-footer__list li + li {
    margin-top: 0.25rem;
}

/* ── Not found (host-level fallback) ─────────────────────────────────── */

.developer-portal-landing {
    max-width: 44rem;
    margin: 0 auto;
    padding: 4rem 1.5rem;
}

.developer-portal-landing h1 {
    font-size: 2rem;
    margin: 0 0 0.75rem;
}

.developer-portal-landing p {
    margin: 0;
    color: var(--dxp-fg-muted);
}

/* ── Code blocks ─────────────────────────────────────────────────────── */

/* Rendered verbatim, never highlighted: highlighting here would need either a script (script-src is
 * closed and there is no interactive render mode to hydrate one) or a server-side tokenizer, and
 * neither earns its keep against a JSON payload and one C# file. */
.dxp-code {
    margin: 1rem 0 0;
}

.dxp-code__caption {
    margin-bottom: 0.375rem;
    font-size: 0.6875rem;
    font-weight: 650;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--dxp-fg-faint);
}

.dxp-code__body {
    /* The run colours for a highlighted body, which on this block is the Try-It console's response.
     *
     * The light ramp's four were chosen against --dxp-bg-subtle, the near-white a .dxp-code figure
     * paints, and this block paints --dxp-bg-code instead: near-black in both ramps. Dark blue on
     * near-black is what this prevents, and it is invisible in review because the same markup is
     * legible on the request-log page, which highlights on the figure.
     *
     * The values are the dark ramp's own, so these declarations change nothing wherever that ramp is
     * in force. TryItResponseStylesheetTests holds the two sets to each other: they are one decision
     * written twice, because CSS cannot read a custom property out of a rule it does not inherit
     * from. */
    --dxp-json-key: #86c8f0;
    --dxp-json-string: #e39a86;
    --dxp-json-number: #9fd3a8;
    --dxp-json-keyword: #bda2f0;
    --dxp-json-punct: #9aa1b4;

    margin: 0;
    padding: 1rem 1.125rem;
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-code);
    color: var(--dxp-fg-code);
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
    line-height: 1.55;
    /* The block scrolls on its own so a long line never widens the page. */
    overflow-x: auto;
    tab-size: 4;
}

.dxp-code__body code {
    font-size: inherit;
}

/* ── Operation code samples ──────────────────────────────────────────── */

/* An operation's eight blocks are grouped into five panels, one per language, and a row of chips
 * chooses which panel shows. There is no script in it and no render mode behind it: the chips are the
 * labels of a hidden radio group, and a checked radio reveals its panel through a sibling selector.
 * That is not a flourish. This host has script-src closed and the documentation surface may never
 * declare a render mode, so anything needing either would be six blocks nobody can read.
 *
 * The cost, and it is the thing to know before editing here: the selectors below have to NAME each
 * language, because CSS cannot correlate the radio's value with the panel's data-dxp-lang. The list
 * therefore exists twice, in SnippetLanguageView and in this file, and a language added to one and
 * not the other produces a chip that selects nothing or a panel that never shows, with valid CSS,
 * a clean build and no test noticing, since nothing in this repository loads the stylesheet.
 * SnippetLanguageViewTests reads this file and fails the build on exactly that. */

/* The container is a fieldset so the visually hidden legend names the group, and so the radios sit as
 * direct siblings of the panels: the sibling combinator cannot climb out of a wrapper.
 *
 * Three resets, and none is cosmetic. The default border and padding are the ones every fieldset
 * needs undone. min-inline-size is the one that is easy to miss: a fieldset defaults to min-content,
 * which stops the code blocks inside it shrinking, so overflow-x on the block never engages and a
 * long sample widens the whole page instead. position:relative keeps the absolutely positioned
 * radios inside it, so focusing one with the keyboard does not scroll the document to a corner. */
.dxp-langs {
    position: relative;
    min-width: 0;
    margin: 1.25rem 0 0;
    padding: 0;
    border: 0;
}

.dxp-langs__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* The chip is the radio's label, so the click target, the hit area and the pointer are all the
 * browser's own. Nothing here has a handler.
 *
 * --dxp-lang is the language's brand colour, set by the modifier class below and read by the mark and
 * by the selected state. It is declared here as well so a chip whose modifier went missing renders in
 * the faint neutral rather than losing its colour declarations to an invalid value. */
.dxp-lang-chip {
    --dxp-lang: var(--dxp-fg-faint);

    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.375rem 0.8125rem 0.375rem 0.5rem;
    border: 1px solid var(--dxp-border);
    border-radius: 999px;
    background: var(--dxp-bg-raised);
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.4;
    cursor: pointer;
    user-select: none;
    transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
}

.dxp-lang-chip:hover {
    border-color: var(--dxp-border-strong);
    color: var(--dxp-fg);
}

/* The brand colour sits on the mark and never on the label. A brand hex chosen for a logo is not a
 * text colour: the .NET purple scores about 2.4:1 on the dark background and the TypeScript blue is
 * marginal on the light one, so a chip whose words carried it would fail AA in one of the two ramps
 * while looking deliberate in the other. The label stays on the foreground tokens, which are already
 * held to that bar, and the mark is small, bold and on a tint of its own colour. */
.dxp-lang-chip__mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.375rem;
    height: 1.375rem;
    padding: 0 0.25rem;
    border-radius: var(--dxp-radius-sm);
    background: color-mix(in srgb, var(--dxp-lang) 14%, transparent);
    color: var(--dxp-lang);
    font-family: var(--dxp-font-mono);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: -0.02em;
}

.dxp-lang-chip--curl {
    --dxp-lang: var(--dxp-lang-curl);
}

.dxp-lang-chip--powershell {
    --dxp-lang: var(--dxp-lang-powershell);
}

.dxp-lang-chip--typescript {
    --dxp-lang: var(--dxp-lang-typescript);
}

.dxp-lang-chip--csharp {
    --dxp-lang: var(--dxp-lang-csharp);
}

.dxp-lang-chip--python {
    --dxp-lang: var(--dxp-lang-python);
}

/* The selected chip. Tinted rather than filled, so the row still reads as one set of controls and the
 * unselected chips do not become the emphasis by contrast. */
.dxp-lang-radio[value="curl"]:checked ~ .dxp-langs__chips .dxp-lang-chip--curl,
.dxp-lang-radio[value="powershell"]:checked ~ .dxp-langs__chips .dxp-lang-chip--powershell,
.dxp-lang-radio[value="typescript"]:checked ~ .dxp-langs__chips .dxp-lang-chip--typescript,
.dxp-lang-radio[value="csharp"]:checked ~ .dxp-langs__chips .dxp-lang-chip--csharp,
.dxp-lang-radio[value="python"]:checked ~ .dxp-langs__chips .dxp-lang-chip--python {
    border-color: color-mix(in srgb, var(--dxp-lang) 55%, var(--dxp-border));
    background: color-mix(in srgb, var(--dxp-lang) 10%, var(--dxp-bg-raised));
    color: var(--dxp-fg);
}

.dxp-lang-radio[value="curl"]:checked ~ .dxp-langs__chips .dxp-lang-chip--curl .dxp-lang-chip__mark,
.dxp-lang-radio[value="powershell"]:checked ~ .dxp-langs__chips .dxp-lang-chip--powershell .dxp-lang-chip__mark,
.dxp-lang-radio[value="typescript"]:checked ~ .dxp-langs__chips .dxp-lang-chip--typescript .dxp-lang-chip__mark,
.dxp-lang-radio[value="csharp"]:checked ~ .dxp-langs__chips .dxp-lang-chip--csharp .dxp-lang-chip__mark,
.dxp-lang-radio[value="python"]:checked ~ .dxp-langs__chips .dxp-lang-chip--python .dxp-lang-chip__mark {
    background: color-mix(in srgb, var(--dxp-lang) 22%, transparent);
}

/* The radio is visually hidden but focusable, so the focus ring has to be drawn on its label instead.
 * Without this the chips are keyboard operable and give no sign of where the focus is, which is worse
 * than not being reachable at all. :focus-visible rather than :focus, so a pointer click does not
 * leave a ring behind. */
.dxp-lang-radio[value="curl"]:focus-visible ~ .dxp-langs__chips .dxp-lang-chip--curl,
.dxp-lang-radio[value="powershell"]:focus-visible ~ .dxp-langs__chips .dxp-lang-chip--powershell,
.dxp-lang-radio[value="typescript"]:focus-visible ~ .dxp-langs__chips .dxp-lang-chip--typescript,
.dxp-lang-radio[value="csharp"]:focus-visible ~ .dxp-langs__chips .dxp-lang-chip--csharp,
.dxp-lang-radio[value="python"]:focus-visible ~ .dxp-langs__chips .dxp-lang-chip--python {
    outline: 2px solid var(--dxp-accent);
    outline-offset: 2px;
}

/* Every panel is mounted and all but one are hidden, per this host's DOM stability rule, and by a
 * stylesheet rule rather than an inline style because style-src drops one. It is also what keeps the
 * Markdown parity test honest: that test reads the block bodies out of the rendered DOM, so a panel
 * that only existed once selected would quietly leave its two blocks out of the comparison. */
.dxp-lang-panel {
    display: none;
    margin-top: 1rem;
}

.dxp-lang-radio[value="curl"]:checked ~ .dxp-lang-panel[data-dxp-lang="curl"],
.dxp-lang-radio[value="powershell"]:checked ~ .dxp-lang-panel[data-dxp-lang="powershell"],
.dxp-lang-radio[value="typescript"]:checked ~ .dxp-lang-panel[data-dxp-lang="typescript"],
.dxp-lang-radio[value="csharp"]:checked ~ .dxp-lang-panel[data-dxp-lang="csharp"],
.dxp-lang-radio[value="python"]:checked ~ .dxp-lang-panel[data-dxp-lang="python"] {
    display: block;
}

/* The install line above an SDK block. Rendered as its own paragraph rather than as the block's
 * caption, because it is a command a reader runs once and not part of the sample. */
.dxp-snippet-install {
    margin: 0.875rem 0 0;
    font-size: 0.8125rem;
    color: var(--dxp-fg-muted);
}

/* ── Blueprints ──────────────────────────────────────────────────────── */

/* The card's chip row. Cards are anchors, so this is a span rather than a block element: an
 * interactive element nested inside an anchor is invalid and the browser reparents it. */
.dxp-blueprint-card__facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-top: 0.75rem;
}

.dxp-step {
    padding: 1.5rem 0;
    border-top: 1px solid var(--dxp-border);
    scroll-margin-top: calc(var(--dxp-header-height) + 1rem);
}

.dxp-step__title {
    font-size: 1.0625rem;
    font-weight: 600;
}

.dxp-step__kind {
    margin: 0.5rem 0 0;
}

.dxp-step__description {
    margin: 0.75rem 0 0;
}

.dxp-step__reference,
.dxp-step__link {
    margin: 0.5rem 0 0;
    font-size: 0.875rem;
}

.dxp-step__captures {
    margin-top: 1rem;
}

.dxp-step__captures-title {
    margin: 0 0 0.375rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--dxp-fg-muted);
}

.dxp-step__captures code {
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
    margin-right: 0.5rem;
}

/* A step's authored response sample: what the endpoint answers with, shown so the prose telling a
   reader to branch on a field has the field in front of them.

   Mirrors .dxp-step__captures rather than declaring a scale of its own, because it sits in the same
   slot in a step's reading order and is the same kind of content: a small titled block under the
   samples. No new custom property, for the reason stated on .dxp-step__result below: the dark ramp is
   declared twice in this file and a token added to one block and not the other leaves half the
   readership on the light value with nothing failing. */
.dxp-step__response-sample {
    margin-top: 1rem;
}

.dxp-step__response-sample-title {
    margin: 0 0 0.375rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--dxp-fg-muted);
}

.dxp-step__response-sample .dxp-muted {
    margin: 0 0 0.5rem;
    font-size: 0.8125rem;
}

/* What a step's run reported, on the authenticated blueprint runner. A sibling of the description
   rather than a new block, because it occupies the same slot in the step's reading order and adding a
   scale of its own would make a run's result look like a different kind of content from the step it
   belongs to.

   No new custom property is introduced here on purpose: the dark ramp is declared twice in this file
   (once under :root[data-theme="dark"] for an explicit choice, once in a prefers-color-scheme query for
   a reader who chose nothing) and ThemeTokenBlockParityTests compares the two. A token added to one and
   not the other leaves half the readership on the light value with nothing failing, so the runner reuses
   the tokens that are already in both. */
.dxp-step__result {
    margin: 0.75rem 0 0;
    font-size: 0.875rem;
    color: var(--dxp-fg-muted);
}

/* Why a failed step failed and whose move it is, under the result line and above the response. A
   compact left-ruled panel in the notices' shape, smaller, because it belongs to one step rather than
   the page. Three colors for five kinds: the failed-request red for a request problem, the amber the
   "Your turn" chip uses for anything somebody other than the request has to change, and the neutral
   accent for a platform fault, where nobody on the page has anything to do but wait. The kind chip
   takes its panel's color so the chip and the rule say one thing.

   No new custom property, for the reason .dxp-step__result gives above. */
.dxp-step__explanation {
    margin: 0.75rem 0 0;
    padding: 0.625rem 0.875rem;
    border: 1px solid var(--dxp-border);
    border-left: 3px solid var(--dxp-method-delete);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
    font-size: 0.875rem;
}

.dxp-step__explanation p {
    margin: 0.375rem 0 0;
}

.dxp-step__explanation p:first-child {
    margin-top: 0;
}

.dxp-step__explanation-cause {
    color: var(--dxp-fg);
}

.dxp-step__explanation-remedy,
.dxp-step__explanation-correlation {
    color: var(--dxp-fg-muted);
}

.dxp-step__explanation-kind {
    border-color: currentColor;
}

.dxp-step__explanation-code {
    font-family: var(--dxp-font-mono);
}

.dxp-step__explanation--request {
    border-left-color: var(--dxp-method-delete);
}

.dxp-step__explanation--request .dxp-step__explanation-kind {
    color: var(--dxp-method-delete);
}

.dxp-step__explanation--setup {
    border-left-color: var(--dxp-method-put);
}

.dxp-step__explanation--setup .dxp-step__explanation-kind {
    color: var(--dxp-method-put);
}

.dxp-step__explanation--fault {
    border-left-color: var(--dxp-accent);
}

.dxp-step__explanation--fault .dxp-step__explanation-kind {
    color: var(--dxp-accent);
}

/* ── A step under a run ──────────────────────────────────────────────────
 *
 * The rail down a step's left edge, and the collapse of a step the run has finished with. Everything
 * in this block hangs off modifiers BlueprintArticle emits only when ShowRunState is set, so the
 * anonymous documentation page renders the step it renders today: a rail there would report a state
 * that page has nothing to produce, and a collapsed step there would hide a body nobody can reopen.
 *
 * No new custom property, for the reason .dxp-step__result carries: the dark ramp is declared twice in
 * this file and ThemeTokenBlockParityTests compares the two blocks, so a token added to one and not
 * the other leaves half the readership on the light value with nothing failing. The four colours here
 * are the ones the state chips already resolve to, which is also what keeps a step's rail and the chip
 * inside it saying the same thing rather than two things that happen to be near each other.
 *
 * BlueprintRunStylesheetTests pins the relationships rather than the numbers. Nothing in this
 * repository runs a browser engine over the portal, so a rule deleted or retuned here takes its
 * behaviour with it while every render assertion stays green.
 */
.dxp-step--run {
    padding-left: 0.875rem;
    border-left: 3px solid var(--dxp-border-strong);
}

/* A step nothing has reached. The neutral line, so the run reads as a column of colour that fills in
 * behind itself rather than as four states competing for attention at once. */
.dxp-step--pending {
    border-left-color: var(--dxp-border-strong);
}

/* The step the run is waiting on: in flight, failed and re-runnable, or asking the developer for
 * something. Nothing in the run state can express this on its own, which is why the page tells the
 * component which step it is. */
.dxp-step--current {
    border-left-color: var(--dxp-method-put);
}

.dxp-step--succeeded {
    border-left-color: var(--dxp-method-get);
}

.dxp-step--failed {
    border-left-color: var(--dxp-method-delete);
}

/* A step that answered with a success status and did not reach its outcome: a declined payment, most
 * often. The chip's colour, so the rail and the chip inside it say the same thing and neither says
 * "failed". */
.dxp-step--declined {
    border-left-color: var(--dxp-method-patch);
}

/* A step the run is done with, collapsed to its heading and the sentence that answers "did it work":
 * the title, the chips, the result line, and the control that reopens it. Everything else goes,
 * including the endpoint line and the request block.
 *
 * Those last two used to stay open, on the reasoning that seeing an identifier from one step appear
 * in the next step's body is most of what makes the capture mechanism believable. That is still true
 * of the step a developer is reading, and it is exactly the step that is not collapsed: a collapse
 * only ever applies to a step the run has already finished with, and a blueprint of eight steps still
 * ran several screens long with a half-collapsed step for each of them, so the step the run was
 * actually waiting on was off the bottom of the page. What a finished step is scanned for is which
 * step it was and whether it worked; the call it sent is one press away.
 *
 * Direct children only. That combinator used to be what separated the samples (bare .dxp-code figures
 * directly inside the step) from the request and the response (each a .dxp-code inside a wrapper of
 * its own), back when the request had to survive a collapse. All three collapse now, so it no longer
 * discriminates between them, and it stays because it is what states the rule: a collapse hides the
 * step's own body and nothing further down. .dxp-code and .dxp-endpoint are generic classes shared
 * with the API reference pages, so a descendant form would silently widen the moment a step nests one
 * of them inside anything.
 *
 * A class rather than an inline style, and a class rather than a details element: this host enforces
 * style-src 'self' and style-src-attr falls back to it, so a dropped inline style renders every
 * collapsed branch open with nothing server-side noticing, and a details element would need the
 * collapsed content to be one contiguous region, which it is not. */
.dxp-step--collapsed > .dxp-endpoint,
.dxp-step--collapsed > .dxp-step__description,
.dxp-step--collapsed > .dxp-step__reference,
.dxp-step--collapsed > .dxp-step__link,
.dxp-step--collapsed > .dxp-step__captures,
.dxp-step--collapsed > .dxp-step__request,
.dxp-step--collapsed > .dxp-step__explanation,
.dxp-step--collapsed > .dxp-step__response,
.dxp-step--collapsed > .dxp-code {
    display: none;
}

/* The control that reopens one. A quiet text button on the step's own line rather than a bordered
 * control: it appears once per step, and a run of eight buttons down the left of the page would read
 * as the page's primary actions when the primary actions are all in the controls above. */
.dxp-step__toggle {
    margin-top: 0.375rem;
    padding: 0;
    border: 0;
    background: none;
    color: var(--dxp-accent);
    font-family: inherit;
    font-size: 0.8125rem;
    cursor: pointer;
}

.dxp-step__toggle:hover {
    text-decoration: underline;
}

/* ── The run progress strip ──────────────────────────────────────────────
 *
 * Stuck under the site header, so the state of a run is readable wherever the developer has scrolled
 * to. A blueprint step is long enough that the first one fills the viewport, which is the whole
 * problem: without this, "did that work" is answered somewhere off the bottom of the screen.
 *
 * z-index below the header's, which is 10 and sticks at 0. The two overlap while scrolling and the
 * header has to win, or the strip slides over the site navigation.
 *
 * An opaque background rather than the page's, because content scrolls under it.
 */
.dxp-run-progress {
    position: sticky;
    top: var(--dxp-header-height);
    z-index: 9;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.875rem;
    margin: 1.5rem 0 0;
    padding: 0.625rem 0.875rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-raised);
}

.dxp-run-progress__pips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin: 0;
    padding-left: 0;
    list-style: none;
}

/* A bar rather than a dot: at this size a row of dots reads as a loading indicator, and the states
 * have to be told apart at a glance rather than counted. */
.dxp-run-progress__pip {
    width: 1.5rem;
    height: 0.375rem;
    border-radius: 999px;
}

.dxp-run-progress__pip--pending {
    background: var(--dxp-border-strong);
}

.dxp-run-progress__pip--current {
    background: var(--dxp-method-put);
}

.dxp-run-progress__pip--succeeded {
    background: var(--dxp-method-get);
}

.dxp-run-progress__pip--failed {
    background: var(--dxp-method-delete);
}

.dxp-run-progress__pip--declined {
    background: var(--dxp-method-patch);
}

/* One line, clipped. It quotes the newest run log entry, whose message is a whole sentence, and a
 * strip that grew to three lines as a run went on would push the page down on every step. The full
 * text is in the run log, which is where a developer reads rather than glances. */
.dxp-run-progress__latest {
    flex: 1 1 12rem;
    min-width: 0;
    margin: 0;
    overflow: hidden;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── Webhook event reference ─────────────────────────────────────────── */

.dxp-event {
    padding: 1.5rem 0;
    border-top: 1px solid var(--dxp-border);
    scroll-margin-top: calc(var(--dxp-header-height) + 1rem);
}

.dxp-event__title {
    font-size: 1rem;
    font-family: var(--dxp-font-mono);
}

.dxp-event__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin: 0.5rem 0 0;
}

.dxp-event__name {
    margin: 0.625rem 0 0;
    font-weight: 600;
}

.dxp-event__description {
    margin: 0.25rem 0 0;
    color: var(--dxp-fg-muted);
}

/* ── Portal overview ─────────────────────────────────────────────────── */

/* The window selector: three plain links, one marked current. Links rather than a control because the
 * whole state of the overview is one query-string token, so a circuit would buy nothing (see
 * PulseRangeSelector). The current one stays a link and is marked by this class plus aria-current. */
.dxp-range {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 1.75rem;
}

.dxp-range__link {
    padding: 0.375rem 0.875rem;
    border: 1px solid var(--dxp-border-strong);
    border-radius: 999px;
    color: var(--dxp-fg-muted);
    font-size: 0.875rem;
    font-weight: 600;
    text-decoration: none;
}

.dxp-range__link:hover {
    border-color: var(--dxp-accent);
    color: var(--dxp-fg);
}

.dxp-range__link--current {
    border-color: var(--dxp-accent);
    background: var(--dxp-accent);
    color: var(--dxp-accent-contrast);
}

/* The one-line verdict above the cards. Every state stays mounted and the others carry dxp-hidden, so
 * these rules must not rely on position: a :first-child reset here would apply to whichever state
 * happens to come first in the markup rather than to the visible one. */
.dxp-health {
    margin-top: 1.25rem;
}

.dxp-health__verdict {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.625rem;
    margin: 0;
    padding: 0.875rem 1.125rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-subtle);
}

.dxp-health__detail {
    color: var(--dxp-fg-muted);
}

/* A card's headline numbers. Auto-fit rather than a fixed column count, so the same rule serves the
 * traffic card's four metrics and the webhook card's three without either being told how wide it is. */
.dxp-metrics {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 1rem;
    margin: 1.25rem 0 0;
}

.dxp-metric {
    padding: 0.875rem 1rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-raised);
}

.dxp-metric__label {
    margin: 0;
    color: var(--dxp-fg-faint);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.dxp-metric__value {
    margin: 0.375rem 0 0;
    font-size: 1.375rem;
    font-weight: 700;
}

/* The request-volume chart. The SVG has a fixed viewBox and no intrinsic size, so the height is set
 * here and preserveAspectRatio="none" on the element lets the width follow the column. */
.dxp-chart {
    margin: 1.25rem 0 0;
}

.dxp-chart__canvas {
    display: block;
    width: 100%;
    height: 11rem;
}

.dxp-chart__baseline {
    stroke: var(--dxp-border-strong);
    stroke-width: 1;
}

/* Colour lives here rather than on a fill/stroke attribute for the same reason the platform's toggles
 * use a class: this host sets style-src 'self', and a token cannot be resolved from an attribute. */
.dxp-chart__area {
    fill: var(--dxp-accent-subtle);
}

.dxp-chart__line {
    fill: none;
    stroke: var(--dxp-accent);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.dxp-chart__axis {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 0.5rem;
    margin: 0.5rem 0 0;
    color: var(--dxp-fg-faint);
    font-family: var(--dxp-font-mono);
    font-size: 0.75rem;
}

.dxp-chart__axis-peak {
    font-family: var(--dxp-font);
    font-weight: 600;
}

/* The error distribution: one row per status code, each with its own single-rect SVG bar. Not one
 * shared SVG, because an SVG <text> element cannot carry attributes inside a .razor file (RZ1023) and
 * because flexbox aligns the label column for free. */
.dxp-bars {
    margin: 1rem 0 0;
    padding: 0;
    list-style: none;
}

.dxp-bars__row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.25rem 0;
}

.dxp-bars__canvas {
    flex: 1 1 auto;
    height: 1.125rem;
    min-width: 4rem;
}

.dxp-bars__bar {
    fill: var(--dxp-accent);
}

.dxp-bars__bar--4xx {
    fill: var(--dxp-method-put);
}

.dxp-bars__bar--5xx {
    fill: var(--dxp-method-delete);
}

.dxp-bars__count {
    flex: 0 0 auto;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
    white-space: nowrap;
}

/* The grouped-error rows reuse .dxp-bars__row for its alignment and then say something different in
 * it: a code, a count, and the two places to go next. There is no bar, because a bar compares
 * magnitudes and the useful comparison here is already the ordering; what a developer does with this
 * row is click it. The code takes the flexible column the bar canvas takes on a status row. */
.dxp-bars__label {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
    font-family: var(--dxp-font-mono);
    font-size: 0.8125rem;
}

.dxp-bars__link {
    flex: 0 0 auto;
    font-size: 0.8125rem;
    white-space: nowrap;
}

/* ── Changelog ───────────────────────────────────────────────────────── */

/* One release. The rule above each section is what turns a long single-column page into a list a
 * reader can scan: the release heading is a level-two heading like every other section title on the
 * site, so without it a release and a subsection of the previous release's notes look alike. */
.dxp-release {
    margin-top: 2.5rem;
    padding-top: 2.5rem;
    border-top: 1px solid var(--dxp-border);
}

.dxp-release:first-of-type {
    margin-top: 2rem;
    padding-top: 0;
    border-top: none;
}

.dxp-release__title {
    font-size: 1.375rem;
    margin: 0;
}

/* The date and the feed's own title sit on one line under the heading. Both are optional and both
 * stay mounted, hidden with dxp-hidden rather than an inline style, so the gap between them has to
 * come from a gap on the row and not from a margin on either child: a hidden child would otherwise
 * leave its margin behind. */
.dxp-release__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 0.75rem;
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-muted);
    font-size: 0.875rem;
}

.dxp-release__date {
    font-variant-numeric: tabular-nums;
}

.dxp-release__link {
    margin: 1rem 0 0;
}

/* The breaking-change callout. It carries the highlight accent rather than the neutral one every
 * other panel on this site uses, and a full border rather than a left rule, because this is the one
 * thing on the page a reader who is upgrading has to act on. It renders only for a release that
 * declares one: a standing "no breaking changes" on every release is alarm fatigue, and it trains a
 * reader to skip the section on the release where it matters. */
.dxp-breaking {
    margin-top: 1.25rem;
    padding: 1.125rem 1.25rem;
    border: 1px solid var(--dxp-highlight);
    border-left-width: 3px;
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-subtle);
}

.dxp-breaking__title {
    font-size: 1rem;
    margin: 0;
    color: var(--dxp-highlight);
}

/* The one icon on the site, and the reason the Font Awesome subset is vendored. It inherits the
 * heading's colour, so it follows --dxp-highlight into the dark ramp with nothing declared here.
 *
 * The size is tied to the heading's font-size rather than fixed: an icon glyph and the text beside
 * it have different optical weights, and 0.9em is what stops the triangle sitting taller than the
 * capital B it leads. */
.dxp-breaking__icon {
    font-size: 0.9em;
    margin-right: 0.5rem;
}

/* The rendered items are guide-styled markdown, whose first child normally carries a top margin. */
.dxp-breaking__body {
    margin-top: 0.5rem;
}

/* The line a release renders in place of its notes when everything it published was folded away
 * behind the disclosure below. Muted rather than a notice: nothing is wrong, the release is simply
 * not about the API, and a coloured panel would read as one more thing to act on. */
.dxp-release__operator-only {
    margin: 1rem 0 0;
    color: var(--dxp-fg-muted);
}

/* The disclosure holding the items the producer classified as back-office changes. It is the site's
 * standard .dxp-disclosure and takes its border, row and caret from there; this only spaces it from
 * the notes above it, at the same rhythm the link below uses, and gives the folded markdown the
 * guide styling the notes above already have. Its own modifier rather than .dxp-disclosure--standalone,
 * because that one's margin is sized for the form it sits under and this one's for a release.
 *
 * Closed on arrival and kept mounted on a release with nothing to fold, hidden with dxp-hidden,
 * whose display: none !important wins over the details element's own display. */
.dxp-release__operator {
    margin-top: 1.25rem;
}

/* The folded notes start with a heading or a list, both of which carry a top margin in the guide
 * styling; the panel already has its own padding, so the first one is pulled back to the edge. */
.dxp-release__operator-body > :first-child {
    margin-top: 0;
}

/* ── Narrow viewports ────────────────────────────────────────────────── */

/* One breakpoint for the whole shell, and 60rem rather than something tighter for the header.
 * A value derived from what the nav measures would be a value that only holds for one instance:
 * the wordmark is the instance's own product name and the support link is optional, so the widest
 * header this stylesheet has to survive is not a number anybody here can know. The cost of erring
 * wide is a disclosure on a viewport where the links would have fitted; the cost of erring narrow
 * is the defect this block exists to fix, on somebody else's deployment. */
@media (max-width: 60rem) {
    .dxp-body {
        flex-direction: column;
        gap: 0;
    }

    /* Stacked above the content and no longer height-capped, so it has nothing to scroll and stops
     * being a scroll container at all. That is what lets the gutter go: with overflow visible the
     * focus ring and the links' background bleed paint into the body's own gutter, exactly as they
     * do beside the content column. Resetting the margin and padding without also resetting the
     * overflow would put the clipping straight back.
     *
     * The tree inside it is a scroll container again once a reader expands it, so the gutter comes
     * back on that element rather than on this one. See .dxp-sidebar__nav-state:checked below. */
    .dxp-sidebar {
        position: static;
        flex-basis: auto;
        width: 100%;
        margin-inline: 0;
        max-height: none;
        overflow: visible;
        padding: 1.5rem 0 1.25rem;
        border-bottom: 1px solid var(--dxp-border);
    }

    /* width rather than max-width alone, and this is the whole of the sideways-scrolling fix.
     *
     * In the wide layout .dxp-body is a row, so this is a flex item on the main axis and its
     * min-width: 0 is what lets a stubborn descendant be squeezed and scroll inside its own box.
     * Here the container is a column, so width is the CROSS axis: with align-items: flex-start the
     * used width is fit-content, which resolves to min-content whenever min-content exceeds the
     * space available, and min-width does not enter that formula at all. One descendant that cannot
     * shrink therefore widened the entire column past the viewport and the page scrolled sideways,
     * on every phone, with the wide layout perfectly correct.
     *
     * Pinning the column to the container's width puts the burden back where it belongs: the code
     * blocks, the reference tables and the field tables all already scroll inside themselves. */
    .dxp-main {
        width: 100%;
        max-width: none;
        padding-top: 1.75rem;
    }

    /* ── The header's nav, as a disclosure ────────────────────────────────
     *
     * The panel hangs off .dxp-header__inner rather than off the header, so it lines up with the
     * wordmark: an absolutely positioned child resolves its insets against the padding box, so the
     * shell gutter here is the same gutter the brand is indented by.
     *
     * The header keeps its declared height throughout. That is the property this shape was chosen
     * for: --dxp-header-height is what the sticky sidebar offset and every scroll-margin-top
     * rule are calculated from, and a header that changed height per breakpoint would land every
     * in-page anchor in the wrong place with nothing failing. */
    .dxp-header__inner {
        position: relative;
    }

    /* Back into the tab order and the accessibility tree, where the control it drives does
     * something. dxp-visually-hidden supplies the rest: it stays off the bar and keeps its
     * accessible name. */
    .dxp-header__menu-state {
        display: block;
    }

    .dxp-header__menu-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 2.25rem;
        height: 2.25rem;
        border: 1px solid var(--dxp-border);
        border-radius: var(--dxp-radius-sm);
        color: var(--dxp-fg-muted);
        cursor: pointer;
    }

    .dxp-header__menu-icon {
        width: 1.25rem;
        height: 1.25rem;
    }

    /* The ring belongs on the label, because the control a reader tabs to is the visually hidden
     * input and there is nothing there to draw one around. Without this the disclosure is operable
     * by keyboard and gives no sign of having been reached, which is worse than not being reachable:
     * a reader tabbing through the header would see focus disappear for one stop. */
    .dxp-header__menu-state:focus-visible ~ .dxp-header__menu-toggle {
        outline: 2px solid var(--dxp-accent);
        outline-offset: 2px;
    }

    .dxp-header__nav {
        display: none;
        position: absolute;
        top: 100%;
        left: var(--dxp-shell-gutter);
        right: var(--dxp-shell-gutter);
        z-index: 20;
        flex-direction: column;
        align-items: stretch;
        gap: 0.125rem;
        margin-top: 0.375rem;
        padding: 0.5rem;
        border: 1px solid var(--dxp-border-strong);
        border-radius: var(--dxp-radius-sm);
        background: var(--dxp-bg-raised);
        box-shadow: 0 0.75rem 1.75rem rgba(0, 0, 0, 0.18);
    }

    .dxp-header__menu-state:checked ~ .dxp-header__nav {
        display: flex;
    }

    /* Rows in a panel rather than items on a bar: the links get a row's worth of padding so each is
     * a target rather than a word, and the two pill variants stop being pills, because a pill that
     * spans the panel reads as a filled row and the two of them together read as two banners. */
    .dxp-header__link {
        padding: 0.5rem 0.625rem;
        border-radius: var(--dxp-radius-sm);
    }

    .dxp-header__link--cta,
    .dxp-header__link--account {
        border-radius: var(--dxp-radius-sm);
        text-align: left;
    }

    /* Left, with the links, rather than centred by the panel's align-items: stretch. A 2.25rem
     * square stretched across the panel would be a very large blank button. */
    .dxp-theme-toggle {
        align-self: flex-start;
    }

    /* ── The sidebar's link tree, as a disclosure ─────────────────────────
     *
     * Full width, because it is the only thing on its row and a reader is reaching for it with a
     * thumb. The chevron turns to point down when the tree is open, which is the only signal the
     * control gives about its own state to a reader who is not using a screen reader. */
    .dxp-sidebar__nav-state {
        display: block;
    }

    .dxp-sidebar__nav-toggle {
        display: flex;
        align-items: center;
        gap: 0.5rem;
        width: 100%;
        padding: 0.625rem 0.75rem;
        border: 1px solid var(--dxp-border);
        border-radius: var(--dxp-radius-sm);
        background: var(--dxp-bg-subtle);
        color: var(--dxp-fg);
        font-size: 0.9rem;
        font-weight: 600;
        cursor: pointer;
    }

    .dxp-sidebar__nav-icon {
        width: 1rem;
        height: 1rem;
        color: var(--dxp-fg-faint);
    }

    .dxp-sidebar__nav-state:checked ~ .dxp-sidebar__nav-toggle .dxp-sidebar__nav-icon {
        transform: rotate(90deg);
    }

    .dxp-sidebar__nav-state:focus-visible ~ .dxp-sidebar__nav-toggle {
        outline: 2px solid var(--dxp-accent);
        outline-offset: 2px;
    }

    .dxp-sidebar__nav {
        display: none;
    }

    /* Capped and scrolling in its own box, so an expanded tree is a panel a reader scrolls rather
     * than a hundred links pushing the page down again, which is the state this whole disclosure
     * exists to end.
     *
     * That makes this element a scroll container, and a scroll container clips on both axes: CSS
     * computes overflow-x from visible to auto whenever the other value is not visible. The two
     * things in here that paint outside their own border box are the links' leftward background
     * bleed and the focus ring on a focused link, and both would be shaved off at the left content
     * edge, which a left-to-right scroll container can never scroll past. So the gutter the sidebar
     * gives up at this breakpoint comes back here, spent as padding and cancelled again by the
     * negative margin so the links stay aligned with the search field above them. The reasoning is
     * the .dxp-sidebar rule's, and SidebarScrollGutterStylesheetTests derives the bound for both. */
    .dxp-sidebar__nav-state:checked ~ .dxp-sidebar__nav {
        display: block;
        max-height: 60vh;
        overflow-y: auto;
        margin-inline: calc(-1 * var(--dxp-sidebar-gutter));
        padding: 1rem var(--dxp-sidebar-gutter);
    }

    /* The sidebar stacks above the content and ends in a border here, so the row's full gap would
     * open every page with two lines of nothing between that border and the page title. */
    .dxp-page-actions {
        margin-bottom: 1rem;
    }
}

/* ── Platform status ─────────────────────────────────────────────────── */

/* The status page reuses the method-verb colours rather than introducing tokens of its own, and
 * that is deliberate rather than lazy. The dark ramp is declared twice in this file (once keyed on
 * data-theme, once inside a prefers-color-scheme query) and the two have to stay in step; every new
 * token is a second edit somebody has to remember, and ThemeTokenBlockParityTests exists because
 * that edit has been forgotten before. These four already carry the right semantics in both ramps:
 * GET's green reads as fine, PUT's amber as caution, DELETE's red as broken, and the faint
 * foreground as "no reading", which is exactly the vocabulary this page needs. */

/* The overall reading is the answer to the question that brought a visitor here, so it is the
 * loudest thing on the page: a banner rather than a note. The level tints the surface as well as the
 * left edge, faintly enough that the tint reads as temperature rather than as a second badge, and
 * mixed against the page's own subtle background so both ramps stay in step without a token. */
.dxp-status-summary {
    margin-top: 2rem;
    padding: 1.5rem 1.625rem;
    border: 1px solid var(--dxp-border-strong);
    border-left: 4px solid var(--dxp-fg-faint);
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-subtle);
}

.dxp-status-summary--operational {
    border-left-color: var(--dxp-method-get);
    background: color-mix(in srgb, var(--dxp-method-get) 7%, var(--dxp-bg-subtle));
}

.dxp-status-summary--degraded {
    border-left-color: var(--dxp-method-put);
    background: color-mix(in srgb, var(--dxp-method-put) 7%, var(--dxp-bg-subtle));
}

.dxp-status-summary--outage {
    border-left-color: var(--dxp-method-delete);
    background: color-mix(in srgb, var(--dxp-method-delete) 7%, var(--dxp-bg-subtle));
}

.dxp-status-summary__headline {
    margin: 0;
    font-size: 1.375rem;
    font-weight: 650;
    letter-spacing: -0.01em;
    color: var(--dxp-fg);
}

.dxp-status-summary__stamp {
    margin: 0.5rem 0 0;
    font-size: 0.875rem;
    color: var(--dxp-fg-muted);
}

/* An incident notice is the operator saying something out loud, so it takes the highlight the
 * degraded notices use rather than the neutral accent an informational notice would get. The level
 * it asserts is already reflected in the summary block above it, which is where a reader looks for
 * severity; repeating it here in colour would say the same thing twice and disagree the moment a
 * component is worse than the notice. */
.dxp-notice--incident {
    border-left-color: var(--dxp-highlight);
}

/* An unavailable reading is not an outage report, and the neutral accent is what says so. Using the
 * fault highlight here would tell an integrator debugging a failure that the platform is down when
 * all this host actually knows is that it could not reach it. */
.dxp-notice--unavailable {
    border-left-color: var(--dxp-accent);
}

/* The component readings are the substance of the page, so the list is a raised card with a strong
 * outer edge rather than a hairline-ruled run of text. It sits on --dxp-bg-raised while the summary
 * above and the legend below sit on --dxp-bg-subtle, which is what makes the middle block read as
 * the one to look at. */
.dxp-status-list {
    list-style: none;
    margin: 0.875rem 0 0;
    padding: 0;
    border: 1px solid var(--dxp-border-strong);
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-raised);
    overflow: hidden;
}

/* Three columns: name, level, and what the component covers. The level column is fixed so the
 * badges line up down the page, which is what makes the list scannable at a glance during an
 * incident. */
.dxp-status-row {
    display: grid;
    grid-template-columns: minmax(10rem, 15rem) 10rem 1fr;
    align-items: baseline;
    gap: 0.75rem 1.25rem;
    padding: 1.0625rem 1.25rem;
    border-top: 1px solid var(--dxp-border);
}

.dxp-status-row:first-child {
    border-top: 0;
}

.dxp-status-row__name {
    font-size: 1rem;
    font-weight: 650;
    color: var(--dxp-fg);
}

/* The level is carried by text as well as colour. A badge that said "operational" only by being
 * green would say nothing at all to a reader who cannot distinguish it, on the one page where the
 * distinction is the whole content.
 *
 * inline-block rather than inline-flex, deliberately. The row aligns its three columns on the text
 * baseline, and an inline-flex box takes its baseline from its first flex item, which here is the
 * empty dot: the badge would hang above the component name beside it. An inline-block takes its
 * baseline from its own last line box, so the badge text and the component name sit on one line. */
.dxp-status-row__level {
    display: inline-block;
    justify-self: start;
    padding: 0.25rem 0.6875rem;
    border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, currentColor 10%, transparent);
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.045em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--dxp-fg-faint);
}

.dxp-status-row__detail {
    font-size: 0.9375rem;
    color: var(--dxp-fg-muted);
}

/* A definition grid rather than a stacked dl. Every term is a level name, so one max-content column
 * sizes itself to the longest of them and the meanings all start on the same edge, which is what
 * turns four paragraphs into something a reader takes in without reading. */
.dxp-status-legend {
    display: grid;
    grid-template-columns: max-content 1fr;
    align-items: baseline;
    gap: 0.75rem 1.25rem;
    margin: 0.875rem 0 0;
    padding: 1.125rem 1.375rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius);
    background: var(--dxp-bg-subtle);
}

/* The legend term is not a badge. It names the level the badges use, so it takes the badge's
 * typography and colour and none of its pill. */
.dxp-status-legend__term {
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.045em;
    text-transform: uppercase;
    color: var(--dxp-fg-faint);
}

.dxp-status-legend__detail {
    margin: 0;
    padding-left: 0;
    font-size: 0.9375rem;
    color: var(--dxp-fg-muted);
}

/* The dot is decoration on top of a word that already says the level, so it is a pseudo-element and
 * never markup: it must not reach the accessibility tree, and it must not reach TextContent either,
 * which is what the page's tests read the level out of. */
.dxp-status-row__level::before,
.dxp-status-legend__term::before {
    content: "";
    display: inline-block;
    width: 0.4375rem;
    height: 0.4375rem;
    margin-right: 0.4375rem;
    border-radius: 50%;
    background: currentColor;
    vertical-align: 0.075em;
}

/* The level colours, shared by the badge in the list and the term in the legend, and last in this
 * block on purpose. Both carriers declare the neutral "no reading" colour themselves, and a modifier
 * is a single class like the rule it has to beat, so the cascade settles this on source order alone.
 * Moving these up silently returns the legend to four grey terms: the modifier is still on the
 * element, still matching, and simply losing. */
.dxp-status-row__level--operational {
    color: var(--dxp-method-get);
}

.dxp-status-row__level--degraded {
    color: var(--dxp-method-put);
}

.dxp-status-row__level--outage {
    color: var(--dxp-method-delete);
}

/* Below the docs shell's narrow breakpoint the three-column row has nowhere to go, so it stacks.
 * The level keeps its own line rather than sharing one with the name: a wrapped badge beside a
 * wrapped component name is the shape that makes a stacked status list unreadable. */
@media (max-width: 60rem) {
    .dxp-status-row {
        grid-template-columns: 1fr;
        gap: 0.4375rem;
    }
}

/* Narrower still, the legend's term column leaves too little for the meaning beside it, so the pair
 * stacks. The term keeps the gap above it that separates it from the previous meaning; the row gap
 * drops to nothing so a term stays attached to the meaning it introduces. */
@media (max-width: 40rem) {
    .dxp-status-legend {
        grid-template-columns: 1fr;
        gap: 0 0;
    }

    .dxp-status-legend__term {
        margin-top: 0.875rem;
    }

    .dxp-status-legend__term:first-child {
        margin-top: 0;
    }

    .dxp-status-legend__detail {
        margin-top: 0.1875rem;
    }
}

/* ── Reconnect dialog ────────────────────────────────────────────────────

   The dialog Blazor shows when a circuit drops. The markup is ours (App.razor explains why the
   framework's own display cannot be themed at all), and Blazor drives it by toggling exactly four
   classes on the outer element: components-reconnect-show while it is retrying,
   components-reconnect-hide once the connection is back, components-reconnect-failed when the
   retries are exhausted, and components-reconnect-rejected when the server no longer holds the
   circuit. Those names come from the framework and are not ours to rename.

   The base rule is display:none, so the two states that must not be visible need no rule of their
   own: no class at all is a healthy page, and the hide class is a recovered one. That is the safe
   direction, because a framework release that stopped adding one of the four would leave the dialog
   hidden rather than pinned open over a working page.

   Every colour is a --dxp-* token, which is the whole point of the exercise: the tokens already
   resolve per theme in all three ramps (explicit dark, explicit light, and system preference), so
   nothing here needs a second declaration and no new token is introduced.

   The inner display toggles are all written .dxp-reconnect .dxp-reconnect__x rather than bare, so
   they beat .dxp-button's own display on specificity instead of on source order. The buttons reuse
   the site's button classes, and .dxp-button is declared far above this block. */

.dxp-reconnect {
    display: none;
    position: fixed;
    inset: 0;
    /* Above everything the site itself stacks (the header sits at 20). Deliberately well below the
       framework's own 10000: nothing of the framework's paints here any more, and leaving room
       under it means a future overlay does not have to reason about this one. */
    z-index: 900;
    padding: 1.5rem;
    /* The page's own background at 80%, so the scrim reads as the site dimming rather than as a
       black wash over a light page or a white one over a dark page. */
    background: color-mix(in srgb, var(--dxp-bg) 80%, transparent);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
}

.dxp-reconnect.components-reconnect-show,
.dxp-reconnect.components-reconnect-failed,
.dxp-reconnect.components-reconnect-rejected {
    display: flex;
    align-items: center;
    justify-content: center;
}

.dxp-reconnect__dialog {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    width: min(26rem, 100%);
    padding: 2rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-lg);
    background: var(--dxp-bg-raised);
    color: var(--dxp-fg);
    box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
    text-align: center;
}

.dxp-reconnect .dxp-reconnect__spinner {
    display: none;
    width: 2.5rem;
    height: 2.5rem;
    border: 3px solid var(--dxp-border-strong);
    border-top-color: var(--dxp-accent);
    border-radius: 50%;
    animation: dxp-reconnect-spin 0.9s linear infinite;
}

.dxp-reconnect.components-reconnect-show .dxp-reconnect__spinner {
    display: block;
}

@keyframes dxp-reconnect-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Slowed rather than stopped. The spinner is the only thing on the dialog saying the page is still
   trying, and a reader who asked for less motion still needs to be able to tell a retry in progress
   from a dialog that has given up. */
@media (prefers-reduced-motion: reduce) {
    .dxp-reconnect .dxp-reconnect__spinner {
        animation-duration: 3s;
    }
}

.dxp-reconnect .dxp-reconnect__status {
    display: none;
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 600;
    line-height: 1.4;
}

.dxp-reconnect.components-reconnect-show .dxp-reconnect__status--rejoining,
.dxp-reconnect.components-reconnect-failed .dxp-reconnect__status--failed,
.dxp-reconnect.components-reconnect-rejected .dxp-reconnect__status--rejected {
    display: block;
}

.dxp-reconnect .dxp-reconnect__detail {
    display: none;
    margin: 0;
    font-size: 0.875rem;
    line-height: 1.5;
    color: var(--dxp-fg-muted);
}

/* Every detail carries its own state modifier, including the attempt counter. Selecting the counter
   as the unmodified .dxp-reconnect__detail would work and is the trap: at equal specificity it also
   matches the other two, so the retrying state would show all three and the only thing keeping it
   honest would be the source order of an override. */
.dxp-reconnect.components-reconnect-show .dxp-reconnect__detail--rejoining,
.dxp-reconnect.components-reconnect-failed .dxp-reconnect__detail--failed,
.dxp-reconnect.components-reconnect-rejected .dxp-reconnect__detail--rejected {
    display: block;
}

.dxp-reconnect .dxp-reconnect__actions {
    display: none;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem;
    margin-top: 0.25rem;
}

.dxp-reconnect.components-reconnect-failed .dxp-reconnect__actions,
.dxp-reconnect.components-reconnect-rejected .dxp-reconnect__actions {
    display: flex;
}

/* Retry is offered on failure and withheld on rejection. A rejected circuit is one the server has
   already discarded, so re-joining it cannot succeed and a button that could only ever fail would
   be worse than not offering one. */
.dxp-reconnect .dxp-reconnect__retry {
    display: none;
}

.dxp-reconnect.components-reconnect-failed .dxp-reconnect__retry {
    display: inline-flex;
}

/* ── Sample-data run progress ────────────────────────────────────────────

   What the sample-data page shows while a run is in flight: a spinner, a sentence carrying a count
   that moves, and the steps the seeder has finished so far.

   CSS only, and that is a host constraint rather than a preference. This host serves script-src with
   no 'unsafe-inline' and nothing but self-hosted files, and style-src 'self' likewise, which covers
   inline style attributes as well as <style> blocks. So an animation driven from script or written
   into a style attribute does not run here: the page toggles classes and the motion lives in this
   file.

   The whole block is inside an element the page hides with .dxp-hidden when no run is in flight, so
   nothing here animates on an idle page and no rule has to stop it. */

.dxp-seed-progress {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.dxp-seed-progress__status {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
    color: var(--dxp-fg-muted);
}

/* flex: none because the sentence beside it wraps on a narrow viewport, and a flex item that is
   allowed to shrink turns a circle into an ellipse the moment it does. */
.dxp-seed-progress__spinner {
    flex: none;
    width: 1rem;
    height: 1rem;
    border: 2px solid var(--dxp-border-strong);
    border-top-color: var(--dxp-accent);
    border-radius: 50%;
    animation: dxp-seed-progress-spin 0.9s linear infinite;
}

@keyframes dxp-seed-progress-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Stopped rather than slowed, which is the opposite of what the reconnect dialog does one block
   above, and the difference is what else is on the screen. There the spinner is the only thing
   saying the page is still trying, so it has to keep moving in some form. Here the sentence carries
   a count and the list below it grows a row per completed call, so a reader who asked for less
   motion still has two things telling them the run is alive, and the ring can simply hold still. */
@media (prefers-reduced-motion: reduce) {
    .dxp-seed-progress__spinner {
        animation: none;
    }
}

/* ── Inline busy line ────────────────────────────────────────────────────

   What an interactive surface shows beside its controls while one call is in flight: a small spinner
   and a sentence saying what is being waited on. The blueprint runner, the scenario pages and the
   console's response panel all mount it (through PortalBusyIndicator or, on the runner's progress
   strip, as a bare spinner beside an existing live region); the sample-data page keeps its own
   dxp-seed-progress, whose status line also carries a step list and a moving count.

   The element stays mounted and its text empties when nothing is in flight, rather than the wrapper
   hiding: it is an aria-live region, and a display:none element is out of the accessibility tree
   entirely, so a region hidden that way announces nothing and the failure is silent. Only the
   spinner, which a screen reader never sees, hides by class.

   CSS only, for the reason the sample-data block above states: this host's Content-Security-Policy
   allows no inline style and no script-driven animation. */

.dxp-busy {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.75rem 0 0;
    color: var(--dxp-fg-muted);
    font-size: 0.875rem;
}

/* flex: none for the reason the seed-progress ring states it: a flex item allowed to shrink turns a
 * circle into an ellipse the moment the sentence beside it wraps. inline-block so the same ring can
 * sit inside a line of text, which is how the runner's progress strip mounts it. */
.dxp-busy__spinner {
    flex: none;
    display: inline-block;
    vertical-align: -0.125em;
    width: 1rem;
    height: 1rem;
    border: 2px solid var(--dxp-border-strong);
    border-top-color: var(--dxp-accent);
    border-radius: 50%;
    animation: dxp-busy-spin 0.9s linear infinite;
}

@keyframes dxp-busy-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Stopped rather than slowed, for the reason the seed-progress ring stops: the sentence beside the
 * ring says what is happening, so a reader who asked for less motion still has the words. */
@media (prefers-reduced-motion: reduce) {
    .dxp-busy__spinner {
        animation: none;
    }
}

/* The go-live checklist on the signed-in home page.

   An ordered list, because the order is the recommended path through the blueprints rather than an
   arbitrary grouping, and a reader who takes the numbering as a sequence is reading it correctly.
   The markers are suppressed: the state word in each row is the thing to scan for, and a numeral in
   front of it would be a second column of digits competing with it. */
.dxp-readiness {
    list-style: none;
    margin: 1rem 0 0;
    padding: 0;
    display: grid;
    gap: 0.75rem;
}

.dxp-readiness__progress {
    margin: 1rem 0 0;
    color: var(--dxp-fg-faint);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.dxp-readiness__item {
    padding: 0.875rem 1rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-raised);
    /* The left edge is the only thing that differs between the two states at a glance, and it is
       deliberately not the only thing that differs at all: the row also carries the state as a word,
       so the card survives a reader who cannot separate the two hues. */
    border-left-width: 3px;
}

.dxp-readiness__item--met {
    border-left-color: var(--dxp-method-get);
}

.dxp-readiness__item--open {
    border-left-color: var(--dxp-border-strong);
}

.dxp-readiness__title {
    margin: 0;
    font-weight: 600;
}

/* Inline-block rather than inline, so the two state words occupy the same width and the titles beside
   them line up down the list. min-width is set in ch against the longer of the two ("To do"). */
.dxp-readiness__state {
    display: inline-block;
    min-width: 5ch;
    margin-right: 0.5rem;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.dxp-readiness__item--met .dxp-readiness__state {
    color: var(--dxp-method-get);
}

.dxp-readiness__item--open .dxp-readiness__state {
    color: var(--dxp-fg-faint);
}

.dxp-readiness__evidence {
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-muted);
    font-size: 0.875rem;
}

/* The caveat on a proxy item. Set apart from the evidence line above it rather than merged into it,
   because the two make different claims: one says what the platform saw, and this one says what it
   did not. A reader skimming for the second must not have to parse the first to find it. */
.dxp-readiness__caveat {
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-faint);
    font-size: 0.8125rem;
    font-style: italic;
}

.dxp-readiness__action {
    margin: 0.625rem 0 0;
}

/* ── Reseller portfolio ──────────────────────────────────────────────── */

/* The portfolio sections a reseller session sees above the selected merchant's cards. Everything here
   is dxp-portfolio- prefixed and none of it is reached by a merchant session, whose overview renders
   none of these elements. */

/* The window total is the card's one number, so it is set in tabular figures and allowed no wrap: a
   total that breaks across two lines on a phone reads as two numbers. */
.dxp-portfolio-traffic__total {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.dxp-portfolio-traffic__rollup-start {
    color: var(--dxp-fg-muted);
    font-size: 0.875rem;
}

/* The merchant cell holds a name that can be long and a marker beside it, so it wraps where the base
   table's body header would not: .dxp-table tbody th sets nowrap for short labels, and a merchant name
   is not one. */
.dxp-portfolio-readiness__merchant {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* The current merchant's row is marked by more than a hue: its left edge takes a width and the
   "Current" marker says so in words, so the row survives a reader who cannot separate the colours
   and a stylesheet whose brand tokens were overridden. */
.dxp-portfolio-readiness__row--current > * {
    background: var(--dxp-accent-subtle);
}

.dxp-portfolio-readiness__row--current > th {
    border-left: 3px solid var(--dxp-accent);
    padding-left: 0.5rem;
}

/* The progress count, its markers and the switch button are short and belong on one line; the merchant
   cell beside them is the one that wraps. */
.dxp-portfolio-readiness__checklist,
.dxp-portfolio-readiness__action {
    white-space: nowrap;
}

.dxp-portfolio-readiness__name {
    font-weight: 600;
}

.dxp-portfolio-readiness__progress {
    font-variant-numeric: tabular-nums;
}

.dxp-portfolio-readiness__unreadable {
    color: var(--dxp-fg-muted);
    font-style: italic;
}

/* A marker is a short word beside a value, never the value itself: the state it carries is always
   written out, so it is styled as a label rather than as a coloured dot. */
.dxp-portfolio-readiness__marker {
    display: inline-block;
    margin-left: 0.5rem;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.dxp-portfolio-readiness__marker--current {
    color: var(--dxp-accent);
}

.dxp-portfolio-readiness__marker--complete {
    color: var(--dxp-method-get);
}

/* The per-row form sits in a table cell, so it drops the block margin a form would otherwise bring
   and stays on one line with its button. */
.dxp-portfolio-readiness__switch {
    display: inline-flex;
    margin: 0;
}

.dxp-portfolio-readiness__submit {
    padding: 0.3125rem 0.75rem;
    font-size: 0.8125rem;
}

.dxp-portfolio-readiness__cap {
    color: var(--dxp-fg-muted);
    font-size: 0.875rem;
}

/* The heading that names the merchant every card below it belongs to. It opens the merchant half of
   the page, so it is separated from the portfolio above it by a rule rather than by space alone. */
.dxp-portfolio-merchant {
    margin: 2rem 0 0;
    padding: 1.25rem 0 0;
    border-top: 1px solid var(--dxp-border-strong);
}

.dxp-portfolio-merchant__eyebrow {
    margin: 0;
    color: var(--dxp-fg-faint);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.dxp-portfolio-merchant__name {
    margin: 0.25rem 0 0;
    overflow-wrap: anywhere;
}

.dxp-portfolio-merchant__lead {
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-muted);
}

/* ── Decline mix ─────────────────────────────────────────────────────── */

/* A decline row reuses .dxp-bars__row for the code, the count and the two links, and then breaks the
 * single-line assumption that rule was written under: this row also carries the processor's own
 * definition and the handling advice, which are sentences rather than cells. So it wraps, and the two
 * block-level paragraphs take a full row of their own via flex-basis. Without the wrap the advice
 * would be squeezed into a flex column beside the code and the row would grow taller than the card. */
.dxp-declines__row {
    flex-wrap: wrap;
    padding: 0.875rem 1rem;
    border: 1px solid var(--dxp-border);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-raised);
    /* The left edge carries the classification, and it is deliberately not the only thing that does:
       the row also carries the handling as a word, so the card survives a reader who cannot separate
       the hues and a stylesheet whose brand tokens were overridden. */
    border-left-width: 3px;
    border-left-color: var(--dxp-border-strong);
}

.dxp-declines__row + .dxp-declines__row {
    margin-top: 0.5rem;
}

/* A settled refusal and a passing one are the two a developer branches on, so they are the two that
   get an edge of their own. Verification is a third answer rather than a point on that scale. */
.dxp-declines__row--donotretry {
    border-left-color: var(--dxp-method-delete);
}

.dxp-declines__row--retriable {
    border-left-color: var(--dxp-method-put);
}

.dxp-declines__row--verification {
    border-left-color: var(--dxp-accent);
}

.dxp-declines__processor {
    flex: 0 0 auto;
    color: var(--dxp-fg-muted);
    font-family: var(--dxp-font-mono);
    font-size: 0.75rem;
}

.dxp-declines__label {
    flex: 0 0 auto;
    color: var(--dxp-fg-muted);
    font-size: 0.8125rem;
}

/* The classification, as a word. Sized and spaced like the checklist's state chip next door, because
   it does the same job: it is the part of the row a reader scans down rather than reads. */
.dxp-declines__handling {
    flex: 0 0 auto;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.dxp-declines__row--donotretry .dxp-declines__handling {
    color: var(--dxp-method-delete);
}

.dxp-declines__row--retriable .dxp-declines__handling {
    color: var(--dxp-method-put);
}

.dxp-declines__row--verification .dxp-declines__handling {
    color: var(--dxp-accent);
}

/* The three sentence lines take a whole row each. flex-basis at 100% rather than a block display,
   because the row is a flex container and a block child would still sit in a column. */
.dxp-declines__advice {
    flex: 1 1 100%;
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-muted);
    font-size: 0.875rem;
}

/* The processor's own definition, set apart from the advice above it rather than merged into it,
   because the two have different authors: one is what the processor published, the other is what this
   platform makes of it. A reader deciding whether to trust the second needs to see the first. */
.dxp-declines__definition {
    flex: 1 1 100%;
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-faint);
    font-size: 0.8125rem;
}

.dxp-declines__range {
    flex: 1 1 100%;
    margin: 0.375rem 0 0;
    color: var(--dxp-fg-faint);
    font-size: 0.8125rem;
    font-style: italic;
}

/* ── Request-log body viewer ─────────────────────────────────────────── */

/* The Pretty and Raw choice for one logged body. Links rather than buttons, because this page
 * declares no render mode: the choice is the address, so every view of a body is shareable and
 * nothing here needs a handler to run. */
.dxp-views {
    display: flex;
    gap: 0.375rem;
    margin: 0.75rem 0 0;
}

.dxp-views__link {
    padding: 0.1875rem 0.625rem;
    border: 1px solid var(--dxp-border);
    border-radius: 999px;
    font-size: 0.8125rem;
    text-decoration: none;
    color: var(--dxp-fg-muted);
}

.dxp-views__link:hover {
    border-color: var(--dxp-border-strong);
    color: var(--dxp-fg);
}

/* The Try-It console's copy of the same choice, as buttons. That page runs on a circuit and its
 * result lives in component state, so there is no address for a link to carry and nothing to share:
 * the choice has to be a control. These are the user-agent defaults a button brings and an anchor
 * does not, undone so the two surfaces render one control.
 *
 * The font is named rather than inherited because the console's choice sits inside the code figure,
 * whose family is the monospace one: a button inheriting from there would be the only monospace
 * control on the page. */
.dxp-views__button {
    background: none;
    font-family: var(--dxp-font);
    font-size: 0.8125rem;
    line-height: 1.5;
    cursor: pointer;
}

/* Inside the code figure the choice sits between the caption and the block, where the figure's own
 * top padding has already opened the space. */
.dxp-views--code {
    margin-top: 0;
    margin-bottom: 0.625rem;
}

/* The view in force. Marked by aria-current in the markup as well, so the distinction is not
 * carried by colour alone. */
.dxp-views__link.is-current {
    border-color: var(--dxp-accent);
    background: var(--dxp-accent-subtle);
    color: var(--dxp-accent);
}

/* What the platform said, above the payload it said it in. */
.dxp-error-summary {
    margin: 0.75rem 0 0;
    padding: 0.875rem 1rem;
    border: 1px solid var(--dxp-border-strong);
    border-left: 3px solid var(--dxp-method-delete);
    border-radius: var(--dxp-radius-sm);
    background: var(--dxp-bg-subtle);
}

.dxp-error-summary__code {
    margin: 0;
    font-family: var(--dxp-font-mono);
    font-weight: 600;
    color: var(--dxp-fg);
}

.dxp-error-summary__message {
    margin: 0.25rem 0 0;
    color: var(--dxp-fg);
}

.dxp-error-summary__details {
    margin: 0.25rem 0 0;
    font-size: 0.8125rem;
    color: var(--dxp-fg-muted);
}

.dxp-error-summary .dxp-table-scroll {
    margin-top: 0.625rem;
}

/* The highlighter's runs. Colour is the only thing highlighting adds: the runs concatenate back to
 * the stored body character for character. */
.dxp-json-key {
    color: var(--dxp-json-key);
}

.dxp-json-string {
    color: var(--dxp-json-string);
}

.dxp-json-number {
    color: var(--dxp-json-number);
}

.dxp-json-keyword {
    color: var(--dxp-json-keyword);
}

.dxp-json-punct {
    color: var(--dxp-json-punct);
}
