/*
 * Playground styling. Every colour, space and type value comes from
 * packages/design-tokens/tokens.css — nothing is declared literally here, so the
 * playground cannot drift away from the site's look.
 */

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

/*
 * A page reserves its scrollbar's width whether or not that scrollbar is drawn — the same rule,
 * for the same reason, as the site's own base.css. It is not cosmetic here: the playground is a
 * short page and /why-justdummies is a tall one, so without it the two documents are laid out
 * in viewports fifteen pixels apart and every centred measure below lands seven and a half
 * pixels off the page it is supposed to match.
 */
html {
    scrollbar-gutter: stable;

    /*
     * Room at the foot of the scrollport for the floating download link, which is fixed to the
     * bottom-right corner of every page here and never leaves it (WCAG 2.2 SC 2.4.11, AA).
     *
     * The copy button is right-aligned in the code bar, so below the shell's own width the two
     * stand in one column: measured at 1000x700, the button occupies y=621..653 and the link
     * y=628..676, with the page unscrolled and covered whole. `main`'s own padding cannot
     * answer that — it is flow padding, which a fixed element ignores.
     *
     * Scroll padding does not only act on focus moves that already scroll. It shrinks the
     * optimal viewing region, so the browser scrolls a control that is technically inside the
     * viewport but sitting in the band, which is exactly the case that failed. Verified in the
     * browser: with this line the button lands clear and the link stays clickable — which a
     * rule that faded the link out did not manage, since a pointer press moves focus and CSS
     * cannot tell that gesture from a keyboard one mid-click.
     *
     * `base.css` says the same thing for the site, with the consent banner's height added.
     */
    scroll-padding-block-end: calc(var(--jd-space-6) + 3.5rem);
}

/*
 * Room at the foot of the document for the floating download link, which is `position: fixed`
 * and therefore invisible to the layout. The scroll padding above moves a control clear when
 * there is somewhere left to scroll to; at the very foot of the document there is not, so the
 * last controls a Tab pass reaches would sit under the link with nothing able to move them.
 * The site says the same thing in `base.css`, where the consent banner's height is added too.
 */
body {
    padding-block-end: calc(var(--jd-space-6) + 3.5rem);
    margin: 0;
    background: var(--jd-background);
    color: var(--jd-text-primary);
    font-family: var(--jd-font-sans);
    font-size: var(--jd-text-base);
    line-height: var(--jd-line-body);
}

/*
 * How a link is drawn, declared once — the same two rules the site's base.css opens with.
 *
 * Duplicated here because the two applications share no stylesheet, and absent here until the
 * header had to match the site's: every link this file did not name individually was rendering
 * in the user agent's own blue, which on --jd-background is barely a colour at all.
 */
a {
    color: var(--jd-accent);
    text-underline-offset: 0.2em;
}

a:focus-visible,
button:focus-visible {
    outline: 2px solid var(--jd-text-primary);
    outline-offset: 2px;
}

/*
 * The page's measure and the air around it — the site's `main` and `.shell` (Page.astro and
 * base.css), to the number.
 *
 * WHY 72rem HERE AND 60rem ON THE HEADER. They are two different measures on the site too: the
 * bar is set to 60rem and the column under it to 72rem, and a playground that folded them into
 * one 60rem box — which is what `.page` used to be — put its every word 103 pixels right of
 * where /why-justdummies puts the same word. The numbers are copied rather than reasoned about
 * a second time; the day the site changes one, this changes with it.
 */
main {
    padding-block: var(--jd-space-8) var(--jd-space-16);
}

.shell {
    max-width: 72rem;
    margin-inline: auto;
    padding-inline: var(--jd-space-4);
}

/*
 * The column the page's own words are set in, inside that shell — the same 60rem the site's
 * comparative page gives its `.why` section, so a line breaks in the same place on both.
 */
.page-body {
    max-width: 60rem;
}

/*
 * A short screen and a narrow one both give the top back, exactly as they do on the site
 * (Page.astro). Two queries rather than one because they answer different facts: a phone is not
 * a short screen, it is a narrow one.
 */
@media (max-width: 47.99rem), (max-height: 820px) {
    main {
        padding-block-start: var(--jd-space-4);
    }
}

/*
 * The playground's own copy of the site's footer (Layout/SiteFooter.razor), styled
 * from the same tokens as apps/site/src/components/Footer.astro so the two read as
 * one footer rather than two designs. Static in the document flow, never fixed to the
 * viewport — same rule as the Astro site's.
 */
.site-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--jd-space-4);
    max-width: 72rem;
    margin: 0 auto;
    padding: var(--jd-space-6) var(--jd-space-4);
    border-top: 1px solid var(--jd-border);
    color: var(--jd-text-muted);
    font-size: var(--jd-text-sm);
}

.footer-nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--jd-space-4);
}

.footer-group {
    display: flex;
    align-items: center;
    gap: var(--jd-space-4);
}

.footer-nav a {
    color: var(--jd-text-muted);
}

.footer-nav a:hover {
    color: var(--jd-text-primary);
}

.copyright {
    margin: 0;
}

/* Visible to a screen reader, absent from the visual design — same rule as the site's
   own base.css, duplicated here because the two applications share no stylesheet. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Blazor's router focuses the page's own title on navigation (App.razor's
   `FocusOnNavigate Selector=".page-title"`) so a screen reader lands on what
   actually changed rather than on the brand it already knows. That focus move
   is what this rule is about, and the rule's whole job is to make it invisible.

   NOTHING IS DRAWN HERE, AND `outline: none` IS THE ONLY WAY TO SAY SO. Left
   unstyled, the heading takes the browser's own ring — a thick box drawn the
   full width of the measure around "Playground" — and `FocusOnNavigate` fires
   on the first render, so every visitor arriving at the playground meets that
   box without having navigated anywhere. The site's own subtitle carries
   nothing of the kind, and the two are meant to be the same page.

   WHAT IT COSTS, STATED PLAINLY, because it is not nothing. Blazor focuses
   this heading through `tabindex="-1"`, so it is out of the tab order and
   nobody reaches it by tabbing — but two links do route in-app and land focus
   here afterwards: "Start over" on the Not Found page, and the bar's own
   "Playground" entry. A keyboard reader following either one is no longer
   shown where focus went, and where their next Tab begins. That is the trade,
   made knowingly: the ring was wrong on the path every visitor takes and right
   on two almost nobody does, so it goes. WCAG 2.4.7 is not in question either
   way — it governs the indicator on components keyboard focus can be moved to
   by the reader, and this heading is not one.

   What is unaffected: the screen reader announcement, which comes from the
   focus moving rather than from anything painted around it, and every control
   a keyboard can actually reach — those still show their own ring, from the
   `a:focus-visible` / `button:focus-visible` rule above and the per-control
   rules further down. */
