/*
    Base layer: reset, document type, focus, and the browser surfaces nobody draws.

    Loaded after tokens.css and before the scoped-component bundle. It holds no colour literals — every
    value resolves through a token (DesignSystemTests.NoComponentRuleContainsALiteralHex).

    Bootstrap used to sit under this file. It was removed rather than re-themed: its JS bundle was never
    loaded, its component look pulls away from the desktop's language at every control, and M10-U11's
    incoming CSP penalises both weight and inline style. What Bootstrap actually supplied here — a reset
    and a handful of layout utilities — is the first 60 lines below.
*/

/* ── Display face ──────────────────────────────────────────────────────────────────────────────────── */

/*
    Archivo, self-hosted. One variable file, latin subset, weight 400–800 and width 100–125%, ~90KB.

    SELF-HOSTED, not linked from fonts.googleapis.com, and that is a requirement rather than a
    preference: M10-U11 lands a Content-Security-Policy on this surface, and a third-party font host is
    the first thing such a policy refuses. Same-origin also means one fewer DNS lookup and no third party
    learning who reads this site. DesignSystemTests.NoStylesheetLoadsFromAnExternalHost enforces it.

    font-display: swap — the system stack renders immediately and Archivo replaces it. On a prerendered
    page that first paint is the whole point, so blocking on a font would trade away the thing the
    prerender exists to deliver.
*/
@font-face {
    font-family: "Archivo";
    src: url("../fonts/archivo-latin-var.woff2") format("woff2-variations");
    font-weight: 400 800;
    font-stretch: 100% 125%;
    font-style: normal;
    font-display: swap;
    /* Latin only. Commercial v1 is English-only and emits no hreflang (PRODUCT.md), so shipping the
       other subsets would be paying for coverage nothing renders. */
    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+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
        U+FFFD;
}

/* ── Reset ─────────────────────────────────────────────────────────────────────────────────────────── */

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

html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body,
h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd, ol, ul {
    margin: 0;
}

ol[role="list"],
ul[role="list"] {
    list-style: none;
    padding: 0;
}

img,
picture,
svg {
    display: block;
    max-width: 100%;
}

/*
    The icon set is stroked, never filled, and this is where that is enforced.

    CfIcon writes fill="none" as a presentation attribute, and a CSS rule beats a presentation attribute —
    so the `svg { fill: currentColor }` that used to sit here silently filled every glyph: the danger
    circle rendered as a solid disc, the warning triangle as a solid wedge, and the whole set stopped
    being an outline family. Measured on the account page's signed-out state, where a filled disc looked
    like a deliberate status dot.

    Scoped to .cf-icon rather than to svg, because CfBrandLockup's artwork carries its own fills.
*/
.cf-icon {
    fill: none;
    flex-shrink: 0;
}

input,
button,
textarea,
select {
    font: inherit;
    color: inherit;
}

button {
    background: none;
    border: 0;
    padding: 0;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

/* ── Document ──────────────────────────────────────────────────────────────────────────────────────── */

html,
body {
    height: 100%;
}

body {
    position: relative;
    margin: 0;
    background-color: var(--surface-base);
    color: var(--text-primary);
    font-family: var(--font-ui);
    font-size: var(--text-md);
    font-weight: var(--weight-normal);
    line-height: var(--leading-normal);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* The desktop sets TextFormattingMode="Display" globally — crisp stems over metric fidelity, because
       every size in the scale is small. This is the nearest web equivalent. */
    text-rendering: optimizeLegibility;
}

/*
    ── The light field ────────────────────────────────────────────────────────────────────────────────

    The page has a light source, and it is the product's own: a warm bloom where the sound wave enters
    and a cool one where the answer lines leave, the two ramps off the brand mark. Everything else in the
    system — glass surfaces, lit top edges, glows — exists to respond to this. Without it they have
    nothing to respond to, which is exactly why the first build read flat: it was one matte fill.

    position: fixed, so the field stays put while content scrolls THROUGH it rather than dragging a
    gradient down the page. Painted on a pseudo-element rather than on body's own background so it can
    sit behind everything at z-index 0 without a stacking-context fight.

    Deliberately below the threshold of "that's a gradient". Measured at these alphas it reads as depth.
*/
body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(60rem 40rem at 8% -6%, var(--field-warm) 0%, transparent 62%),
        radial-gradient(70rem 46rem at 88% 4%, var(--field-cool) 0%, transparent 60%),
        radial-gradient(52rem 34rem at 70% 52%, var(--field-cool-hi) 0%, transparent 58%);
}

h1, h2, h3, h4, h5, h6 {
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-display);
    text-wrap: balance;
}

h1 { font-size: var(--text-2xl); }
h2 { font-size: var(--text-xl); }
h3 { font-size: var(--text-lg); }
h4 { font-size: var(--text-base); }
h5, h6 { font-size: var(--text-sm); }

/*
    The display voice, applied by class rather than by element — an <h1> in a settings panel is a UI
    label and must not inherit a 6rem instrument face just because it is an h1.

    font-stretch is what makes this setting specific: Archivo at 116% width is the wide, flat-terminalled
    letter of an instrument faceplate, and Archivo at 100% is an ordinary grotesque.
*/
.display {
    font-family: var(--font-display);
    font-stretch: var(--display-stretch);
    font-weight: 800;
    letter-spacing: -0.035em;
    line-height: 0.98;
    text-wrap: balance;
}