.page-title:focus {
    outline: none;
}

/* The page's opening line, drawn as the site draws the same line —
   WhyJustDummiesContent.astro's `.lede` for both the type and the measure. FULL
   WIDTH AND text-lg BY THE SAME EDITORIAL DECISION THE SITE MADE, and load-bearing
   this time rather than restated for its own sake: tests/browser/chrome-parity.spec.ts
   measures the element right after this page's `<h2>` against the one right after
   /why-justdummies's, so whatever sits there has to keep this exact rule, one
   confident sentence at a time — the fuller explanation belongs in `.lede-detail`
   below, not here. */
.lede {
    font-size: var(--jd-text-lg);
    line-height: var(--jd-line-body);
    color: var(--jd-text-primary);
    margin: 0 0 var(--jd-space-4);
}

/* The rest of the opening explanation, after the one-line `.lede` above — normal
   body size, folded to a classic 38rem measure, the way an ordinary paragraph
   reads rather than a display line. */
.lede-detail {
    max-width: 38rem;
    font-size: var(--jd-text-base);
    line-height: var(--jd-line-body);
    color: var(--jd-text-primary);
    margin: 0 0 var(--jd-space-4);
}

/* Same treatment as the site's own ApiIndexContent.astro `.english-note` — a caveat, not part
   of the pitch, so it drops out of the reading flow: muted, smaller, set off by a rule rather
   than sized and colored like the lede text above it. */
.playground-note {
    max-width: 38rem;
    font-size: var(--jd-text-sm);
    line-height: var(--jd-line-body);
    color: var(--jd-text-muted);
    border-left: 2px solid var(--jd-generated);
    padding-left: var(--jd-space-3);
    margin: 0 0 var(--jd-space-4);
}

/* The pre-runtime shell. Styled from the same tokens so the boot state and the
   booted page are the same page, not two designs — which is why it takes the shell's
   measure rather than one of its own: the word "Playground" would otherwise start at
   one x before the runtime lands and at another after it, and a reader watching that
   happen sees the page move for no reason it can name. */
.boot {
    max-width: 72rem;
    margin: 0 auto;
    padding: var(--jd-space-12) var(--jd-space-4);
    font-family: var(--jd-font-sans);
}

.boot-title {
    font-size: var(--jd-text-xl);
    line-height: var(--jd-line-tight);
    margin: 0 0 var(--jd-space-4);
}

.boot-note {
    color: var(--jd-text-muted);
    margin: 0;
}

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    z-index: 1000;
    padding: var(--jd-space-3) var(--jd-space-4);
    background: var(--jd-surface-raised);
    border-top: 2px solid var(--jd-error);
    color: var(--jd-text-primary);
    box-shadow: var(--jd-shadow-raised);
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: var(--jd-space-3);
    top: var(--jd-space-3);
}

/*
 * THE CODE CARD — the one box both halves of the product show a chain in.
 *
 * Two widgets wear it: the landing page's hero (§9.8, Pages/Hero.razor, framed by the site
 * at /hero) and the playground itself (Pages/Home.razor). They show different things — the
 * hero runs one compiled chain with movable arguments, the playground builds a chain of the
 * visitor's own — but they are the same card, and everything a visitor can measure between
 * them is declared here once rather than twice: the box, the padding, the line-height, the
 * row of controls under it, the button in that row, and the gap between the two.
 *
 * IT USED TO BE `.hero-widget` ALL THE WAY DOWN, and the playground drew its own unrelated
 * shapes. That is what let the two drift: the hero's card sat on --jd-surface with a
 * --jd-radius-md border while the playground had a bare column of <select>s, and nothing in
 * either file would have noticed if one had moved. Named once, the parity is structural —
 * tests/browser/code-card-parity.spec.ts then only has to check that the two ended up the
 * same width on the page, which is the one thing a shared class cannot state on its own.
 *
 * What stays widget-specific stays below, under `.hero-widget` / `.playground-widget`.
 */
.code-widget {
    font-family: var(--jd-font-mono);
    font-size: var(--jd-text-sm);
}

/* The same box as the site's static sample (LiveHero.astro's .sample): its own border,
   radius and background, clipping the expression and the result bar to rounded corners.
   This used to be background-only, with the border living on .frame in the parent
   document instead — the div that holds this iframe — which meant that border wrapped
   the "Generate"/version row below the card too, a row the static example never draws a
   box around (its equivalent, .invite, sits free underneath .sample). Moved here so only
   the card is boxed, same as the page it is matching. */
.code-widget .card {
    background: var(--jd-surface);
    border: 1px solid var(--jd-border);
    border-radius: var(--jd-radius-md);
    overflow: hidden;
}

/* Just the text inside the card — .card already draws the box.

   The line-height is the number LiveHero.astro's own <pre> copies, not the other way
   round: both widgets underline editable fields, and a tighter line here read as a size
   change the moment the live widget replaced the static sample. */
.code-widget .expression {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: var(--jd-space-4); /* = the padding of the site's <pre> */
    line-height: 2.1;
    word-break: break-word;
}

/*
 * WHAT IS THE HERO'S ALONE (§9.8, Pages/Hero.razor): the depth its continuation lines are
 * indented to, and the cap hint under the card. Everything else it draws is `.code-widget`
 * above, shared with the playground.
 */

/* Every line but the first, indented to land its "." under the one right after "Any" on
   the first line — the same 22 columns as the site's own snippet, since this widget prints
   "string reference = " too: 19 characters for that, plus 3 for "Any". The playground
   indents to 3 for the same reason and a shorter prefix; see .playground-widget below. */
.hero-widget .expression > :not(:first-child) {
    padding-left: 22ch;
}

/* Below the width the site's own header stacks at (Base.astro) — same breakpoint as
   .site-header's own narrow query, further down this file.
   22ch alone is over half of a 375px phone's usable width before a single method name or
   input has been laid out, and this element cannot shrug that off the way the static
   example does: .sample's <pre> is `overflow-x: auto` and never rewraps, while this one is
   `word-break: break-word` because its lines hold real <input> elements a visitor has to
   reach, not read-only text a scrollbar can hide part of. Left at 22ch here, every
   continuation line broke across several more of its own, and the card that is supposed to
   match the static example on desktop grew taller than it on the phone that opens it. */
@media (max-width: 28rem) {
    .hero-widget .expression > :not(:first-child) {
        padding-left: 3ch;
    }
}

/*
 * The same vocabulary `base.css` colours from `highlight.ts`'s output on the site — see
 * the comment there. Duplicated here because the two applications share no stylesheet
 * (same reason `.visually-hidden` and the footer nav below are duplicated too). Neither
 * widget's expression can be run through `highlight()`: some of its "tokens" are `<input>`
 * elements and `<select>`s, not text a tokeniser can read — so the classes are placed by
 * hand instead, but they are the same classes over the same design-token colours, so every
 * code block in the product reads as one language.
 */
.tok-keyword {
    color: var(--jd-code-keyword);
}

.tok-type {
    color: var(--jd-code-type);
}

.tok-member {
    color: var(--jd-code-member);
}

.tok-string {
    color: var(--jd-code-string);
}

.tok-number {
    color: var(--jd-code-number);
}

/* The playground's doc lines (ChainLink.razor). Same colour `highlight()` gives a `//` on
   the site, so a summary lifted from the library's XML documentation reads as what it is:
   a comment sitting in the code, not a caption bolted under it. */
.tok-comment {
    color: var(--jd-code-comment);
}

/* Parentheses, commas and the trailing semicolon: present, and deliberately quiet. They
   are the punctuation a reader's eye skips over in real code, and colouring them like the
   names they surround is what made the playground's calls read as a wall. */
.tok-punctuation {
    color: var(--jd-text-muted);
}

/* Editable, and looking it. An argument styled like its surrounding code reads as code
   and never gets clicked; one styled like a form control in a code block reads as a
   mistake. This sits between: the code's own token colour (.tok-string/.tok-number,
   above), with a floor under it. The floor is `currentColor` rather than a colour of its
   own, so it always matches whichever token class is set on the same input.

   The width is set by the `[data-width]` rules below, sized to what the field actually
   holds rather than to the widest value it could ever legally hold — which is what put
   "ORD-" in a box built for ten characters and stranded the quotes drawn around it several
   characters past the text. `min-width` stays here as a floor under the smallest of them,
   so an empty field is still a click target rather than a sliver. */
.code-widget input {
    font: inherit;
    background: transparent;
    border: 0;
    border-bottom: 1px dashed currentColor;
    padding: 0 var(--jd-space-1);
    min-width: 3ch;
    /* NEVER WIDER THAN THE CARD, whatever the ladder below asks for. A Guid is 36 characters, so
       FieldWidth reaches for the 40ch rung — around 300px, more than a 375px phone has left once
       the card's padding, the step's indent and `.DifferentFrom(` have taken their share. The card
       clips (`overflow: hidden`, for its rounded corners), so the overflow was not scrolled but
       cut: the end of the field, and with it the caret, went off the edge and stayed there while
       the visitor typed. Capped, the field stops at the card and wraps onto a line of its own —
       an <input> is an atomic box that cannot break, but it can move. */
    max-width: 100%;
}

/* The static example prints "12" as plain text, with nothing either side of it — the
   number field's own spin buttons are this browser's addition, not part of the
   expression. Left in, they also don't fit: FieldWidth sizes the box to the digits
   alone, and a box that narrow shows the buttons instead of the digits they would
   otherwise sit beside. */
.code-widget input[type='number'] {
    -moz-appearance: textfield;
}

.code-widget input[type='number']::-webkit-outer-spin-button,
.code-widget input[type='number']::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/*
 * How wide an argument field renders, in `ch`, written as an attribute rather than into a
 * `style`: the site's CSP is `style-src 'self'` with no `unsafe-inline`, so a computed
 * inline width would be parsed, dropped, and never applied — see Hero.razor's FieldWidth
 * for the whole story. One rule per column is what a CSP without `unsafe-inline` costs a
 * value that would otherwise be one `calc()` away.
 *
 * WHY THE LADDER GOES SPARSE AFTER 12. The hero's own fields cap at ten characters, so it
 * never asks past 12 and every column below that is exact for it. The playground's do not
 * cap: a Guid argument is 36 characters and a DateTimeOffset is around 33, and a rule per
 * column up to 40 would be forty rules to spare a handful of values three columns of slack.
 * Past 12 the callers round up to the next rung instead (ChainLink.razor's FieldWidth), so
 * a long value sits in a field a little wider than itself rather than in one that clips it.
 */
.code-widget input[data-width='1'] {
    width: 1ch;
}

.code-widget input[data-width='2'] {
    width: 2ch;
}

.code-widget input[data-width='3'] {
    width: 3ch;
}

.code-widget input[data-width='4'] {
    width: 4ch;
}

.code-widget input[data-width='5'] {
    width: 5ch;
}

.code-widget input[data-width='6'] {
    width: 6ch;
}

.code-widget input[data-width='7'] {
    width: 7ch;
}

.code-widget input[data-width='8'] {
    width: 8ch;
}

.code-widget input[data-width='9'] {
    width: 9ch;
}

.code-widget input[data-width='10'] {
    width: 10ch;
}

.code-widget input[data-width='11'] {
    width: 11ch;
}

.code-widget input[data-width='12'] {
    width: 12ch;
}

.code-widget input[data-width='14'] {
    width: 14ch;
}

.code-widget input[data-width='16'] {
    width: 16ch;
}

.code-widget input[data-width='20'] {
    width: 20ch;
}

.code-widget input[data-width='24'] {
    width: 24ch;
}

.code-widget input[data-width='28'] {
    width: 28ch;
}

.code-widget input[data-width='32'] {
    width: 32ch;
}

.code-widget input[data-width='36'] {
    width: 36ch;
}

.code-widget input[data-width='40'] {
    width: 40ch;
}

.code-widget input:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
}

/* The result — a value or a refusal — in the same shape as the site's own <figcaption>:
   a filed second zone under a divider, not a block of its own further down the page. */
.code-widget .result-bar {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--jd-space-2);
    padding: var(--jd-space-3) var(--jd-space-4); /* = the padding of <figcaption> */
    border-top: 1px solid var(--jd-border);       /* = the rule under <figcaption> */
    background: var(--jd-surface-raised);         /* = the raised ground of <figcaption> */
}