/* Three steps: hero claim, page title, section heading. A page uses at most two of them. */
.display--xl { font-size: var(--text-display); }
.display--md { font-size: var(--text-display-sm); }
.display--sm { font-size: var(--text-display-xs); }

/* The caption idiom gets the display face too, at the other end of the scale: tracked-out micro-labels
   are the same instrument-panel language as the headline, which is what ties the two ends together. */
.caption {
    font-family: var(--font-display);
    font-stretch: 108%;
}

p {
    text-wrap: pretty;
}

/* Numerals in anything measured — credits, sizes, latency, checksums — line up. The desktop reaches for
   Cascadia Mono for exactly this; on the web the font feature does most of the work without a face swap. */
.tabular,
td[data-numeric],
th[data-numeric] {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

code,
kbd,
samp,
pre,
.mono {
    font-family: var(--font-mono);
    font-size: 0.9em;
}

code {
    background-color: var(--overlay-03);
    border-radius: var(--radius-xs);
    padding: 0.1em 0.35em;
}

pre {
    background-color: var(--surface-inset);
    border: 1px solid var(--stroke);
    border-radius: var(--radius-md);
    padding: var(--space-3);
    overflow-x: auto;
}

pre code {
    background: none;
    padding: 0;
}

hr {
    border: 0;
    border-top: 1px solid var(--stroke);
    margin: var(--space-6) 0;
}

/* ── Links ─────────────────────────────────────────────────────────────────────────────────────────── */

a {
    color: var(--accent);
    text-decoration: underline;
    /* Browser-default underlines crowd descenders. This is one of the cheapest tells that a page was
       drawn rather than assembled. */
    text-underline-offset: 0.18em;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--accent-line);
    transition: text-decoration-color var(--motion-fast) var(--ease-out),
                color var(--motion-fast) var(--ease-out);
}

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

/* ── Focus ─────────────────────────────────────────────────────────────────────────────────────────── */

/* One ring, every interactive element, keyboard only. WCAG 2.4.7 is release-blocking here (M10-U11,
   M12-U04) and the desktop has no focus state to port on its primary controls. */
:focus-visible {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
    border-radius: var(--radius-xs);
}

:focus:not(:focus-visible) {
    outline: none;
}

/*
    The consoles' focus ring, restored from underneath the Syncfusion theme (M10-U11).

    fluent2-dark.css carries `.e-control:focus, .e-control *:focus { outline: none }` — a blanket suppression
    over every Syncfusion control AND every descendant of one, measured at line 460 of the shipped theme. Its
    specificity (0,2,0) already beats the bare `:focus-visible` above, and ConsoleLayout injects the theme
    through <HeadContent>, so it also arrives LATER in the cascade than this file. Either fact alone would be
    enough to win. The result is a data grid, a dropdown and a toolbar with no visible keyboard focus, on the
    two surfaces an operator drives entirely from the keyboard.

    AXE CANNOT SEE THIS, which is why the rule needs a comment rather than just existing: focus visibility is
    a property of a computed style under a state no static scan enters, so the console routes pass a WCAG scan
    with the ring absent. It was found by reading the vendor stylesheet, and it is guarded by
    TheConsoleThemeCannotSuppressTheFocusRing rather than by the browser suite.

    `!important` deliberately, and it is the smaller evil. The alternative is escalating specificity against a
    fifteen-thousand-line vendor sheet whose source order we do not control — a race lost on the next theme
    bump, silently, in the direction of no focus indicator.
*/
.e-control:focus-visible,
.e-control *:focus-visible {
    outline: var(--focus-ring) !important;
    outline-offset: var(--focus-offset) !important;
}

/*
    Windows High Contrast, and this is not belt-and-braces — without it the focus ring disappears for
    exactly the people the mode exists for.

    Forced-colors mode replaces the author's palette outright, outline-color included. Both rings above are
    drawn from --focus-ring, which resolves to --accent; under forced colors that becomes whatever the
    system substitutes, and on several shipped themes it lands on or near the background. The result is a
    keyboard user, in the accessibility mode they turned on, with no visible focus at all — a WCAG 2.4.7
    failure invisible to every ordinary browser test, because the palette it depends on is the one no test
    runs under.

    Highlight is the system keyword for the current selection colour, so it tracks whatever theme the user
    chose rather than reintroducing a colour of ours. forced-color-adjust is left alone deliberately: opting
    out of the substitution would put our palette back and defeat the mode.
*/
@media (forced-colors: active) {
    :focus-visible {
        outline: 2px solid Highlight;
        outline-offset: var(--focus-offset);
    }

    .e-control:focus-visible,
    .e-control *:focus-visible {
        outline: 2px solid Highlight !important;
    }

    /* The skip link is revealed by transform, not by colour, so it survives — but its own contrast comes
       from a background token that forced colours will replace. Stating both keeps it legible. */
    .skip-link:focus-visible {
        background: Canvas;
        color: CanvasText;
        outline: 2px solid Highlight;
    }
}