.code-widget .result-bar .arrow,
.code-widget .result-bar .label {
    color: var(--jd-text-muted);
}

/* The drawn value, at the size of the row it shares with the arrow and the label rather
   than at a heading's — it is a caption's worth of text, not a banner. */
.code-widget .result-bar .value {
    color: var(--jd-generated);
}

/* Nothing drawn yet: the playground opens on an empty chain, so this bar has a state the
   hero never has. Muted and in the sans face, because it is the page talking about the
   card rather than anything the card produced. */
.code-widget .result-bar .pending {
    color: var(--jd-text-muted);
    font-family: var(--jd-font-sans);
}

/* The refusal is the library defending the demonstration (§9.9), so it is legible and
   plain — the error colour on the text, and nothing around it that reads as an alarm.

   It sits behind the same arrow and label a value does. That is a reversal: the arrow used
   to be withheld here, on the reasoning that "→" promises a value and a refusal is not one.
   True, and beside the point — the bar then changed shape between two presses of the same
   button, which is a bigger claim than the arrow was ever making, and the colour says which
   of the two came back without the layout having to. */
.code-widget .result-bar .refusal {
    color: var(--jd-error);
    font-family: var(--jd-font-sans);
    line-height: var(--jd-line-body);
}

/* Outside the card, at the same place and the same size as the site's own "Run" button and
   its cost mention (LiveHero.astro's .invite) — the visitor's eye should not have to find
   them again after the click. */
.code-widget .controls {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--jd-space-3);
    margin-top: var(--jd-space-4);
}

.code-widget .generate {
    font: inherit;
    color: var(--jd-text-primary);
    background: var(--jd-surface-raised);
    border: 1px solid var(--jd-border);
    border-radius: var(--jd-radius-sm);
    padding: var(--jd-space-2) var(--jd-space-4); /* = the padding of .run, for "same size" */
    cursor: pointer;
}

.code-widget .generate:hover:not(:disabled) {
    border-color: var(--jd-accent);
}

/* The playground can offer Generate before the chain is drawable (an empty chain, an
   argument that will not parse); the hero's is only ever disabled between a cleared length
   field and the next keystroke. Same treatment either way. */
.code-widget .generate:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.code-widget .live {
    color: var(--jd-text-muted);
    font-family: var(--jd-font-sans);
}

/* This playground's own cap, not the library's — see the file header comment on why it
   surfaces the instant a keystroke hits it rather than waiting for Generate. Muted rather
   than the refusal's error colour: it is a rail, not a rejection. */
.hero-widget .hint {
    display: block;
    margin-top: var(--jd-space-4);
    color: var(--jd-text-muted);
    font-family: var(--jd-font-sans);
    line-height: var(--jd-line-body);
}

/* Always in the DOM now (Hero.razor), for the live region to work — so empty is a real,
   frequent state, not a corner case, and it must take no space in it. */
.hero-widget .hint:empty {
    margin-top: 0;
}

/*
 * SiteHeader.razor and BrandHeading.razor — the standalone playground's own chrome, carried
 * by MainLayout above every routed page (never above /hero, which stays under BareLayout: it
 * is framed inside a page that already has this header). Same tokens, same shapes as the
 * site's Base.astro and BrandHeading.astro, so the two halves of the product read as one —
 * without sharing markup, because an Astro island and a Blazor component cannot share a file.
 */
.site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--jd-space-4);
    max-width: 60rem;
    margin: 0 auto;
    padding: var(--jd-space-4);
    font-family: var(--jd-font-sans);
}

/*
 * FOUR THINGS HERE ARE COPIES, NOT CHOICES, and each of them was a visible disagreement with
 * the site before it was copied. The measure and the centring: without them this bar ran the
 * full width of the window and put "JustDummies" at x=16 against the site's x=248. The absent
 * border: the site draws none, and the one that used to be here pushed everything below it a
 * pixel down. The absent `font-size`: it was set on the header rather than on the nav, so the
 * brand was rendered at the small size and the site's is not. The weight: 600, the site's, not
 * the 700 that was here. Change one of them the day Base.astro changes it, and not before.
 */
.brand {
    color: var(--jd-text-primary);
    font-weight: 600;
    text-decoration: none;
}

/* The nav carries the small size, not the header — so the brand beside it keeps the body
   size the site gives it. Its links take their colour and their underline from the `a`
   rule above, exactly as the site's do: nothing here restates them. */
.site-nav {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--jd-space-4);
    font-size: var(--jd-text-sm);
}

/* Tighter around the rule than between the links, so the rule reads as separating two
   groups rather than as a third item with space of its own. */
.site-nav > a {
    margin-right: var(--jd-space-2);
}

.divider {
    width: 1px;
    align-self: stretch;
    min-height: 1rem;
    background: var(--jd-border);
}

/* Base.astro's own breakpoint, and the same number rather than one measured again: this bar
   carries the same brand and the same five items as the site's, so the width at which they
   stop fitting on one line is the site's width. It used to carry three items and break at
   30rem, which meant the two headers stacked at different windows — one more way for them to
   disagree.

   32rem until "Docs" joined the row. Base.astro carries the re-measurement and the whole of
   the reasoning for 36 — including why it sits a little further above the measured fit than
   32 did, and what would have to be re-measured to tighten it. Read it there; this is the
   copy, and a second account of the same number here would be a second thing to keep true.
   Move this the day that one moves, and not a day apart from it: two headers that stack at
   different windows is the disagreement this file exists to prevent. */
@media (max-width: 36rem) {
    .site-header {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--jd-space-2);
    }

    .site-nav {
        gap: var(--jd-space-4);
    }
}

/* The language menu — a <details>/<summary> disclosure with no script driving it beyond
   Blazor itself, matching the shape (not the file) of the site's LanguageSelector.astro. */
.language-selector {
    position: relative;
    font-size: var(--jd-text-sm);
}

.language-selector summary {
    display: inline-flex;
    align-items: center;
    gap: var(--jd-space-2);
    padding: var(--jd-space-1) var(--jd-space-2);
    border-radius: var(--jd-radius-sm);
    color: var(--jd-text-muted);
    cursor: pointer;
    list-style: none;
}

.language-selector summary::-webkit-details-marker {
    display: none;
}

.language-selector summary:hover {
    color: var(--jd-text-primary);
}

.language-selector summary:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
}

.language-selector .caret {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: transform var(--jd-duration-fast) var(--jd-ease);
}

.language-selector details[open] summary {
    color: var(--jd-text-primary);
}

.language-selector details[open] .caret {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .language-selector .caret {
        transition: none;
    }
}

.language-selector ul {
    position: absolute;
    right: 0;
    z-index: 10;
    min-width: 100%;
    margin: var(--jd-space-1) 0 0;
    padding: var(--jd-space-2);
    list-style: none;
    background: var(--jd-surface-raised);
    border: 1px solid var(--jd-border);
    border-radius: var(--jd-radius-md);
    display: flex;
    flex-direction: column;
    gap: var(--jd-space-1);
    white-space: nowrap;
}

.language-selector li button,
.language-selector li [aria-current] {
    display: block;
    width: 100%;
    text-align: left;
    padding: var(--jd-space-1) var(--jd-space-2);
    border-radius: var(--jd-radius-sm);
    font: inherit;
    border: 0;
    background: none;
}

.language-selector li button {
    color: var(--jd-text-primary);
    cursor: pointer;
}

.language-selector li button:hover {
    background: var(--jd-surface);
}

.language-selector li [aria-current] {
    color: var(--jd-text-muted);
}

/* The brand block: mark, product name as this app's one <h1>, and tagline — the same rules,
   in the same order, as the site's BrandHeading.astro. The block owns its own bottom margin
   so what follows starts at the same distance here as it does there. */
.brand-heading {
    margin-bottom: var(--jd-space-4);
}

.brand-heading h1 {
    font-size: var(--jd-text-2xl);
    line-height: var(--jd-line-tight);
    margin: 0;
}

/*
 * A short screen gets a smaller name, and the room under it goes too — the site's rule, which
 * this file was missing. Without it the two brands agreed at 900 pixels of window height and
 * parted company at 800, where the site's heading drops to `--jd-text-xl` and this one
 * did not.
 */
@media (max-height: 820px) {
    .brand-heading h1 {
        font-size: var(--jd-text-xl);
    }

    .brand-heading {
        margin-bottom: var(--jd-space-2);
    }
}

/*
 * The fallback `js/brand-heading.js` replaces, and the closest CSS can state on its own:
 * `1cap` is exactly the cap height, and a baseline-aligned image ends on the baseline. Top edge
 * exact, bottom edge short by the descender.
 *
 * `margin-right` rather than a flex `gap`, because this is an inline image in the heading's own
 * text flow now, exactly as it is on the site — a flex row centres the mark on the line box
 * instead of hanging it off the cap line, and that alone put it two pixels off. Optical, not
 * metric: the file carries 3 pixels of transparent margin down its left side and 2 down its
 * right, out of 64, so a gap set to the eye reads narrower than the number says.
 */
.brand-heading .mark {
    height: 1cap;
    width: auto;
    vertical-align: baseline;
    margin-right: 0.18em;
}

.brand-heading .tagline {
    font-size: var(--jd-text-lg);
    color: var(--jd-accent);
    margin: var(--jd-space-3) 0 0;
}

/* The page's own title, one level below the brand's <h1> — Home.razor's "Playground",
   NotFound.razor's "Not found". Sized and spaced as the site's own section heading is
   (WhyJustDummiesContent.astro's h2), so "Playground" lands where "Pourquoi JustDummies"
   lands and the first line of prose under it does too.

   THE GAP UNDER IT IS space-4, NOT space-6, and that is the site's number rather than a
   choice made here: the heading's own box agreed with the site's all along, so the extra
   half-rem was invisible on this element and showed up one element down, holding the
   opening paragraph eight pixels below where the site starts the same words. */
.page-title {
    font-size: var(--jd-text-xl);
    line-height: var(--jd-line-tight);
    margin: 0 0 var(--jd-space-4);
}


/*
 * THE BUILDER (specification §10) — one <ChainLink> per step, inside the shared code card
 * declared as `.code-widget` further up this file.
 *
 * IT IS A CODE BLOCK NOW, NOT A FORM. It used to be a column of bordered <select>s with a
 * doc paragraph under each and a separate strip below showing the code they added up to —
 * so a visitor read the chain twice, once as controls and once as text, and neither reading
 * was the one the landing page had just shown them. What is here instead is the landing
 * page's card with the chain written inside it: a chosen step is C# in the same token
 * colours highlight.ts gives the site's own snippets, its documentation is a `//` comment
 * where a comment would be, and the only <select> left is the one on the step not yet
 * chosen — the single place where something is still being asked.
 *
 * WHAT THAT COSTS, STATED PLAINLY: a chosen method can no longer be swapped in place, only
 * removed. Nothing is actually lost — choosing a method already truncated everything after
 * it (Home.razor's HandleMethodChosen), so "change this step" was always "and discard the
 * rest" — but it is now two gestures where it used to be one, and the delete control is
 * what makes the second one reachable. That is the trade for a chain that reads as code.
 */

/* The chain's own lines, inside the card's <pre>-equivalent. The card, the padding and the
   line-height are `.code-widget .expression`'s; this only stacks the steps. */
.playground-widget .chain {
    display: flex;
    flex-direction: column;
}

/* Every step but the first, indented to land its "." under the "." of "Any." on the first
   line — the hero's 22ch rule with this widget's own, much shorter prefix: "Any" is three
   columns, so the dot after it sits in the fourth, and so does every dot below it. */
.playground-widget .chain > :not(:first-child) {
    padding-left: 3ch;
}

/* The closing call, printed rather than offered: Generate() is a button under the card
   (specification §10.2's "regenerate immediately"), never a step the visitor picks from the
   same list as a constraint — but the chain it closes is not valid C# without it, and this
   block is read as C#. Indented with the steps above it, and only drawn once there is a
   chain for it to close. */
.playground-widget .closing {
    padding-left: 3ch;
}

/*
 * A phone gets the shallower indent for the reason the hero's own narrow query gives: these
 * lines hold real controls a visitor has to reach, so they wrap rather than scroll, and
 * every column of indent is a column the wrapped remainder does not get. 3ch is already the
 * shallow end, so what gives here is the indent entirely — the leading "." still marks each
 * step as a continuation.
 */
@media (max-width: 28rem) {
    .playground-widget .chain > :not(:first-child),
    .playground-widget .closing {
        padding-left: 0;
    }
}