/*
    App.razor focuses the <h1> on every navigation (FocusOnNavigate Selector="h1") so assistive tech
    announces where the route landed. The heading is not keyboard-operable and is not in the tab order,
    so WCAG 2.4.7 — which governs "any keyboard operable user interface" — does not ask it for a focus
    indicator; the ring on the interactive elements above is what satisfies that requirement.

    Suppressing it is not merely permitted, it is necessary. :focus-visible was tried first, on the
    reasoning that a keyboard-driven navigation ought to show the ring — but Chrome matches
    :focus-visible on programmatic focus whenever the last input was a keypress OR there has been no
    interaction at all, and a cold page load is the second case. Every freshly loaded page wore a teal
    box around its heading. Measured on all five routes.
*/
h1:focus {
    outline: none;
}

/* ── Browser surfaces ──────────────────────────────────────────────────────────────────────────────── */

/* The desktop sets SelectionBrush=AccentSoftBrush and CaretBrush=AccentBrush on every TextBox. Same two
   values, same two roles. */
::selection {
    background-color: var(--accent-soft);
    color: var(--text-primary);
}

input,
textarea {
    caret-color: var(--accent);
}

::placeholder {
    color: var(--text-muted);
    opacity: 1; /* Firefox dims placeholders on top of the colour, which would undo the contrast fix. */
}

/* App.xaml retemplates the scrollbar to an 11px gutter over a 7px rail, thumb StrokeStrong -> Secondary
   on hover -> Accent while dragging, and no arrow buttons. Both syntaxes, so the port holds in Firefox. */
* {
    scrollbar-color: var(--stroke-strong) transparent;
    scrollbar-width: thin;
}

::-webkit-scrollbar {
    width: 11px;
    height: 11px;
}

::-webkit-scrollbar-track {
    background: transparent;
}

::-webkit-scrollbar-thumb {
    background-color: var(--stroke-strong);
    border: 2px solid transparent;
    background-clip: content-box;
    border-radius: var(--radius-xs);
}

::-webkit-scrollbar-thumb:hover {
    background-color: var(--text-secondary);
}

::-webkit-scrollbar-thumb:active {
    background-color: var(--accent);
}

::-webkit-scrollbar-corner {
    background: transparent;
}

/* ── Layout primitives ─────────────────────────────────────────────────────────────────────────────── */

.app-shell {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
}

.app-shell > main {
    flex: 1 1 auto;
    /* min-height:0 keeps a long table from forcing the footer off the flex line. */
    min-height: 0;
}

.page-shell {
    width: 100%;
    max-width: var(--page-max);
    margin-inline: auto;
    padding-inline: var(--page-gutter);
}

.page-shell--wide {
    max-width: var(--page-max-wide);
}

.stack > * + * {
    margin-block-start: var(--flow, var(--space-4));
}

.prose {
    max-width: var(--measure);
    color: var(--text-secondary);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
}

.prose > * + * {
    margin-block-start: var(--space-4);
}

/* More space above a heading than below it — the rhythm rule, applied once here rather than per page. */
.prose > :is(h2, h3, h4) {
    margin-block-start: var(--space-8);
    margin-block-end: calc(var(--space-2) * -1 + var(--space-4));
    color: var(--text-primary);
}

.prose > :is(h2, h3, h4) + * {
    margin-block-start: var(--space-2);
}

/* ── Utility ───────────────────────────────────────────────────────────────────────────────────────── */

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.skip-link {
    position: absolute;
    inset-block-start: var(--space-2);
    inset-inline-start: var(--space-2);
    z-index: 100;
    background-color: var(--surface-raised);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    padding: var(--space-2) var(--space-3);
    text-decoration: none;
    transform: translateY(-200%);
}

.skip-link:focus-visible {
    transform: none;
}

.caption {
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-caption);
    text-transform: uppercase;
}

/* ── Label/value rows ──────────────────────────────────────────────────────────────────────────────────
   Global rather than scoped because seven account pages render this same shape inside a CfPanel, and a
   scoped copy per page is seven places to change a row's rhythm. A definition list, because that is what
   these are — the pairing is what a grid of divs would fail to give assistive technology. */

.kv {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: 0;
}

.kv__row {
    display: flex;
    flex-direction: column;
    gap: var(--space-05);
}

.kv dt {
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-caption);
    text-transform: uppercase;
}

.kv dd {
    color: var(--text-primary);
    font-size: var(--text-sm);
    margin: 0;
    /* An account id or a device name has no word breaks and would otherwise widen the panel past its column. */
    overflow-wrap: anywhere;
}

/* The quiet explanatory line under a control. Small, muted, and never the only place a rule is stated. */
.note {
    color: var(--text-muted);
    font-size: var(--text-xs);
    line-height: var(--leading-snug);
}

/* ── Blazor framework surfaces ─────────────────────────────────────────────────────────────────────── */