/* One step: the leading ".", the call, its diagnostic flag, its delete control, and its
   doc comment on the line below (.doc claims the whole row, further down). Baseline
   alignment rather than centre, so the buttons sit on the line of code they belong to
   rather than floating in the middle of its leading. */
.chain-link {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0;
}

.chain-link .prefix {
    color: var(--jd-text-muted);
}

/*
 * The one control still asking a question: the step nobody has chosen yet.
 *
 * A native <select> (§11.5 — never a custom widget), but drawn to sit in a line of code
 * rather than in a form: the surface it stands on, a dashed underline in the same idiom as
 * the argument fields above, and the accent colour, because in a block where everything
 * else is settled text this is the one thing that is still an invitation.
 */
.chain-link select {
    font: inherit;
    color: var(--jd-accent);
    background: var(--jd-surface-raised);
    border: 0;
    border-bottom: 1px dashed currentColor;
    border-radius: var(--jd-radius-sm) var(--jd-radius-sm) 0 0;
    padding: 0 var(--jd-space-1);
    cursor: pointer;
}

.chain-link select:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
}

/* A chosen step, written as the code it is. The method name takes the member colour the
   site's highlighter gives `.StartingWith` in its own snippets; the parentheses and commas
   take .tok-punctuation. Nothing here is a control any more except the argument fields. */
/* `min-width: 0` is the other half of the input cap above: a flex item refuses by default to
   shrink below its content, so without this the call would hold the card open at the width of its
   widest field and the cap would never bind. */
.chain-link .call {
    min-width: 0;
    color: var(--jd-text-primary);
}

/* Removal, and only removal (see this section's header). Quiet until wanted: the glyph
   carries the muted colour until the pointer or the keyboard arrives, then the error
   colour, because what it does is destructive — it truncates the chain from here on.
   `visibility` rather than `display` on the hover rule would still reserve the column;
   this stays visible at all times instead, for §5.7's reason: there is no hover on a
   phone, and a control nobody can discover is a control that is not there. */
.chain-link .delete {
    font: inherit;
    line-height: 1;
    color: var(--jd-text-muted);
    background: none;
    border: 0;
    padding: 0 var(--jd-space-2);
    cursor: pointer;
}

.chain-link .delete:hover {
    color: var(--jd-error);
}

.chain-link .delete:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
    border-radius: var(--jd-radius-sm);
}

/*
 * THE DOC, AS A COMMENT (§5.7, §10.7). The summary comes from the library's own XML
 * documentation, so this is the library documenting itself inside the code it documents —
 * which is where a reader of C# already looks for it, and it costs the card nothing: a
 * comment is what the eye skips when it wants the code and finds when it wants the prose.
 *
 * ALWAYS VISIBLE, NEVER ON HOVER — §5.7's rule, and the reason this is a `<p>` in the flow
 * rather than a tooltip on the method name.
 *
 * THE MARKER IS ITS OWN COLUMN so the text hangs. CSS cannot repeat "//" down the left of a
 * wrapped paragraph — there is no selector for a line box — so the choice is one marker
 * with the remainder aligned under the text, which is what an editor shows for a soft-
 * wrapped comment, or a marker per line, which would need the summary split into lines here
 * and would be wrong at every width but the one it was split for. This takes the first.
 */
.chain-link .doc {
    /*
     * ON THE STEP'S OWN LINE, running on from its delete control — where a trailing comment
     * goes in C#, and where it stops costing the chain a line of its own. It had a row to
     * itself first, which doubled the height of every chosen step and pushed the chain that
     * much further from the shape the landing page shows.
     *
     * `flex: 1 1 24ch` is what makes that conditional rather than absolute: it runs on while
     * a readable width of it fits beside the call, and drops to a line of its own when the call
     * is long enough that it would not — on a phone, that is most of them. `min-width: 0` is what
     * lets it shrink at all; without it a flex item refuses to go below its longest word and
     * the line overflows the card instead of wrapping inside it.
     */
    flex: 1 1 24ch;
    min-width: 0;
    display: flex;
    gap: 1ch;
    margin: 0 0 0 var(--jd-space-2);
    line-height: var(--jd-line-body);
}

.chain-link .doc .marker {
    flex: none;
}

.chain-link .doc .text {
    flex: 1;
    min-width: 0;
}

/* Inside the comment and reading as part of it — the same colour as the words around it,
   underlined so it is still identifiably a link (§13.4: external links identifiable), and
   only picking up the accent when it is pointed at or focused. A link in the site's accent
   here would have been the brightest thing in the block, which is not what a `see also` in
   a comment is worth. */
.chain-link .doc .help {
    color: inherit;
    text-decoration: underline;
    white-space: nowrap;
}

.chain-link .doc .help:hover,
.chain-link .doc .help:focus-visible {
    color: var(--jd-accent);
}

.chain-link .doc .arrow {
    stroke: currentColor;
    stroke-width: 1.4;
    fill: none;
}

/*
 * THE DIAGNOSTIC FLAG, and what it is allowed to hide.
 *
 * It stands between the code and its comment, where an editor puts a squiggle's gutter
 * mark, and it opens the message rather than showing it inline. That is a deliberate
 * narrowing of what used to be printed under every failing step, and it applies to exactly
 * one of the two kinds of failure a step can have:
 *
 *   - AN ARGUMENT THIS PLAYGROUND COULD NOT PARSE ("this argument expects an integer") is
 *     the site's own text about a value the visitor is mid-way through typing. Folded in
 *     here, because it is a rail rather than a result and it fires on a keystroke;
 *   - THE LIBRARY'S OWN REFUSAL is not folded anywhere. It keeps a flag on the step that
 *     caused it, for §13.4's "associated with the zone that provokes it", and its wording
 *     is printed in full in the result bar under the card, always visible, exactly as the
 *     hero prints it. §9.9 is explicit that the refusal is the demonstration defending
 *     itself, and Pages/Hero.razor's own header says what hiding it to keep a page tidy
 *     would cost. A tooltip is a way of hiding it.
 *
 * WHY A BUTTON AND NOT A TOOLTIP. There is no hover on a phone (§5.7), so a message reachable
 * only by pointing at something is a message a phone cannot read. This opens on click, on
 * Enter and on Space like the button it is, says whether it is open through `aria-expanded`,
 * and closes on Escape — and the message is wired to the offending field with
 * `aria-describedby` whether or not it is open, so a screen reader never has to find this
 * control at all.
 */