/*
    The boot splash. It keeps the class name `loading-progress` deliberately: the published prerender
    output is asserted NOT to contain that string (PrerenderedHydrationTests
    .PrerenderedOutputIsRealMarkupAndNotTheSplash), which is what proves the prerender replaced the splash
    with real markup. Renaming the class would leave the assertion passing for the wrong reason.
*/
.loading-progress {
    position: absolute;
    display: block;
    width: 5rem;
    height: 5rem;
    inset: 34vh 0 auto 0;
    margin-inline: auto;
}

.loading-progress circle {
    fill: none;
    stroke: var(--stroke);
    stroke-width: 0.28rem;
    transform-origin: 50% 50%;
    transform: rotate(-90deg);
}

.loading-progress circle:last-child {
    stroke: var(--accent);
    stroke-linecap: round;
    stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
    transition: stroke-dasharray 0.12s ease-in-out;
}

.loading-progress-text {
    position: absolute;
    inset: calc(34vh + 5.75rem) 0 auto 0;
    color: var(--text-muted);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-caption);
    text-align: center;
    text-transform: uppercase;
}

.loading-progress-text::after {
    content: var(--blazor-load-percentage-text, "Loading");
}

/* Blazor's unhandled-error bar. Shipped as `lightyellow`; it is a failure notice, so it takes the danger
   treatment and reads as part of the product. */
#blazor-error-ui {
    display: none;
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 1000;
    background-color: var(--surface-raised);
    border-block-start: 1px solid var(--danger-fill);
    box-shadow: var(--shadow-modal);
    color: var(--text-primary);
    font-size: var(--text-sm);
    padding: var(--space-3) var(--space-6);
}

#blazor-error-ui a.reload {
    color: var(--accent);
    margin-inline-start: var(--space-3);
}

#blazor-error-ui .dismiss {
    display: flex;
    position: absolute;
    inset-block-start: var(--space-2);
    inset-inline-end: var(--space-3);
    color: var(--text-muted);
    cursor: pointer;
    padding: var(--space-1);
}

#blazor-error-ui .dismiss:hover {
    color: var(--text-primary);
}

.blazor-error-boundary {
    background-color: var(--danger-wash);
    border: 1px solid var(--danger-fill);
    border-radius: var(--radius-md);
    color: var(--danger-text);
    font-size: var(--text-sm);
    padding: var(--space-3) var(--space-4);
}

.blazor-error-boundary::after {
    content: "Something went wrong on this page. Reload to try again.";
}

/* ── Scroll reveal ─────────────────────────────────────────────────────────────────────────────────── */

/*
    THE HIDDEN STATE IS GATED ON .js-motion, WHICH ONLY JAVASCRIPT CAN ADD, AND THAT IS THE WHOLE DESIGN.

    These public routes are prerendered to static HTML at publish time — that snapshot is what a search
    engine and the first paint see, before any script runs. A reveal implemented as `opacity: 0` in plain
    CSS would therefore ship a blank page to every crawler and to every visitor whose JS failed, and it
    would look exactly like a working animation to whoever wrote it.

    So the content is visible by default and cf-motion.js opts into hiding it, one frame before it starts
    observing. No JS, no observer, reduced motion, a script error: the page is simply visible.
*/
.js-motion [data-reveal] {
    opacity: 0;
    transform: translateY(18px);
}

.js-motion [data-reveal-item] {
    opacity: 0;
    transform: translateY(14px);
}

.js-motion [data-reveal],
.js-motion [data-reveal-item] {
    transition: opacity var(--motion-reveal) var(--ease-out),
                transform var(--motion-reveal) var(--ease-out);
}

.js-motion [data-reveal].is-revealed,
.js-motion [data-reveal-item].is-revealed {
    opacity: 1;
    transform: none;
}

/*
    The load sequence: the hero's own staggered entrance, on animation rather than on the observer
    because it must start immediately rather than when something scrolls into view.

    The step is carried in the attribute's VALUE, not in an inline style setting a custom property.
    M10-U11's CSP blocks style ATTRIBUTES as well as style elements unless style-src-attr is loosened, so
    an inline `--enter-index` would silently collapse the whole stagger to zero under the shipped policy —
    on the one surface where the choreography is the point. Six steps is more than any hero needs.
*/
.js-motion [data-enter] {
    animation: cf-enter var(--motion-reveal) var(--ease-out) both;
}

.js-motion [data-enter="0"] { animation-delay: 0ms; }
.js-motion [data-enter="1"] { animation-delay: 90ms; }
.js-motion [data-enter="2"] { animation-delay: 180ms; }
.js-motion [data-enter="3"] { animation-delay: 270ms; }
.js-motion [data-enter="4"] { animation-delay: 360ms; }
.js-motion [data-enter="5"] { animation-delay: 450ms; }

@keyframes cf-enter {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: none; }
}

/* ── Reduced motion ────────────────────────────────────────────────────────────────────────────────── */

/* Neither the desktop app nor the reference Blazor app honours this. Every transition here is a colour
   swap standing in for an instant desktop trigger, so removing them returns the original behaviour. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* Belt to cf-motion.js's braces. The script already refuses to add .js-motion under reduced motion,
       but zeroing the duration alone would leave a reveal stuck at opacity 0 if the class ever arrived —
       a "reduced motion" setting that hides the page is worse than the animation it replaced. */
    .js-motion [data-reveal],
    .js-motion [data-reveal-item],
    .js-motion [data-enter] {
        opacity: 1 !important;
        transform: none !important;
        animation: none !important;
    }
}