.chain-link .flag {
    flex: none;
    display: inline-flex;
    align-items: center;
    margin-left: var(--jd-space-2);
    padding: 0;
    border: 0;
    background: none;
    color: var(--jd-error);
    cursor: pointer;
    /* Aligned to the text's own centre rather than to its baseline: a circle hung off a
       baseline sits visibly high against the letters it annotates. */
    align-self: center;
}

.chain-link .flag:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
    border-radius: 50%;
}

.chain-link .flag .disc {
    fill: currentColor;
}

/* The cross inside the disc, in the card's own ground so it reads as cut out of the circle
   rather than drawn on it. It is also what keeps this flag from being a red dot and nothing
   else: §13.4 forbids meaning carried by colour alone, and the glyph is the other half of
   the statement — the accessible name on the button is the rest of it. */
.chain-link .flag .cross {
    stroke: var(--jd-surface);
    stroke-width: 2;
    stroke-linecap: round;
    fill: none;
}

/*
 * The message the flag opens. In the flow on its own row rather than floating over the
 * code: the card clips to its rounded corners (`overflow: hidden` on .code-widget .card),
 * so an absolutely positioned bubble on the last step would be cut off by the very box it
 * belongs to. Drawn as a small callout anyway — its own ground, a border in the error
 * colour, sitting under the line it is about, which is where an editor puts one.
 *
 * SCOPED `:not(.visually-hidden)` BECAUSE THE ELEMENT NEVER LEAVES THE DOM. Closed, it wears
 * `.visually-hidden` so `aria-describedby` on the offending field still resolves — and that
 * rule is one class less specific than this one, so without the guard here the callout would
 * keep its padding, its border and its own margins while pretending to be a one-pixel box.
 */
.chain-link .error:not(.visually-hidden) {
    /* 100% of the line and no cap on it — the cap is what kept this on the code's own row.
       `flex-basis: 100%` is the standard way to force a flex item onto a line of its own, and
       it only works while nothing lets the item shrink to fit beside its siblings: a
       `max-width` did exactly that, and the callout ended up wedged to the right of the call
       it was about, pushing that step's comment down past it. */
    flex-basis: 100%;
    margin: var(--jd-space-1) 0 var(--jd-space-2);
    padding: var(--jd-space-2) var(--jd-space-3);
    background: var(--jd-surface-raised);
    border: 1px solid var(--jd-error);
    border-radius: var(--jd-radius-sm);
    color: var(--jd-error);
    font-family: var(--jd-font-sans);
    line-height: var(--jd-line-body);
}

/*
 * THE CODE BAR — the middle zone of the playground's card, between the block the chain is
 * built in and the bar its value comes back in.
 *
 * TWO BARS RATHER THAN ONE, and they answer two different questions. The block above holds
 * the chain the way it is built: arguments in fields, summaries in comments, wrapped over as
 * many lines as that takes. This bar holds the same chain as the single line that compiles —
 * no comments, every argument re-emitted as a real C# literal (Home.razor's
 * FormatArgumentLiteral), which is a different string and deliberately so. The bar below
 * holds what running it produced.
 *
 * WHICH IS WHY THE COPY BUTTON IS HERE. It hung off the block's top-right corner before, so
 * the one control promising "you get exactly what you see" was attached to the one thing
 * showing something else. On this bar the promise is literal, and Home.razor's CodeSegments
 * makes it so by construction: this bar draws the runs and the clipboard gets them joined.
 *
 * Drawn from the same three declarations as the result bar under it — the divider, the raised
 * ground, the padding — because they are two zones of one card, not a zone and a decoration.
 */
.playground-widget .code-bar {
    display: flex;
    align-items: center;
    gap: var(--jd-space-3);
    padding: var(--jd-space-3) var(--jd-space-4);
    border-top: 1px solid var(--jd-border);
    background: var(--jd-surface-raised);
}

/* One line, and it stays one line: this is the copyable form, and a copyable form that
   wrapped would read as several statements. It scrolls in its own box rather than widening
   the card — `min-width: 0` is what allows that, since a flex item will not shrink below its
   content without it and the overflow would land on the card instead. */
.playground-widget .code-text {
    flex: 1;
    min-width: 0;
    overflow-x: auto;
    white-space: pre;
}

/* What stands where the line would, while an argument has no literal form to stand in for it.
   In the error colour and the sans face: it is the page talking about the code, not code. */
.playground-widget .not-compilable {
    flex: 1;
    min-width: 0;
    margin: 0;
    color: var(--jd-error);
    font-family: var(--jd-font-sans);
}

/* The label is carried rather than dropped for an icon: there is room for it on a bar of its
   own, and it is no longer positioned over code that a longer translation could collide with. */
.playground-widget .copy {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: var(--jd-space-2);
    font: inherit;
    font-family: var(--jd-font-sans);
    color: var(--jd-text-muted);
    background: var(--jd-surface);
    border: 1px solid var(--jd-border);
    border-radius: var(--jd-radius-sm);
    padding: var(--jd-space-1) var(--jd-space-3);
    cursor: pointer;
}

.playground-widget .copy:hover:not(:disabled) {
    color: var(--jd-text-primary);
    border-color: var(--jd-accent);
}

/* Dimmed rather than gone, and the sentence beside it says why — the same treatment the
   Generate button under the card already gets for the same kind of reason. */
.playground-widget .copy:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.playground-widget .copy:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
}

.playground-widget .copy svg {
    flex: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    fill: none;
}

/* Copied. The tick is the sighted half of the feedback §13.4 asks copy buttons for; the
   other half is the live region CopyCodeButton keeps beside it. */
.playground-widget .copy[data-copied] {
    color: var(--jd-success);
    border-color: var(--jd-success);
}

/*
 * THE MASCOT (Pages/Home.razor) — purely decorative, so it asks for a positioned ancestor
 * rather than any layout weight of its own: `.playground-widget` is where it visually
 * belongs, and its own box is untouched by giving it one.
 *
 * WHY IT SITS INSIDE THE CARD'S OWN RIGHT EDGE, NOT PAST IT. `.playground-widget` fills
 * the whole 72rem shell (§9.8's own note on the card above), so there is no free column
 * beside it to draw into — only the page's own outer margin, and that margin's width
 * depends on the browser window in a way this file has no way to know ahead of time.
 * Anchored to the card's own corner instead, the same drawing holds at every width wide
 * enough to draw it at all.
 *
 * AND "WIDE ENOUGH" IS A MEASURED NUMBER, NOT A GUESSED ONE. The robot hangs above the
 * card, which puts it beside the prose rather than beside anything of the card's — and
 * that prose is capped at 38rem (`.lede-detail`, `.playground-note`), so the gap it can
 * fall into only opens once the shell is wider than that cap plus the artwork. Measured
 * against the rendered line boxes in both locales, the painted robot still crosses the
 * note at 840px and is clear at 860px; the query below rounds that up to 56rem so the
 * margin survives a font that renders a little wider. A breakpoint set by eye instead
 * put the robot over two lines of the note for a whole band of ordinary tablet widths.
 */