/*
    ── Syncfusion type (M10-U07) ─────────────────────────────────────────────────────────────────────────

    The palette bridge lives in tokens.css because it is a palette; this does not, because it is a rule
    about elements. Syncfusion sets an explicit font stack on every control it renders, so a grid on a
    CueFlect page would otherwise be the one block of Segoe UI on a site that has chosen its own UI stack —
    which reads as a control pasted in from somewhere else, no matter how well the colours match.

    `.e-control` is on every Syncfusion component and `.e-css` on the ones rendered without a wrapper, so
    the pair is the whole surface. Unscoped, which is why it is in app.css rather than in a scoped
    stylesheet: these elements belong to Syncfusion and carry no component of ours' scope attribute, so a
    scoped rule would compile to a selector matching nothing at all.
*/
.e-control,
.e-css {
    font-family: var(--font-ui);
}

/*
    ── The operations consoles (M10-U07) ─────────────────────────────────────────────────────────────────

    UNSCOPED, and that is the decision rather than the leftovers. Two kinds of rule live here and neither
    can be written in a scoped stylesheet:

      * Anything reaching a Syncfusion element. Those carry SYNCFUSION's markup and none of our
        components' b-xxxxx scope attributes, so a scoped rule naming .e-grid compiles to a selector that
        matches nothing at all — the same trap NoComponentStylesAnotherComponentsIconMarkup exists to
        catch one component further out.
      * The layout classes shared by twelve pages. Duplicating them into twelve scoped stylesheets is how
        the reference implementation ended up with four near-identical empty states.

    No colour literals: every value resolves through the token ladder, including the Syncfusion bridge in
    tokens.css, so re-theming reaches these surfaces like any other.
*/

.console-filters {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: var(--space-4);
    align-items: end;
}

.console-form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: var(--space-4);
    align-items: start;
}

.console-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
    gap: var(--space-4);
}

.console-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-block-start: var(--space-4);
}

/* Inside a grid cell, where the panel's own rhythm does not apply. */
.console-actions--tight {
    gap: var(--space-2);
    margin-block-start: 0;
}

/*
    A panel a control scrolls the reader to, by taking focus.

    THE MARGIN IS THE WHOLE RULE. focus() brings its element into view, but "into view" means flush with the
    viewport edge — and AppHeader is sticky, so a panel scrolled to the top of the window has its heading
    behind the header. The same reservation .prose headings make for the same header.

    NO OUTLINE RESET. The container is tabindex="-1", so it is never a tab stop; browsers draw the default
    ring only for :focus-visible, which means a keyboard user is shown where they landed and a mouse user is
    not. Suppressing it would take that from the one audience the focus move exists for.
*/
.console-focus-target {
    scroll-margin-block-start: calc(var(--header-height) + var(--space-6));
}

/*
    Smooth scrolling, on the consoles and nowhere else.

    THE PROPERTY BELONGS TO THE SCROLLING BOX, NOT THE TARGET, which is why this cannot be written on
    .console-focus-target beside the margin above: the box is <html>. `:has(.console-shell)` — ConsoleLayout's
    root, mounted only while a console route is — is what keeps the rule off the twenty prerendered marketing
    pages, where Router's FocusOnNavigate would animate a jump to the heading on every navigation.

    IT IS THE FOCUS CALL THIS IS FOR. `focus()` performs "scroll an element into view", which honours the
    scrolling box's scroll-behavior — so the Edit buttons move the reader rather than teleporting them, with
    no JS and no second code path to keep in step with the margin.

    A BROWSER WITHOUT `:has()` LANDS ON THE INSTANT JUMP, which is what this replaces and is never wrong,
    only abrupt.

    Reduced motion is answered by the block near the end of this file, which resets scroll-behavior to auto
    with !important — a rule that until now guarded nothing, because nothing set it.
*/
@media (prefers-reduced-motion: no-preference) {
    :root:has(.console-shell) {
        scroll-behavior: smooth;
    }
}

/*
    A table cell carrying a label and a chip about it.

    THE GAP IS THE WHOLE RULE, and it is needed because .cf-chip is inline-flex while the whitespace that
    would separate two inline elements is not emitted: Razor collapses what surrounds an @if block, so a
    conditional chip lands flush against the text before it. An &nbsp; would fix the look and give the pair
    a space that cannot wrap, which in a narrow column is worse than no gap.
*/
.console-cell {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.console-subhead {
    color: var(--text-secondary);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-caption);
    margin-block: var(--space-5) var(--space-2);
    text-transform: uppercase;
}

.console-note {
    color: var(--text-muted);
    font-size: var(--text-xs);
    line-height: var(--leading-relaxed);
    margin-block-start: var(--space-3);
    max-width: var(--measure);
}

/* The one headline number on the business dashboard. Nothing else on a console is set this large, which is
   the point: everything beside it is a supporting count, and the caveat that follows it is deliberately
   `.console-note` so the qualification reads as part of the figure rather than as small print elsewhere. */
.console-figure {
    color: var(--text-primary);
    font-size: var(--text-2xl);
    font-variant-numeric: tabular-nums;
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-display);
    margin: 0;
}

/* The email-template preview frame.

   ITS GROUND IS A TOKEN THAT IS WHITE IN BOTH SCHEMES — see --email-preview-canvas for why an email is
   judged against a mail client rather than against this page.

   A FIXED HEIGHT WITH ITS OWN SCROLL, because the frame's content length is entirely operator-controlled —
   a template being edited towards a hundred kilobytes would otherwise push every control below it off the
   page, including the one that fixes it.

   RESIZABLE, THOUGH, because that default makes a long email something you read through a 512px window. The
   drag handle keeps the safe default and lets it be opened up on demand. It cannot size itself: the frame is
   sandboxed with no allow-scripts and its document is cross-origin to this page, so nothing can report the
   content height back — an auto-growing preview would need exactly the capability the sandbox exists to
   withhold. */
.email-preview {
    background-color: var(--email-preview-canvas);
    block-size: 32rem;
    border: 1px solid var(--stroke);
    border-radius: var(--radius-md);
    inline-size: 100%;
    margin-block-start: var(--space-3);
    resize: vertical;
}

/*
    The same frame inside a dialog, where the height above is wrong in both directions.

    A GLOBAL MODIFIER, NOT A RULE IN CfDialog.razor.css. The frame is rendered by the PAGE and handed to the
    dialog as ChildContent, so it carries the page's scope attribute and not the dialog's — a rule written in
    the dialog's stylesheet would have to reach it with ::deep, which is one component styling another's
    markup. It sits here beside the shape it modifies instead.

    RELATIVE TO THE VIEWPORT because the card around it is capped at the viewport too: a fixed 32rem is short
    on a tall window and taller than the card on a laptop, where it would push the footer out of the scroll
    container it was just given. The resize handle is inherited and still works.
*/
.email-preview--modal {
    block-size: min(58vh, 34rem);
}

.console-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    list-style: none;
    margin: 0;
    padding: 0;
}

.console-list li {
    align-items: center;
    display: flex;
    gap: var(--space-3);
    justify-content: space-between;
}

/*
    A checkbox and its sentence, as one target.

    align-items: start rather than center, because these labels run to two and three lines — the toggle
    that decides whether a support reply reaches the customer restates its own consequence in words, and a
    vertically-centred box beside a three-line sentence reads as belonging to the middle line.
*/
.console-toggle {
    align-items: start;
    color: var(--text-secondary);
    cursor: pointer;
    display: flex;
    font-size: var(--text-sm);
    gap: var(--space-2);
    line-height: var(--leading-snug);
    max-width: var(--measure);
}

.console-toggle input {
    flex: none;
    margin-block-start: 0.15em;
}

/* ── The agent's ticket thread ────────────────────────────────────────────────────────────────────── */

.thread {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    list-style: none;
    margin: 0;
    padding: 0;
}

.thread__item {
    background-color: var(--surface-inset);
    border: 1px solid var(--stroke);
    border-radius: var(--radius-sm);
    padding: var(--space-3) var(--space-4);
}

/*
    An internal note is marked THREE ways — this edge, a chip saying so in words, and the composer's own
    heading — because the single worst thing the agent console can do is let somebody believe a note is
    private when it is not, or the reverse. Two of those three survive greyscale.
*/
.thread__item--note {
    background-color: var(--warning-wash);
    border-inline-start: 3px solid var(--warning);
}

.thread__meta {
    align-items: center;
    color: var(--text-muted);
    display: flex;
    flex-wrap: wrap;
    font-size: var(--text-2xs);
    gap: var(--space-2);
}

.thread__body {
    color: var(--text-primary);
    line-height: var(--leading-relaxed);
    margin-block-start: var(--space-2);
    /* The customer wrote this in a plain-text box, so its line breaks are meaning rather than wrapping. */
    white-space: pre-wrap;
}

/*
    The attachment list under the agent's thread.

    HERE RATHER THAN IN A SCOPED SHEET because the rest of this .thread family already is, and the console
    reads it: SupportTicket.razor.css carries its own .thread__heading and .thread__files, but CSS isolation
    scopes those to that component's attribute, so the agent page could not see them and the list rendered as
    unstyled bullets. The duplication is the isolation boundary, not an oversight.
*/
.thread__heading {
    color: var(--text-primary);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    margin-block-start: var(--space-6);
}

.thread__files {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    list-style: none;
    margin: var(--space-2) 0 0;
    padding: 0;
}