.playground-widget {
    position: relative;
}

/* THE OFFSET IS MEASURED OFF THE ARTWORK, NOT OFF ITS BOX. dummy-driving.png is a square
   canvas whose painted robot occupies (51,20)-(688,778) of it, so its feet sit at 97% of
   the box's own height and the transparent remainder below them is what would otherwise
   rest on the card. -12.6rem would land the feet exactly flush with the card's top edge;
   -9.5rem lets them cross it, which is what makes the robot read as sitting on the card
   rather than balanced on its rim. Re-measure both numbers if the artwork is ever
   recropped. */
.mascot {
    display: none;
    position: absolute;
    top: -9.5rem;
    right: 0.5rem;
    width: 13rem;
    height: auto;
    pointer-events: none;
    z-index: 1;
}

/* Hidden by default and drawn only here, rather than drawn by default and hidden below:
   a decoration that obscures the page's own words is worse than no decoration, so the
   narrow case is the one that needs no query to get right. */
@media (min-width: 56rem) {
    .mascot {
        display: block;
    }
}

/*
 * THE CONSENT BANNER (wwwroot/index.html) — the site's own ConsentBanner.astro, to the
 * token, for the reason the download control below is: one question asked in two
 * applications should not look like two questions (ADR-0025). The markup is in the shell
 * rather than in a component, so that it can be read before the runtime is and so that the
 * build can see it; these rules are the site's own, transcribed.
 *
 * `[hidden]` FIRST, AND IT IS NOT A FORMALITY. The banner ships hidden and is unhidden by
 * `consent.js`, and `display: flex` below beats the user agent's own
 * `[hidden] { display: none }` on specificity alone — so without this rule the attribute
 * would be set correctly and the banner shown anyway, on every page, to every reader
 * including one who has already answered. The site's base.css carries the identical rule
 * and says so at length; this is that rule, in the other stylesheet.
 */
[hidden] {
    display: none !important;
}

.consent {
    position: fixed;
    inset: auto 0 0 0;
    /* Above the page, and above the download control floating in the corner beneath it
       (`z-index: 5`) — that control reads its own bottom offset from this banner's
       published height rather than sharing its layer. */
    z-index: 10;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--jd-space-4);
    padding: var(--jd-space-4) var(--jd-space-6);
    background: var(--jd-surface-raised);
    border-top: 1px solid var(--jd-border);
    box-shadow: var(--jd-shadow-raised);
    /* Anchored to the bottom and out of flow, so a banner taller than the viewport would
       grow off the TOP of the screen, where the document cannot be scrolled to reach it.
       That happens at high zoom and on short landscape viewports, and what it hides is the
       disclosure and the privacy link: the two things a visitor needs before answering.
       dvh rather than vh so a mobile browser's retracting toolbar is accounted for. */
    max-height: 100dvh;
    overflow-y: auto;
}

.consent-text {
    flex: 1 1 24rem;
    min-width: 0;
}

.consent-title {
    margin: 0 0 var(--jd-space-1);
    font-weight: 600;
}

.consent-body {
    margin: 0;
    color: var(--jd-text-muted);
    font-size: var(--jd-text-sm);
    line-height: var(--jd-line-body);
}

.choices {
    flex: none;
    display: flex;
    gap: var(--jd-space-3);
}

/* One class for both answers. Any rule that distinguishes them by size, weight or colour
   is the bug, not a refinement — the site's component says why at length. */
.choice {
    font: inherit;
    font-size: var(--jd-text-sm);
    color: var(--jd-text-primary);
    background: var(--jd-surface);
    border: 1px solid var(--jd-border);
    border-radius: var(--jd-radius-sm);
    padding: var(--jd-space-2) var(--jd-space-4);
    cursor: pointer;
    transition: border-color var(--jd-duration-fast) var(--jd-ease);
}

.choice:hover {
    border-color: var(--jd-accent);
}

.choice:focus-visible,
.consent:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
}

.consent:focus-visible {
    outline-offset: -2px;
}

/*
 * THE DOWNLOAD FAB (Layout/DownloadFab.razor) — the site's own DownloadFab.astro, to the
 * token: same position, same shape, same corner, so the control reads as one the two
 * halves of the product share rather than two that happen to agree today. That now
 * includes the site's `--jd-consent-height` calculation: since ADR-0025 this document
 * carries a consent banner too, and a control that ignored its height would sit under it.
 * The variable is published by `consent.js` and defaults to zero, so a build with no
 * banner puts the control exactly where it was.
 */
.download-fab {
    position: fixed;
    right: var(--jd-space-6);
    bottom: calc(var(--jd-space-6) + var(--jd-consent-height, 0px));
    z-index: 5;
    display: flex;
    align-items: center;
    gap: var(--jd-space-2);
    color: var(--jd-text-primary);
    background: var(--jd-surface-raised);
    border: 1px solid var(--jd-border);
    border-radius: 999px;
    padding: var(--jd-space-3) var(--jd-space-4);
    font-family: var(--jd-font-sans);
    font-size: var(--jd-text-sm);
    font-weight: 600;
    text-decoration: none;
    box-shadow: var(--jd-shadow-raised);
    transition: border-color var(--jd-duration-fast) var(--jd-ease);
}

.download-fab:hover {
    border-color: var(--jd-accent);
    color: var(--jd-accent);
}

.download-fab:focus-visible {
    outline: 2px solid var(--jd-accent);
    outline-offset: 2px;
}


.download-fab .icon {
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* The same narrow query as the site's own DownloadFab.astro, and the same number: below
   it the label goes, the icon alone still says what the control does, and `aria-label`
   says it to a screen reader either way. */
@media (max-width: 26rem) {
    .download-fab .label {
        display: none;
    }

    .download-fab {
        padding: var(--jd-space-3);
    }
}