.thread__files li {
    align-items: baseline;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.canned__key {
    color: var(--text-muted);
    font-size: var(--text-2xs);
}

.canned__body {
    color: var(--text-secondary);
    line-height: var(--leading-relaxed);
    margin-block-start: var(--space-2);
    white-space: pre-wrap;
}

/* The content catalog's list rows (M10-U08). Global, beside .canned__* which they mirror: the same
   panel-per-item shape, so a scoped copy would be a second place to change one rhythm. */

.content-route {
    color: var(--text-muted);
    font-size: var(--text-2xs);
}

.content-blurb {
    color: var(--text-secondary);
    line-height: var(--leading-relaxed);
    margin-block-start: var(--space-2);
}

/* The editor's preview. .prose already sets the measure and the rhythm; what this adds is a ceiling, because
   an operator pasting a 2,000-word guide into the form should not have to scroll past the whole rendered
   article to reach the Save button that is above it. */
.content-preview {
    max-block-size: 32rem;
    overflow-y: auto;
}

/* ── The Syncfusion grid, on this product's surfaces ──────────────────────────────────────────────── */

/*
    The palette comes from the bridge in tokens.css; what is left here is the handful of decisions the
    theme's variables cannot express — the panel already draws the outer border, and a grid that draws its
    own inside a Flush panel produces the doubled hairline the desktop's own tables avoid.
*/
.console-grid.e-grid {
    border: none;
}

.console-grid.e-grid .e-gridheader {
    border-block-start: none;
}

/*
    THE GRID PRINTS AN INTERNAL ID AT THE TOP OF ITSELF, AND ONLY THE VENDOR THEME HIDES IT.

    Syncfusion's Blazor grid renders a <caption> holding "{id}_header_table" inside its header table, where
    {id} is the identifier Blazor generates for a component given none — so what lands on screen is a
    string like sfgridlltbbhlbraj_header_table, above the queue, in the panel's own body. fluent2-dark.css
    carries NO RULE FOR THE CAPTION ELEMENT AT ALL; it hides that one through `.e-grid .e-hide`, which is a
    class on vendor markup and a selector in a 4.9 MB stylesheet this app injects late, from a layout, on
    two routes. Two things have to hold for the text to stay hidden and neither of them is ours.

    HIDING IT IS AN ACCESSIBILITY IMPROVEMENT, not a loss, which is worth saying because a <caption> is
    normally a table's accessible name. This one is not a name: it is an internal element id, and a screen
    reader reads it aloud, character group by character group, before the first ticket.

    Unscoped, like the focus-ring counter-rule further up this file and for the same reason — it is a rule
    about SYNCFUSION's markup rather than about the console layout, so it holds for any grid this app ever
    mounts instead of only for the ones that remembered a class.
*/
.e-grid caption {
    display: none;
}

/*
    ONE GUTTER. THERE WERE THREE.

    A console grid sits in a `Flush` panel — body padding removed precisely so the table can meet the
    panel's edges and set the inset itself — and the vendor sets one that agrees with nothing else on the
    page: 8px on the cells, 0 on the pager, against the 12px `.cf-panel__head` gives the title directly
    above them. A panel whose heading, whose first column and whose pager each start at a different place
    reads as broken before it reads as anything, and no single one of the three looks wrong on its own.

    --space-3 because that is what CfTable's cells already use. This is the app's one table gutter, not a
    third opinion about what a table gutter should be.
*/
.console-grid.e-grid :is(.e-rowcell, .e-headercell),
.console-grid.e-grid .e-pager {
    padding-inline: var(--space-3);
}

/*
    Column labels in the section-label idiom: the same 11px SemiBold muted caps CfTable's header row and
    `.cf-panel__title` are set in. The vendor's is 14px regular sentence case, and that mismatch is the
    loudest tell that this block came from another product — the panel says TICKETS in one voice directly
    above a row that says Priority in another.
*/
.console-grid.e-grid .e-headercell {
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-caption);
    padding-block: var(--space-2);
    text-transform: uppercase;
}

/*
    A queue is read down a column, so rows want to be scannable rather than airy.

    text-overflow COMPLETES A RULE THE VENDOR LEFT TWO-THIRDS WRITTEN. Its rowcell is `white-space: nowrap`
    with `overflow: hidden` and no third declaration, so a subject wider than its column is cut mid-glyph
    and reads as a subject that ends there — the failure is that it looks like data rather than like
    truncation. Tabular figures are the same defect one level down: a column of instants that do not line
    up cannot be scanned, which is the only thing a due-date column is for.
*/
.console-grid.e-grid .e-rowcell {
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    text-overflow: ellipsis;
}

/* The pager as the panel's base, under a hairline, against the raised header strip's lid. CfPanel's
   two-tone split is what makes these read as containers with a lid rather than as cards, and a paged grid
   is the one place something other than CfPanel draws the bottom of the container. */
.console-grid.e-grid .e-pager {
    border-block-start: 1px solid var(--stroke);
}

/*
    The Router's <Navigating> content, shown only while a console's deferred assemblies are in flight.

    Deliberately not a CfSkeleton: this renders in place of the whole page, so the skeleton it would want
    lives inside a component that has not loaded yet. One line of status text is the honest shape for a
    state that lasts a fraction of a second on a warm cache and is never reached on a marketing route.
*/
.route-loading {
    color: var(--text-muted);
    font-size: var(--text-sm);
    padding: var(--space-16) var(--page-gutter);
    text-align: center;
}

/* ── Long-form reading (M10-U08) ────────────────────────────────────────────────────────────────────────

   GLOBAL RATHER THAN SCOPED, and the reason is DESIGN.md's scoped-CSS trap rather than convenience. The
   article body is rendered by ContentProse and its inline runs by ContentRuns, so every element below
   carries one of those two components' scope attributes — a rule written in the page's own stylesheet
   would compile to `.prose-list[b-page]` and match nothing. The same argument the `.kv` rows are global
   for: one shape rendered by several components wants one home.

   `.prose` itself, its measure and its flow rhythm are already defined above. What follows is only what
   an ARTICLE adds to a page of prose: the elements a marketing page never renders. */

.prose > :is(h2, h3, h4) {
    scroll-margin-block-start: calc(var(--header-height) + var(--space-6));
}

.prose a {
    color: var(--text-primary);
    text-decoration: underline;
    text-decoration-color: var(--accent-line);
    text-underline-offset: 0.2em;
    transition: text-decoration-color var(--motion-fast) var(--ease-out);
}

.prose a:hover {
    text-decoration-color: var(--accent);
}

.prose-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
    padding-inline-start: var(--space-5);
}

.prose-list li::marker {
    color: var(--text-muted);
}

/* Inline code sits INSIDE running text, so it takes a wash rather than a box: a bordered chip mid-sentence
   breaks the line's rhythm at every occurrence, and these articles name types and settings constantly. */
.prose-code {
    background-color: var(--surface-inset);
    border-radius: var(--radius-xs);
    color: var(--text-primary);
    font-family: var(--font-mono);
    font-size: 0.92em;
    padding: 0.1em 0.35em;
}

.prose-pre {
    background-color: var(--surface-sunken);
    border: 1px solid var(--stroke);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: var(--leading-normal);
    margin: 0;
    overflow-x: auto;
    padding: var(--space-4);
}

/* A quotation is set off by a lit edge rather than by an indent: the edge is this system's depth device,
   and an indented block at a 68ch measure simply reads as a narrower paragraph. */
.prose-quote {
    border-inline-start: 2px solid var(--accent-line);
    color: var(--text-primary);
    margin: 0;
    padding-inline-start: var(--space-4);
}

.prose-quote p {
    margin: 0;
}

/* ── An FAQ entry ───────────────────────────────────────────────────────────────────────────────────────

   <details>, which DESIGN.md already names as this system's accessible collapse mechanism — it is what the
   reference footer's hover drawer was measured against and rejected for. It costs no script, which matters
   on a route whose prerendered snapshot is what a crawler reads: the questions and answers are in the
   markup whether or not anything runs. */

.qa {
    background-color: var(--glass);
    border: 1px solid var(--stroke);
    border-radius: var(--radius-md);
    box-shadow: var(--edge-lit);
}

.qa__q {
    align-items: center;
    color: var(--text-primary);
    cursor: pointer;
    display: flex;
    font-weight: var(--weight-semibold);
    gap: var(--space-3);
    justify-content: space-between;
    list-style: none;
    padding: var(--space-4);
}

/* Safari still draws its own triangle through ::-webkit-details-marker even with list-style: none. */
.qa__q::-webkit-details-marker {
    display: none;
}

.qa__q:hover {
    background-color: var(--overlay-01);
}

.qa__text {
    text-wrap: pretty;
}

.qa__chev {
    color: var(--text-muted);
    display: inline-flex;
    flex: none;
    transition: transform var(--motion-fast) var(--ease-out);
}

.qa[open] .qa__chev {
    transform: rotate(180deg);
}

.qa__a {
    border-block-start: 1px solid var(--stroke);
    color: var(--text-secondary);
    padding: var(--space-4);
}

.qa__a p {
    margin: 0;
}

/* ── The entry list ─────────────────────────────────────────────────────────────────────────────────────

   A LIST WITH HAIRLINES, NOT A CARD GRID. Home.razor's own comment records the refusal, and this is the
   page shape that invites it hardest. A list also lets the blurb be a full sentence, which is what makes
   an index useful; a card forces it to a length that says nothing.

   Global because the section index and the library hub both render it, and two scoped copies is two places
   to change one rhythm. */

/*
    Capped at the measure like everything else on a reading surface. Without it the row — and its hover
    band — spanned the full 1040px shell while the blurb inside stopped at 550, which reads as a half-empty
    highlight on a wide screen and left the lists visibly out of line with the section heads above them,
    which were already at the measure.
*/
.entries {
    display: flex;
    flex-direction: column;
    list-style: none;
    margin: 0;
    max-width: var(--measure);
    padding: 0;
}

.entries__item + .entries__item {
    border-block-start: 1px solid var(--stroke);
}

.entries__link {
    display: block;
    color: inherit;
    padding: var(--space-5) var(--space-4);
    text-decoration: none;
    transition: background-color var(--motion-fast) var(--ease-out);
}

.entries__link:hover {
    background-color: var(--overlay-01);
}

.entries__date {
    color: var(--text-muted);
    margin: 0 0 var(--space-2);
}

.entries__title {
    color: var(--text-primary);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-snug);
    margin: 0;
    text-wrap: balance;
}

.entries__link:hover .entries__title {
    color: var(--accent);
}

.entries__blurb {
    color: var(--text-secondary);
    font-size: var(--text-md);
    line-height: var(--leading-normal);
    margin: var(--space-2) 0 0;
    max-width: var(--measure);
    text-wrap: pretty;
}

.entries--compact .entries__link {
    padding-block: var(--space-4);
}
