/* /Components/ArenaFailureCard.razor.rz.scp.css */
/* ── ArenaFailureCard ────────────────────────────────────────────────────────
   Moved out of Arena.razor.css, where these rules could not reach this component's markup.
   See the note in ArenaViewportPanel.razor.css for the mechanism and the measurement. */

.arena-failure__card[b-5hydoiwzol] {
    border: 1px solid var(--danger-border);
    background: var(--danger-surface);
    border-radius: var(--radius-sm);
    padding: var(--space-sm) var(--space-smd);
    color: var(--accent-red);
}

.arena-failure__title[b-5hydoiwzol] {
    font-size: var(--text-md);
    font-weight: var(--weight-bold);
    margin-bottom: var(--space-xs);
}

.arena-failure__body[b-5hydoiwzol] {
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
}

/* Finding #10: collapse raw error JSON behind a disclosure */
.arena-failure__details[b-5hydoiwzol] {
    margin-top: var(--space-sm);
    font-size: var(--text-sm);
}

.arena-failure__details summary[b-5hydoiwzol] {
    cursor: pointer;
    color: var(--accent-red);
    font-weight: var(--weight-semibold);
    user-select: none;
    padding: var(--space-2xs) 0;
}

.arena-failure__details summary:hover[b-5hydoiwzol] {
    color: var(--text);
}

.arena-failure__raw[b-5hydoiwzol] {
    margin: var(--space-sm) 0 0;
    padding: var(--space-sm);
    background: var(--surface-1);
    border: 1px solid var(--danger-border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-family: 'JetBrains Mono', monospace;
    font-size: var(--text-xs);
    line-height: var(--leading-snug);
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 8rem;
    overflow-y: auto;
}
/* /Components/ArenaStatusBand.razor.rz.scp.css */
/* ── ArenaStatusBand ─────────────────────────────────────────────────────────
   New component, new block. The rules below are the four superseded blocks' own colours
   carried over unchanged — only the selectors merged. They live here rather than in
   Arena.razor.css because a scoped stylesheet can only reach its own component's markup;
   see ArenaViewportPanel.razor.css for what happened the last time that was got wrong. */

/* ── Status band ──────────────────────────────────────────────────────────────
   One element with five treatments, replacing .arena__verdict-recorded,
   .arena__judge-note (+ --auto, --error) and .arena__generating-notice. Those were four
   blocks in the markup that could never appear together but did not know it; they are one
   ArenaStatusBand now, keyed on ArenaPhase. The colours below are each block's own, carried
   over unchanged — only the selectors merged. */
.arena-band__box[b-t8fto6ysc3] {
    text-align: center;
    font-size: var(--text-md);
    color: var(--text);
    padding: var(--space-sm) var(--space-smd);
    margin-bottom: var(--space-sm);
    border-radius: var(--radius-sm);
}

/* A settled verdict. Sticky, because it is the answer the page exists to give and the
   evidence above it is taller than any screen. */
.arena-band__box--verdict[b-t8fto6ysc3] {
    color: var(--accent-green);
    font-weight: var(--weight-semibold);
    font-size: var(--text-lg);
    padding: var(--space-sm) var(--space-lg);
    background: var(--success-surface);
    border: 1px solid var(--success-border);
    position: sticky;
    top: 0;
    z-index: 10;
    box-shadow: var(--glass-shadow);
}

/* The optimistic window: the pick is on screen but the server has not confirmed it. Muted
   rather than green so a confirmed verdict and a predicted one never look the same. */
.arena-band__box--pending[b-t8fto6ysc3] {
    color: var(--text-muted);
    background: var(--surface-2);
    border-color: var(--border-strong);
}

/* Nothing left to judge. */
.arena-band__box--error[b-t8fto6ysc3] {
    color: var(--accent-red);
    border: 1px solid var(--danger-border);
    background: var(--danger-surface);
}

/* Auto-judge countdown, and the moment the judge is deciding. Amber reads as "time pressure"
   without competing with the red failure note. --accent-yellow on --warn-surface is
   contrast-checked in both themes. */
.arena-band__box--judging[b-t8fto6ysc3] {
    color: var(--accent-yellow);
    border: 1px solid var(--warn-border);
    background: var(--warn-surface);
}

/* Inference is still streaming. Reports that work is happening, so it must not read as an
   error or as a result. */
.arena-band__box--live[b-t8fto6ysc3] {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    font-size: var(--text-sm);
    color: var(--text-muted);
    background: var(--info-surface);
    border: 1px solid var(--info-border);
}

/* "Waiting for the other model" and "Pick the winner below" — plain text, no panel. */
.arena-band__box--note[b-t8fto6ysc3] {
    color: var(--text);
}

.arena-band__rationale[b-t8fto6ysc3] {
    display: block;
    margin-top: var(--space-xs);
    color: var(--accent-green);
    font-weight: var(--weight-normal);
    font-size: var(--text-md);
    font-style: italic;
}

.arena-band__by[b-t8fto6ysc3] {
    display: block;
    color: var(--text-muted);
    font-size: var(--text-sm);
    margin-top: var(--space-xs);
}

/* The auto-judge beat the user's pick. It has to read as a correction, not as flavour text —
   the click genuinely did not count. Was .arena__override-notice, a second live region. */
.arena-band__notice[b-t8fto6ysc3] {
    display: block;
    margin-top: var(--space-xs);
    padding: var(--space-xs) var(--space-sm);
    color: var(--accent-yellow);
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    background: var(--warn-surface);
    border: 1px solid var(--warn-border);
    border-radius: var(--radius-sm);
}

/* ── Phase edge ──────────────────────────────────────────────────────────────
   A hairline under the band whose two colours belong to the phase. --po-edge-a/-b are
   registered as <color> in app.css (@property), which is what lets a gradient's stops
   TRANSITION when the phase class changes — an unregistered custom property flips instantly.
   One transition per phase change, not a loop. The band is the same element across phases
   (only its class changes), so the colours travel rather than restarting. */
/* The settled verdict is already positioned (sticky), which anchors the edge just as well —
   and restating position here would outrank it and unstick the band. */
.arena-band__box:not(.arena-band__box--verdict)[b-t8fto6ysc3] {
    position: relative;
}

.arena-band__box[b-t8fto6ysc3]::before {
    content: "";
    position: absolute;
    inset-inline: var(--space-sm);
    bottom: -1px;
    height: 2px;
    border-radius: var(--radius-full);
    background: linear-gradient(90deg, transparent, var(--po-edge-a), var(--po-edge-b), var(--po-edge-a), transparent);
    transition: --po-edge-a 700ms ease, --po-edge-b 700ms ease;
    pointer-events: none;
}

.arena-band__box--live[b-t8fto6ysc3]::before {
    --po-edge-a: var(--accent-blue);
    --po-edge-b: var(--accent-cyan);
}

.arena-band__box--judging[b-t8fto6ysc3]::before {
    --po-edge-a: var(--accent-yellow);
    --po-edge-b: var(--accent-cyan);
}

.arena-band__box--verdict[b-t8fto6ysc3]::before {
    --po-edge-a: var(--accent-green);
    --po-edge-b: var(--accent-cyan);
}

.arena-band__box--pending[b-t8fto6ysc3]::before {
    --po-edge-a: var(--border-strong);
    --po-edge-b: var(--border-strong);
}

.arena-band__box--error[b-t8fto6ysc3]::before {
    --po-edge-a: var(--accent-red);
    --po-edge-b: var(--accent-red);
}

@media (prefers-reduced-motion: reduce) {
    .arena-band__box[b-t8fto6ysc3]::before {
        transition: none;
    }
}

/* ── The judge's checklist ───────────────────────────────────────────────────
   A disclosure under the one-line reason. Plain body text, left-aligned and not green: it is
   several sentences to read, not a headline, and the band's centred semibold accent colour
   is wrong for that. */
.arena-band__analysis[b-t8fto6ysc3] {
    margin-top: var(--space-xs);
    font-size: var(--text-sm);
    font-weight: var(--weight-normal);
    color: var(--text-muted);
}

.arena-band__analysis > summary[b-t8fto6ysc3] {
    display: inline-block;
    cursor: pointer;
    text-decoration: underline dotted;
}

.arena-band__analysis-text[b-t8fto6ysc3] {
    max-width: 70ch;
    margin: var(--space-xs) auto 0;
    color: var(--text);
    line-height: var(--leading-normal);
    text-align: left;
}
/* /Components/ArenaViewportPanel.razor.rz.scp.css */
/* ── ArenaViewportPanel ──────────────────────────────────────────────────────
   These rules used to live in Arena.razor.css under an `arena__` prefix, and they had
   never once applied. Blazor stamps a scoped stylesheet with its OWN component's scope id,
   so `.arena__viewport-panel[b-vc13joezsl]` could only ever match an element written in
   Arena.razor — and this panel is written here. Verified in the browser before the move:
   the panel computed `display: block` against the declared `flex`, and `.arena__hud`
   computed `container-type: normal` against the declared `inline-size`, which meant
   TelemetryHud's @container rules had been matching nothing and silently falling back to
   two columns for the whole life of the component.

   Exactly the trap CLAUDE.md already records once (ModelHealthPanel.razor.css styling
   LabModelCard's markup), recurring in the Arena's extraction. It is also why the block is
   renamed: this file owns `arena-panel__`, per "every surface owns exactly one BEM block,
   named after its file". Sharing `arena__` across four files is how the rules ended up in
   the wrong stylesheet in the first place.

   check-clean.ps1 cannot see this class of bug — it asks whether a rule exists SOMEWHERE,
   not whether it can reach the markup. */

/* No height floor. The preview inside sizes itself (see SandboxedViewport's --preview-h),
   and the two sides line up because both frames resolve to the same box — a floor here just
   pushed the verdict below the fold. A failed side is one small card and needs no reserve. */
.arena-panel__box[b-pm46d9t3ly] {
    /* The containing block for the first-token spark and the UPSET stamp. */
    position: relative;
    display: flex;
    flex-direction: column;
    /* Pack to the top and let every child keep its own height. Without this the children
       stretch to fill the taller of the two panels, which is how the HUD came to overlap
       the preview when one side finished before the other. */
    align-items: stretch;
    justify-content: flex-start;
    gap: var(--space-sm);
}

.arena-panel__box > *[b-pm46d9t3ly] {
    flex: 0 0 auto;
    min-height: 0;
}

.arena-panel__hud[b-pm46d9t3ly] {
    flex-shrink: 0;
    /* Names a containment context so TelemetryHud can size its metric grid against this panel
       rather than against the window — the panel is full width on a desktop duel, about half
       that with two panels side by side, and narrower again on a phone, so the viewport is the
       wrong thing for it to measure. Without this declaration the component's @container rules
       match nothing and it falls back to two columns, which is the correct degradation. */
    container: telemetry / inline-size;
}

.arena-panel__source[b-pm46d9t3ly] {
    max-width: 100%;
}

.arena-panel__source > summary[b-pm46d9t3ly] {
    width: fit-content;
    min-height: var(--target-min);
    padding: var(--space-xs) var(--space-sm);
    color: var(--text);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    list-style: none;
}

.arena-panel__source > summary[b-pm46d9t3ly]::-webkit-details-marker {
    display: none;
}

.arena-panel__source > summary:focus-visible[b-pm46d9t3ly] {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

.arena-panel__source pre[b-pm46d9t3ly] {
    max-height: 20rem;
    margin: var(--space-xs) 0 0;
    padding: var(--space-sm);
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    color: var(--text-muted);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: var(--text-xs);
    line-height: var(--leading-normal);
}

/* The side that won. Static on purpose — a looping shimmer here was a non-compositor animation
   (background-position) running beside a WebGPU tab. */
.arena-panel__box--winner[b-pm46d9t3ly] {
    border-color: var(--accent-green);
    box-shadow: 0 0 28px color-mix(in srgb, var(--accent-green) 35%, transparent);
}

/* The moment the verdict lands (only then — see the Reveal parameter): the winning panel
   dips and springs forward once. Transform only, and one-shot: inference is over. */
.arena-panel__box--reveal[b-pm46d9t3ly] {
    animation: arena-panel-win-b-pm46d9t3ly 640ms cubic-bezier(.2, .9, .25, 1.2) both;
}

@keyframes arena-panel-win-b-pm46d9t3ly {
    from { transform: scale(0.97); }
    55%  { transform: scale(1.03); }
    to   { transform: none; }
}

/* ── First token ──────────────────────────────────────────────────────────────
   A band of light crosses the panel's top edge once and the edge glows and fades. Both are
   pseudo-elements animating transform and opacity only, because this plays while a browser
   model may be generating on the GPU. Left mounted afterwards, invisible. */
.arena-panel__spark[b-pm46d9t3ly] {
    position: absolute;
    inset: 0;
    z-index: 1;
    overflow: hidden;
    border-radius: var(--radius-md);
    pointer-events: none;
}

.arena-panel__spark[b-pm46d9t3ly]::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 45%;
    height: 3px;
    background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
    transform: translateX(-100%);
    animation: arena-panel-streak-b-pm46d9t3ly 900ms cubic-bezier(.3, .6, .3, 1) both;
}

.arena-panel__spark[b-pm46d9t3ly]::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow:
        inset 0 0 0 2px var(--accent-cyan),
        0 0 24px color-mix(in srgb, var(--accent-cyan) 50%, transparent);
    opacity: 0;
    animation: arena-panel-flash-b-pm46d9t3ly 1100ms ease-out both;
}

@keyframes arena-panel-streak-b-pm46d9t3ly {
    from { transform: translateX(-100%); }
    to   { transform: translateX(230%); }
}

@keyframes arena-panel-flash-b-pm46d9t3ly {
    from { opacity: 0; }
    20%  { opacity: 1; }
    to   { opacity: 0; }
}

/* ── Upset ────────────────────────────────────────────────────────────────────
   A stamp over the winner's corner. A solid surface under the text so its contrast does not
   depend on whatever the model's page painted behind it. */
.arena-panel__upset[b-pm46d9t3ly] {
    position: absolute;
    top: calc(-1 * var(--space-sm));
    right: calc(-1 * var(--space-xs));
    z-index: 2;
    padding: var(--space-2xs) var(--space-sm);
    color: var(--accent-yellow);
    background: var(--surface-1);
    border: 2px solid var(--accent-yellow);
    border-radius: var(--radius-sm);
    box-shadow: 0 0 18px color-mix(in srgb, var(--accent-yellow) 40%, transparent);
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    transform: rotate(-6deg);
    pointer-events: none;
}

/* Lands after the odometer settles, with the stinger (playUpset's 1.2 s). */
.arena-panel__upset--reveal[b-pm46d9t3ly] {
    animation: arena-panel-stamp-b-pm46d9t3ly 380ms cubic-bezier(.2, .9, .3, 1.4) 1200ms both;
}

@keyframes arena-panel-stamp-b-pm46d9t3ly {
    from { opacity: 0; transform: rotate(-6deg) scale(2.4); }
    to   { opacity: 1; transform: rotate(-6deg) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .arena-panel__box--reveal[b-pm46d9t3ly],
    .arena-panel__upset--reveal[b-pm46d9t3ly] {
        animation: none;
    }

    .arena-panel__spark[b-pm46d9t3ly] {
        display: none;
    }
}
/* /Components/ArenaVoteButtons.razor.rz.scp.css */
/* ── ArenaVoteButtons ────────────────────────────────────────────────────────
   Moved out of Arena.razor.css, where these rules could not reach this component's markup.
   See the note in ArenaViewportPanel.razor.css for the mechanism and the measurement.

   One declaration, not two: the flex layout came from `.arena__actions` over there and the
   sticky treatment from `.arena__actions--vote` beside it. Splitting one element's rules
   across a base class and a modifier earns its keep only when something else uses the base,
   and a class shared across two files is exactly what put these rules in the wrong stylesheet
   to begin with. Arena.razor keeps its own `.arena__actions` for the secondary row.

   The row rides the bottom of the viewport until the page scrolls down to its real position,
   then settles into flow. It occupies its own space either way, so nothing is permanently
   covered — only what you are scrolling past. Opaque background and a top edge, because it is
   drawn over the outputs for most of the page. */
.arena-vote__row[b-uzfr9s3scr] {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-md);
    position: sticky;
    bottom: 0;
    z-index: 5;
    background: var(--surface-1);
    border-top: 1px solid var(--border);
    padding: var(--space-smd) var(--space-md);
    margin-top: var(--space-sm);
    /* No shadow: the raw rgb(0 0 0 / 12%) it had was one theme's value and there is no
       upward-shadow token. The border-top and the opaque fill already separate the row. */
}

/* Layout only — the look is .po-btn's, per the app-wide rule. */
.arena-vote__btn[b-uzfr9s3scr] {
    /* ~180px, from the scale: keeps the two buttons a matched pair for short model names. */
    min-width: calc(var(--space-2xl) * 4);
}
/* /Components/BrandMark.razor.rz.scp.css */
/* ── BrandMark ────────────────────────────────────────────────────────────────
   The PL tile + wordmark used in NavMenu and Login. The login form wants a
   centred, larger take; the navbar wants the same tile with no wordmark below
   992px (which is what Navmenu's --hide rule does there). */

.brand-mark[b-vtt3g7brdf] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    text-decoration: none;
    color: inherit;
    font-weight: var(--weight-bold);
    line-height: 1;
}

.brand-mark__glyph[b-vtt3g7brdf] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-blue);
    color: var(--on-accent);
    border-radius: var(--radius-sm, 4px);
    font-size: var(--text-md);
    font-weight: var(--weight-bold);
    letter-spacing: 0.04em;
    inline-size: 2rem;
    block-size: 2rem;
}

.brand-mark__label[b-vtt3g7brdf] {
    font-size: var(--text-md);
    letter-spacing: 0.01em;
}

.brand-mark--large[b-vtt3g7brdf] {
    flex-direction: column;
    gap: var(--space-sm);
}

.brand-mark--large .brand-mark__glyph[b-vtt3g7brdf] {
    inline-size: 4rem;
    block-size: 4rem;
    font-size: var(--text-2xl);
}

.brand-mark--large .brand-mark__label[b-vtt3g7brdf] {
    font-size: var(--text-xl);
}

/* Login sits the form vertically centred and large; the navbar keeps the mark
   inline-level so it sits on the same baseline as the toggler. */
/* /Components/EmptyState.razor.rz.scp.css */
/* ── EmptyState ──────────────────────────────────────────────────────────────
   Tightens the existing .po-empty treatment: a single icon glyph, the title the page
   would have used anyway, and the same action row shape that used to be seven different
   ``__empty-actions`` classes. Geometry: icon capped at 2.5rem, actions wrap on phone. */

.empty-state[b-5f11lfsw7u] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-md);
    text-align: center;
    padding-block: var(--space-xl);
}

.empty-state__icon[b-5f11lfsw7u] {
    font-size: var(--text-3xl);
    line-height: 1;
    opacity: 0.85;
}

.empty-state__title[b-5f11lfsw7u] {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: var(--weight-bold);
}

.empty-state__hint[b-5f11lfsw7u] {
    margin: 0;
    color: var(--text-muted);
    max-inline-size: 36rem;
}

.empty-state__body[b-5f11lfsw7u] {
    inline-size: 100%;
    display: flex;
    justify-content: center;
}

.empty-state__actions[b-5f11lfsw7u] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    justify-content: center;
}
/* /Components/MicrosoftSignInButton.razor.rz.scp.css */
/* ── MicrosoftSignInButton ────────────────────────────────────────────────────
   The Microsoft mark is trademarked artwork with a mandated presentation — the
   button background must be white in both themes (CLAUDE.md). */

.ms-sign-in[b-urt7drb0bl] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    background: #fff;
    color: #5e5e5e;
    border: 1px solid #8c8c8c;
    font-weight: var(--weight-medium);
    text-decoration: none;
}

.ms-sign-in:hover[b-urt7drb0bl],
.ms-sign-in:focus-visible[b-urt7drb0bl] {
    background: #fff;
    border-color: var(--accent-blue);
    color: #5e5e5e;
}

.ms-sign-in__logo[b-urt7drb0bl] {
    flex: 0 0 auto;
}

.ms-sign-in--small[b-urt7drb0bl] {
    padding-inline: var(--space-sm);
    padding-block: var(--space-2xs);
    font-size: var(--text-sm);
}

.ms-sign-in--large[b-urt7drb0bl] {
    padding-inline: var(--space-md);
    padding-block: var(--space-sm);
    font-size: var(--text-md);
    inline-size: 100%;
}
/* /Components/ModelCard.razor.rz.scp.css */
/* Extracted from a <style> block inside ModelCard.razor. Scoped now, so these
   .model-card rules can no longer leak into other components. */

/* ── A picker row, not a datasheet ───────────────────────────────────────────
   Name on the left, the figures on the right, one line wherever they fit and two where they
   do not (flex-wrap, so nothing is ever clipped). 2.75rem is the touch-target floor, and the
   whole row is the target. It was a five-line card at ~115px: a 1440x900 desktop showed 15 of
   26 models inside a scrolling picker, and a phone four. */
.model-card[b-icg4zlrl91] {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs) var(--space-sm);
    min-height: 2.75rem;
    padding: var(--space-xs) var(--space-smd);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    overflow: hidden;
    cursor: pointer;
    transition: border-color var(--transition-fast), background var(--transition-fast);
}

.model-card[b-icg4zlrl91]::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: linear-gradient(180deg, var(--accent-green), var(--accent-cyan));
    transform: scaleY(0);
    transition: transform var(--transition-medium);
}

.model-card:hover:not(.model-card--selected)[b-icg4zlrl91] {
    border-color: var(--border-strong);
    background: var(--surface-2);
}

.model-card:hover:not(.model-card--selected)[b-icg4zlrl91]::before {
    transform: scaleY(0.3);
}

.model-card--selected[b-icg4zlrl91] {
    border-color: var(--accent-green);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-green) 40%, transparent);
    background: var(--success-surface);
}

.model-card--selected[b-icg4zlrl91]::before {
    transform: scaleY(1);
}

.model-card--disabled[b-icg4zlrl91] {
    cursor: not-allowed;
}

/* A long model name must never be clipped or ellipsised — it is the only thing that
   identifies the row, and two of the catalog's names differ only in their suffix. */
.model-card__name[b-icg4zlrl91] {
    flex: 1 1 8rem;
    min-width: 0;
    color: var(--text);
    font-weight: var(--weight-semibold);
    font-size: var(--text-md);
    line-height: var(--leading-tight);
    overflow-wrap: anywhere;
    hyphens: auto;
}

.model-card__slot-badge[b-icg4zlrl91] {
    flex: 0 0 auto;
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    animation: scaleIn 0.3s var(--spring-bounce);
}

/* --on-accent flips with the theme (white on the darkened light accents, black on
   the bright dark ones), so a filled chip stays legible either way. */
.model-card__slot-badge--left[b-icg4zlrl91] {
    background: var(--accent-cyan);
    color: var(--on-accent);
}

.model-card__slot-badge--right[b-icg4zlrl91] {
    background: var(--accent-yellow);
    color: var(--on-accent);
}

/* Rating, size and price, separated by a dot that is drawn rather than typed so a screen
   reader does not read "dot" three times (the aria-label already states the essentials).
   No opacity on any of this: the tokens are contrast-checked per theme, and multiplying them
   down is what once pushed fourteen pricing nodes below 4.5:1. */
.model-card__meta[b-icg4zlrl91] {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: flex-end;
    gap: 0 var(--space-sm);
    margin-inline-start: auto;
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.model-card__meta > * + *[b-icg4zlrl91]::before {
    content: "·";
    content: "·" / "";
    margin-inline-end: var(--space-sm);
    color: var(--text-dim);
}

.model-card__elo[b-icg4zlrl91] {
    color: var(--text);
}

.model-card__unrated[b-icg4zlrl91] {
    font-style: italic;
}

.model-card__free[b-icg4zlrl91] {
    color: var(--accent-green);
    font-style: italic;
}
/* /Components/ModelTypeBadge.razor.rz.scp.css */
/* ── ModelTypeBadge ──────────────────────────────────────────────────────────
   The shared colour palette is the existing --success/info/warn surface/border
   tokens defined in app.css. Remote (cloud) reads on the blue surface,
   Browser (local) on green, Ollama (localservice) on yellow.

   The token pairs are opaque (per CLAUDE.md "Status tints are the --*-surface /
   --*-border tokens, never an rgba() literal"), so the contrast of --accent-x
   on --x-surface is the same wherever the chip lands. */

.model-type-badge[b-8r8je97raq] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding-inline: var(--space-xs);
    padding-block: 1px;
    border-radius: var(--radius-sm, 4px);
    border: 1px solid transparent;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
    line-height: 1;
    vertical-align: middle;
}

.model-type-badge--long[b-8r8je97raq] {
    font-size: var(--text-sm);
    text-transform: none;
    letter-spacing: normal;
    font-weight: var(--weight-semibold);
}

.model-type-badge--remote[b-8r8je97raq] {
    background: var(--info-surface);
    border-color: var(--info-border);
    color: var(--accent-blue);
}

.model-type-badge--local[b-8r8je97raq] {
    background: var(--success-surface);
    border-color: var(--success-border);
    color: var(--accent-green);
}

.model-type-badge--localservice[b-8r8je97raq] {
    background: var(--warn-surface);
    border-color: var(--warn-border);
    color: var(--accent-yellow);
}
/* /Components/PageActions.razor.rz.scp.css */
/* ── PageActions ─────────────────────────────────────────────────────────────
   The follow-up row that used to be seven different per-surface action bands. Below
   640px the row wraps, gap is tokenised against --space-sm so it stays consistent with
   the rest of the app, and the warn variant inherits .po-btn--warn's amber treatment
   without each surface redefining it. */

.page-actions[b-xcgh5g28ta] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
}

.page-actions--align-start[b-xcgh5g28ta] { justify-content: flex-start; }
.page-actions--align-center[b-xcgh5g28ta] { justify-content: center; }
.page-actions--align-end[b-xcgh5g28ta] { justify-content: flex-end; }

.page-actions--primary[b-xcgh5g28ta] { /* visually identical to default; reserved for future emphasis */ }
.page-actions--secondary[b-xcgh5g28ta] { /* same */ }
.page-actions--warn[b-xcgh5g28ta] { /* hover ring driven by .po-btn--warn */ }
/* /Components/PageHeader.razor.rz.scp.css */
/* ── PageHeader ──────────────────────────────────────────────────────────────
   The page title row. Was carried by .home__header-row, .po-header__row and the
   Profile card's flex. The primitive below is geometry-only — no font-sizes here,
   those come from the .po-title on the inner h1. */

.page-header[b-p9ns6keip1] {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    margin-block-end: var(--space-md);
}

.page-header__row[b-p9ns6keip1] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-sm) var(--space-md);
    min-width: 0;
}

.page-header__title[b-p9ns6keip1] {
    margin: 0;
    min-width: 0;
}

.page-header__status[b-p9ns6keip1],
.page-header__actions[b-p9ns6keip1] {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

.page-header__subtitle[b-p9ns6keip1] {
    margin: 0;
}

.page-header__warning[b-p9ns6keip1] {
    margin: 0;
    color: var(--accent-yellow);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
}
/* /Components/PromptPicker.razor.rz.scp.css */
.prompt-picker[b-xt3p0f0lga] {
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    margin-top: var(--space-md);
}

/* Each section is a column so its head, list and disclosure stack with one gap rather than
   relying on the margins of whatever happens to be inside it. */
.prompt-picker__section[b-xt3p0f0lga] {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

/* Layout-only, alongside .po-btn: the library opens from here, so it needs to sit clear of
   the textarea above it. */
.prompt-picker__disclosure[b-xt3p0f0lga] {
    margin-top: var(--space-xs);
}

/* ── Category filters ─────────────────────────────────────────────────────── */

.prompt-picker__filters[b-xt3p0f0lga] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
}

/* The category chips are .po-btn--chip now — see app.css. This was the third private copy
   of the control (Home's type filters and Archive's verdict filter were the others), and the
   only one of the three that got aria-pressed right; the shared modifier keeps that and
   gives it to the other two. */

/* ── Library cards ────────────────────────────────────────────────────────── */

.prompt-picker__grid[b-xt3p0f0lga] {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-sm);
}

.prompt-picker__card[b-xt3p0f0lga] {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding: var(--space-sm) var(--space-md);
    text-align: left;
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: border-color var(--transition-fast), transform var(--transition-fast);
}

.prompt-picker__card:hover[b-xt3p0f0lga] {
    border-color: var(--accent-blue);
    transform: translateY(-1px);
}

.prompt-picker__card--active[b-xt3p0f0lga] {
    border-color: var(--accent-green);
    background: var(--surface-accent);
}

.prompt-picker__card:focus-visible[b-xt3p0f0lga] {
    outline: none;
    box-shadow: var(--focus-ring);
}

.prompt-picker__card-head[b-xt3p0f0lga] {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

.prompt-picker__card-emoji[b-xt3p0f0lga] {
    font-size: var(--text-lg);
}

.prompt-picker__card-title[b-xt3p0f0lga] {
    font-size: var(--text-md);
    font-weight: 700;
    color: var(--text);
}

.prompt-picker__card-tag[b-xt3p0f0lga] {
    margin-left: auto;
    font-size: var(--text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--accent-cyan);
    border: 1px solid currentColor;
    border-radius: var(--radius-full);
    padding: 0 var(--space-sm);
}

.prompt-picker__card-text[b-xt3p0f0lga] {
    font-size: var(--text-xs);
    line-height: 1.45;
    color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
    .prompt-picker__card:hover[b-xt3p0f0lga] {
        transform: none;
    }
}
/* /Components/SandboxedViewport.razor.rz.scp.css */
/* Extracted from a <style> block inside SandboxedViewport.razor — scoped, so these rules
   no longer leak to every component on the page. */

/* --preview-w / --preview-h are the one knob for how big a rendered result is. Both frames
   read them, so the two Arena panels are always the same size as each other. Fixed pixels
   rather than a viewport fraction: the frame is a canvas the model's own HTML lays itself out
   against, so a size that moves with the window would render a different page on every
   screen. Previously `flex: 1` with a 500px floor, which made each side taller than the
   viewport and put the verdict below the fold on every duel.

   --preview-scale is the display knob, and it is deliberately NOT declared here: it inherits
   from whoever hosts this component and falls back to 1. The Arena sets it on wide screens so
   the preview fills its column, while the document inside still lays out at exactly
   --preview-w x --preview-h — the size the models were told to design for and the judge
   screenshots at. A transform, so it is compositor-only (CLAUDE.md's motion rule). */
.sandboxed-viewport[b-vmgrxrsqv3] {
    --preview-w: 320px;
    --preview-h: 180px;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--surface-1);
    transition: filter 0.4s ease, opacity 0.4s ease;
    /* Sized by its contents, and exactly as wide as the frame. It used to be `height: 100%`,
       which resolves against an auto-height parent and stretched the card well past the
       320x180 frame inside it — the empty remainder is what the telemetry HUD ended up
       drawn over. Nothing here should stretch; the frame's fixed box is the whole size.
       Width is min(100%, frame+border) rather than `fit-content`: fit-content against a
       child capped by max-width:100% is a cyclic resolution that collapses to ~1px whenever
       this card has no other wide content — which in the Arena is ALWAYS, because the model
       name and quality chip live in the slot header *outside* this card. Measured live: the
       preview iframe rendered 0x180 inside a 352px grid slot. The explicit min() keeps the
       frame's exact size while guaranteeing a real width to resolve the frame's max against. */
    width: min(100%, calc(var(--preview-w) * var(--preview-scale, 1) + 2px));
    min-width: 0;
    margin-inline: auto;
}

/* The losing side is marked, not hidden.
   This used to be `filter: grayscale(80%) brightness(0.5); opacity: 0.6`, which dragged every
   colour underneath it below 4.5:1 — the header, the LOSER badge and the diagnostics line all
   failed contrast in both themes (2.3–3.1:1), and the output itself became grey-on-grey. The
   losing output is exactly what a person needs to read to check the verdict, so the loss is
   now signalled by the badge and a red edge that cost the content nothing. */
.sandboxed-viewport--loser[b-vmgrxrsqv3] {
    border-color: var(--accent-red);
    box-shadow: inset 3px 0 0 0 var(--accent-red);
}

.sandboxed-viewport__header[b-vmgrxrsqv3] {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-smd);
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
}

.sandboxed-viewport__model-name[b-vmgrxrsqv3] {
    color: var(--text);
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sandboxed-viewport__score[b-vmgrxrsqv3] {
    margin-left: auto;
    color: var(--accent-blue);
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    border: 1px solid var(--info-border);
    border-radius: var(--radius-full);
    padding: var(--space-2xs) var(--space-sm);
    background: var(--info-surface);
}

/* With findings the score is a <details>: the summary is the chip, and the list opens over
   the top of the frame rather than pushing it down (the two panels must stay level). */
.sandboxed-viewport__score--findings > summary[b-vmgrxrsqv3] {
    cursor: pointer;
    list-style: none;
}

.sandboxed-viewport__score--findings > summary[b-vmgrxrsqv3]::-webkit-details-marker {
    display: none;
}

.sandboxed-viewport__findings[b-vmgrxrsqv3] {
    position: absolute;
    inset-inline: var(--space-sm);
    inset-block-start: 100%;
    z-index: 2;
    margin: 0;
    padding: var(--space-sm) var(--space-smd) var(--space-sm) var(--space-lg);
    font-size: var(--text-xs);
    font-weight: var(--weight-normal);
    color: var(--text);
    background: var(--warn-surface);
    border: 1px solid var(--warn-border);
    border-radius: var(--radius-sm);
}

.sandboxed-viewport__badge[b-vmgrxrsqv3] {
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    padding: var(--space-2xs) var(--space-sm);
    border-radius: var(--radius-sm);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    white-space: nowrap;
    /* The ELO shift counts up inside the badge; tabular figures stop it jittering. */
    font-variant-numeric: tabular-nums;
}

/* ── ELO odometer ────────────────────────────────────────────────────────────
   Each digit is a 1em window onto a strip of 0–9 twice over; the page moves --reel from 0 to
   the digit + 10, and the transform transition is the roll. Later (rightward) reels spin
   longer, like the real thing. Starts after the badge's stamp has landed. Compositor-only. */
.sandboxed-viewport__odometer[b-vmgrxrsqv3] {
    display: inline-flex;
    line-height: 1;
    vertical-align: text-bottom;
}

.sandboxed-viewport__reel[b-vmgrxrsqv3] {
    display: inline-block;
    height: 1em;
    overflow: hidden;
}

.sandboxed-viewport__reel-strip[b-vmgrxrsqv3] {
    display: flex;
    flex-direction: column;
    transform: translateY(calc(var(--reel, 0) * -1em));
    transition: transform calc(700ms + var(--reel-index, 0) * 180ms) cubic-bezier(.15, .85, .25, 1) 350ms;
}

.sandboxed-viewport__reel-strip > span[b-vmgrxrsqv3] {
    height: 1em;
}

@media (prefers-reduced-motion: reduce) {
    .sandboxed-viewport__reel-strip[b-vmgrxrsqv3] {
        transition: none;
    }
}

/* --on-accent, not #fff: white on --accent-red measured 3.76:1 in the dark theme. */
.sandboxed-viewport__badge--loser[b-vmgrxrsqv3] {
    background: var(--accent-red);
    color: var(--on-accent);
}

.sandboxed-viewport__badge--winner[b-vmgrxrsqv3] {
    background: var(--accent-green);
    color: var(--on-accent-green);
}

.sandboxed-viewport__empty[b-vmgrxrsqv3] {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    color: var(--text-muted);
    font-size: var(--text-md);
    font-style: italic;
    /* Matches the stage's box so the panel does not resize when a result arrives. */
    width: calc(var(--preview-w) * var(--preview-scale, 1));
    height: calc(var(--preview-h) * var(--preview-scale, 1));
    max-width: 100%;
    margin-inline: auto;
}

.sandboxed-viewport__empty-icon[b-vmgrxrsqv3] {
    font-size: var(--text-3xl);
    opacity: 0.4;
}

/* The frame's box at display size. A transform does not change layout, so without this the
   scaled iframe would keep occupying 320x180 and paint over whatever follows it. At the
   default scale of 1 this is exactly the old fixed 320x180 card. */
.sandboxed-viewport__stage[b-vmgrxrsqv3] {
    flex: 0 0 auto;
    width: calc(var(--preview-w) * var(--preview-scale, 1));
    height: calc(var(--preview-h) * var(--preview-scale, 1));
    max-width: 100%;
    margin-inline: auto;
    overflow: hidden;
}

/* A fixed 320x180 card rather than a stretched panel, so both results and the verdict fit
   in one screen without scrolling. */
.sandboxed-viewport__frame[b-vmgrxrsqv3] {
    /* Block, not the iframe default of inline: an inline replaced element sits on the text
       baseline and leaves a descender gap under it inside the bordered card. */
    display: block;
    width: var(--preview-w);
    height: var(--preview-h);
    max-width: 100%;
    border: none;
    transform: scale(var(--preview-scale, 1));
    transform-origin: 0 0;
    /* Stays white in both themes on purpose: this frame renders a model's own HTML
       document, which brings its own background and assumes a white canvas. */
    background: #fff;
}

/* ── Container queries ───────────────────────────────────────────────────────
   The frame is a fixed 320×180 (the size the models are told to design for), but the
   HEADER above it is fluid and renders in three contexts: an Arena panel at half the
   page, a Tournament slot, and a full-width phone card. It had no responsive handling.

   Container, not media query — same reasoning as TelemetryHud and ModelCard: what
   varies is the box this component was given, not the window. */
.sandboxed-viewport[b-vmgrxrsqv3] {
    container-type: inline-size;
    container-name: viewport;
}

/* Below the frame's own width the header cannot hold `name ... score` on one line, and
   the name — which is ellipsised — is what loses. Stack instead, so the model name gets
   the full width and the score keeps its own row.

   This is the fix for the truncation itself: the name only ellipsised because it was
   competing for a line it could not win. */
@container viewport (max-width: 320px) {
    .sandboxed-viewport__header[b-vmgrxrsqv3] {
        flex-wrap: wrap;
        gap: var(--space-2xs) var(--space-sm);
    }

    .sandboxed-viewport__model-name[b-vmgrxrsqv3] {
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
        overflow-wrap: anywhere;
        flex: 1 1 100%;
    }

    .sandboxed-viewport__score[b-vmgrxrsqv3] {
        margin-left: 0;
    }
}

/* ── Verdict reveal ──────────────────────────────────────────────────────────
   One-shot, and only when the verdict lands while the page is watching (--reveal; a duel
   opened already decided renders its badges still). Transform and opacity only, and by the
   time a verdict exists inference has finished.

   The loser dips and comes back to FULL opacity. It is never left dimmed or desaturated: the
   losing output is what a person reads to check the verdict (see .sandboxed-viewport--loser). */
.sandboxed-viewport--reveal.sandboxed-viewport--loser[b-vmgrxrsqv3] {
    animation: sandboxed-viewport-concede-b-vmgrxrsqv3 620ms ease-out both;
}

.sandboxed-viewport--reveal .sandboxed-viewport__badge[b-vmgrxrsqv3] {
    animation: sandboxed-viewport-stamp-b-vmgrxrsqv3 420ms cubic-bezier(.2, .9, .25, 1.4) 120ms both;
}

/* A band of light crossing the WINNER badge once. Clipped to the badge by overflow. */
.sandboxed-viewport--reveal .sandboxed-viewport__badge--winner[b-vmgrxrsqv3] {
    position: relative;
    overflow: hidden;
}

.sandboxed-viewport--reveal .sandboxed-viewport__badge--winner[b-vmgrxrsqv3]::after {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    width: 45%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.75), transparent);
    transform: translateX(-130%);
    animation: sandboxed-viewport-sweep-b-vmgrxrsqv3 760ms ease-out 460ms both;
    pointer-events: none;
}

@keyframes sandboxed-viewport-concede-b-vmgrxrsqv3 {
    from { opacity: 1; transform: none; }
    35%  { opacity: 0.55; transform: translateY(3px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

@keyframes sandboxed-viewport-stamp-b-vmgrxrsqv3 {
    from { opacity: 0; transform: scale(1.8); }
    to   { opacity: 1; transform: none; }
}

@keyframes sandboxed-viewport-sweep-b-vmgrxrsqv3 {
    from { transform: translateX(-130%); }
    to   { transform: translateX(330%); }
}

@media (prefers-reduced-motion: reduce) {
    .sandboxed-viewport--reveal.sandboxed-viewport--loser[b-vmgrxrsqv3],
    .sandboxed-viewport--reveal .sandboxed-viewport__badge[b-vmgrxrsqv3] {
        animation: none;
    }

    .sandboxed-viewport--reveal .sandboxed-viewport__badge--winner[b-vmgrxrsqv3]::after {
        display: none;
    }
}
/* /Components/TelemetryHud.razor.rz.scp.css */
/* Extracted from a <style> block inside TelemetryHud.razor — scoped, so these rules
   no longer leak to every component on the page. */

/* --glass-bg rather than a literal rgba(0,0,0,.85): the HUD floats over a model's
   rendered output, so it needs a translucent scrim that darkens on a dark page and
   lightens on a light one. */
.telemetry-hud[b-04700tr5m5] {
    background: var(--glass-bg);
    /* The blur the --glass-bg token always implied. Without it this is a translucent panel,
       not glass. backdrop-filter composites on the GPU compositor, not the 3D pipeline, so it
       does not contend with WebLLM inference the way a shader pass would. */
    backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-smd);
    font-size: var(--text-sm);
    color: var(--text);
}

/* tok/s history. `color` is what sparkline.js strokes with, so the line follows the theme. */
.telemetry-hud__spark[b-04700tr5m5] {
    display: block;
    width: 100%;
    height: var(--space-xl);
    margin-top: var(--space-sm);
    color: var(--accent-cyan);
}

/* Diagnostics disclosure. Closed by default — these are the numbers you go looking for,
   not the ones you compare two models on. */
.telemetry-hud__more[b-04700tr5m5] {
    margin-top: var(--space-sm);
}

.telemetry-hud__more-summary[b-04700tr5m5] {
    font-size: var(--text-xs);
    color: var(--text-muted);
    cursor: pointer;
    min-height: var(--target-min);
    display: flex;
    align-items: center;
}

.telemetry-hud__more-summary:hover[b-04700tr5m5] {
    color: var(--text);
}

.telemetry-hud__more-summary:focus-visible[b-04700tr5m5] {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: var(--radius-sm);
}

.telemetry-hud__grid[b-04700tr5m5] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* See the container-query note at the foot of this file. */
    gap: var(--space-xs) var(--space-smd);
}

.telemetry-hud__item[b-04700tr5m5] {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
}

.telemetry-hud__label[b-04700tr5m5] {
    color: var(--text-muted);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.telemetry-hud__value[b-04700tr5m5] {
    color: var(--text);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
}

.telemetry-hud__value--green[b-04700tr5m5] {
    color: var(--accent-green);
}

.telemetry-hud__value--yellow[b-04700tr5m5] {
    color: var(--accent-yellow);
}

.telemetry-hud__pending[b-04700tr5m5] {
    color: var(--text-muted);
    font-style: italic;
}

/* ── Live track ──────────────────────────────────────────────────────────────
   While the side streams: how far along it is against the front-runner. Was TokenRace's lane,
   one per side in a separate panel above both outputs. --pos is written by the component. */
.telemetry-hud__track[b-04700tr5m5] {
    height: var(--space-xs);
    margin-bottom: var(--space-sm);
    border-radius: var(--radius-full);
    background: var(--surface-2);
    overflow: hidden;
}

.telemetry-hud__fill[b-04700tr5m5] {
    width: var(--pos, 0%);
    height: 100%;
    background: linear-gradient(90deg, var(--accent-blue), var(--accent-cyan));
    transition: width var(--transition-medium);
}

.telemetry-hud__fill--failed[b-04700tr5m5] {
    background: var(--accent-red);
}

@media (prefers-reduced-motion: reduce) {
    .telemetry-hud__fill[b-04700tr5m5] {
        transition: none;
    }
}

/* ── Container queries ────────────────────────────────────────────────────
   This HUD is placed inside the Arena's viewport panel, whose width varies far more than the
   viewport's does: full width on a desktop duel, roughly half that when the Arena is showing
   two panels side by side, and narrower again on a phone. A @media query asks the wrong
   question in all three cases — it measures the window, not the box this component was given.

   The panel opts in by declaring itself a container (see .arena-panel__hud in ArenaViewportPanel.razor.css), and the metric grid
   then responds to the space it actually has. */
@container telemetry (max-width: 260px) {
    .telemetry-hud__grid[b-04700tr5m5] {
        grid-template-columns: 1fr;
    }
}
/* /Components/ValueChart.razor.rz.scp.css */
/* The viewBox is 720 units wide and the figure is capped at 720px, so one unit is at most one
   pixel and the type tokens below render at their real size — an SVG that scaled up with a
   wide page would scale its text with it. */
.value-chart[b-peqr0idoca] {
    max-inline-size: 45rem;
    margin: 0 0 var(--space-lg);
    /* A size container, so the type below can answer the width the figure was given. */
    container: value-chart / inline-size;
}

.value-chart__svg[b-peqr0idoca] {
    display: block;
    inline-size: 100%;
    block-size: auto;
}

.value-chart__legend[b-peqr0idoca] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs) var(--space-md);
}

.value-chart__key[b-peqr0idoca] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
}

/* Filled against hollow, not one colour against another: the distinction survives greyscale. */
.value-chart__swatch[b-peqr0idoca] {
    inline-size: var(--space-smd);
    block-size: var(--space-smd);
    border: 2px solid var(--text-muted);
    border-radius: 50%;
    background: var(--surface-1);
}

.value-chart__swatch--frontier[b-peqr0idoca] {
    border-color: var(--accent-cyan);
    background: var(--accent-cyan);
}

.value-chart__grid[b-peqr0idoca] {
    stroke: var(--border);
    stroke-width: 1;
}

.value-chart__divider[b-peqr0idoca] {
    stroke: var(--border-strong);
    stroke-width: 1;
    stroke-dasharray: 3 3;
}

.value-chart__tick[b-peqr0idoca] {
    fill: var(--text-muted);
    font-size: var(--text-2xs);
    font-variant-numeric: tabular-nums;
}

.value-chart__label[b-peqr0idoca] {
    fill: var(--text);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    /* A surface-coloured halo, so a name crossing a gridline or the frontier stays legible. */
    paint-order: stroke;
    stroke: var(--surface-base);
    stroke-width: 3;
    stroke-linejoin: round;
}

.value-chart__frontier[b-peqr0idoca] {
    fill: none;
    stroke: var(--accent-cyan);
    stroke-width: 2;
    stroke-linejoin: round;
}

.value-chart__hit[b-peqr0idoca] {
    fill: transparent;
}

.value-chart__dot[b-peqr0idoca] {
    fill: var(--surface-1);
    stroke: var(--text-muted);
    stroke-width: 2;
}

.value-chart__dot--frontier[b-peqr0idoca] {
    fill: var(--accent-cyan);
    stroke: var(--surface-base);
}

.value-chart__link:hover .value-chart__dot[b-peqr0idoca],
.value-chart__link:focus-visible .value-chart__dot[b-peqr0idoca] {
    stroke: var(--text);
    stroke-width: 3;
}

/* Phones drop the cost and value columns from the table (see Leaderboard.razor.css); the chart
   of those same numbers goes with them rather than shrinking its labels past reading. */
@media (max-width: 640px) {
    .value-chart[b-peqr0idoca] {
        display: none;
    }
}

/* In a column narrower than the viewBox (the leaderboard's side column is ~544px) every unit
   is about three quarters of a pixel, which took the tick labels under 9px. One step up the
   scale each, so they render near their intended size again. Container, not media: what
   varies is the box this figure was given. */
@container value-chart (max-width: 38rem) {
    .value-chart__tick[b-peqr0idoca] {
        font-size: var(--text-sm);
    }

    .value-chart__label[b-peqr0idoca] {
        font-size: var(--text-md);
    }

    /* The dot's invisible hit circle is r=12 units — 24px at full size, 18px here. */
    .value-chart__hit[b-peqr0idoca] {
        r: 17px;
    }
}
/* /Layout/MainLayout.razor.rz.scp.css */
.page[b-z74awdrume] {
    position: relative;
    display: flex;
    flex-direction: column;
    /* dvh so the layout matches the visible viewport on mobile, where the collapsing
       browser toolbar is counted inside 100vh. */
    min-height: 100dvh;
}

/* Transparent, not a colour: html/body already paint --surface-base plus the page
   gradient, and both follow the theme. These two used to be hardcoded #000000, which
   was invisible while Radzen's always-dark stylesheet sat underneath -- once that went,
   a light-theme page rendered near-black --text on a black canvas. */
main[b-z74awdrume] {
    flex: 1;
    background-color: transparent;
    color: var(--text);
}

article.content[b-z74awdrume] {
    background-color: transparent;
    color: var(--text);
    padding-top: var(--space-md);
}

article.app-content[b-z74awdrume] {
    /* max(), not two rules. This was previously TWO selectors of equal specificity in this
       same file — `article.app-content { padding-inline: clamp(...) }` here and
       `article.content { padding-inline: env(...) }` at the bottom — and the later one
       silently won, resolving the gutter to env() = 0px on every device without a notch.
       The responsive gutter was therefore dead code for its whole life, and nothing warns,
       because an overridden declaration is valid CSS.

       max() is the correct composition anyway: the clamp is the floor and a real inset
       extends it rather than replacing it.

       The floor is 1rem rather than 0.75rem because this is now the app's ONLY horizontal
       gutter: .po-page used to add --space-md on top of it and no longer does (see the
       .po-page rule in app.css for the measurements). 16px on a phone, growing to 24px past
       1200px, is what every route gets — where before they got 28px, 36px or 40px depending
       on which shell the page happened to use. */
    padding-inline: max(clamp(1rem, 2vw, 1.5rem), env(safe-area-inset-left))
                    max(clamp(1rem, 2vw, 1.5rem), env(safe-area-inset-right));
    overflow-x: clip;
}

/* AGENT.MD §12 Rule 21: YYMMDD build version in the bottom-right corner. */
/* Absolute to .page, not fixed to the viewport: fixed, it sat on top of whatever scrolled
   under the corner — the last Archive card on a phone. Here it is the corner of the page
   itself, inside .po-page's bottom padding, which is the viewport's corner on every page
   that fits one screen. */
.build-stamp[b-z74awdrume] {
    position: absolute;
    right: max(var(--space-sm), env(safe-area-inset-right));
    bottom: max(var(--space-2xs), env(safe-area-inset-bottom));
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-muted);
    opacity: 0.65;
    pointer-events: none;
    user-select: none;
    z-index: 10;
}



/* ── Responsive ──────────────────────────────────────────────────────────────
   The layout itself is a single column, so there is no breakpoint to add — what it
   needs is the safe-area guarantee. The sticky nav sits at the top inset and the
   page content runs to the bottom one; on a notched phone in landscape the left and
   right insets are what would otherwise clip the first and last controls in a row.

   The inset goes on the CONTENT, not on .page. Putting it on .page indents the sticky
   nav's background with it, so the page gradient shows as two bars down the outside of
   a bar that is meant to be full-bleed. NavMenu insets its own inner row instead.

   The horizontal inset is composed into the `article.app-content` rule above (via
   max()); it must NOT be restated here as its own `article.content` rule, which is what
   silently zeroed the gutter — see the comment there. */
/* /Layout/NavMenu.razor.rz.scp.css */
/* ── Top horizontal navbar — portrait/phone-first ───────────────────────────
   Everything here is namespaced .navmenu__. The markup used to carry Bootstrap
   class names (navbar-*, container-fluid, nav-item, btn-sm, btn-outline-*)
   without Bootstrap being loaded, so several of them styled nothing at all. */
.navmenu[b-vmilrheswj] {
    background-color: var(--surface-base);
    border-bottom: 1px solid var(--border);
    padding: 0 var(--space-md);
    /* The token, not the literal. app.css tells callers to write
       calc(100dvh - var(--nav-height)) for a viewport-relative floor, and that instruction
       is only true while this bar and that token agree — so the bar reads it rather than
       restating 3.5rem beside it. */
    min-height: var(--nav-height);
    position: sticky;
    top: 0;
    z-index: 100;
    width: 100%;
    /* Include the 1rem inline padding in the 100% width; without border-box the
       header overflowed the viewport by 2rem and MainLayout's overflow-x:clip
       sheared the right edge off the nav toggle on mobile portrait. */
    box-sizing: border-box;
}

.navmenu__inner[b-vmilrheswj] {
    width: 100%;
    max-width: 1400px;
    margin: 0 auto;
    /* Safe-area insets go on this inner row, not on the bar or on .page: the bar's
       background must stay full-bleed edge to edge, while the CONTROLS inside it must
       clear a landscape notch and the curved corners. env() is 0px everywhere else. */
    padding: 0 env(safe-area-inset-right) 0 env(safe-area-inset-left);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-smd);
}

/* ::deep — the element is rendered by <BrandMark>, outside this file's scope id. */
[b-vmilrheswj] .navmenu__brand {
    /* Geometry-only hook for <BrandMark />: flex, gap, the touch-target min-height, and a
       hover accent. The brand glyph and label live in BrandMark.razor.css. */
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex: 0 0 auto;
    white-space: nowrap;
    min-height: 44px;           /* ≥44px touch target */
    text-decoration: none;
    color: var(--text);
    font-weight: 700;
    font-size: var(--text-xl);
    letter-spacing: -0.02em;
}

[b-vmilrheswj] .navmenu__brand:hover {
    color: var(--accent-green);
}


.navmenu__collapse[b-vmilrheswj] {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-width: 0;
    flex: 1 1 auto;
}

.navmenu__links[b-vmilrheswj] {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    list-style: none;
    margin: 0;
    padding: 0;
    /* Never shrink: the links are nowrap, so a shrunken box would let its text
       spill out and be overlapped by whatever sits next to it. */
    flex: 0 0 auto;
    min-width: 0;
}

.navmenu__links > li[b-vmilrheswj] {
    flex: 0 0 auto;
}

/* ── Nav links ─────────────────────────────────────────────────────────────
   ::deep because NavLink renders the anchor itself, outside this component's
   scope id. The .active class is appended by NavLink, not by our markup. */
[b-vmilrheswj] .navmenu__link {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    white-space: nowrap;
    color: var(--text-muted);
    padding: var(--space-sm) var(--space-smd);
    border-radius: 6px;
    text-decoration: none;
    font-size: var(--text-md);
    transition: color 0.15s ease, background 0.15s ease;
}

[b-vmilrheswj] .navmenu__link:hover {
    color: var(--text);
    background: var(--surface-1);
}

[b-vmilrheswj] .navmenu__link.active {
    color: var(--accent-green);
    background: var(--success-surface);
    font-weight: 600;
}

.navmenu__link-text[b-vmilrheswj] {
    font-size: var(--text-md);
}

/* ── Mobile collapsed menu ──────────────────────────────────────────────────── */
/* ── Known debt: Bootstrap-era breakpoints ────────────────────────────────
   767.98 / 991.98 are Bootstrap's off-by-0.02 boundaries (1279.98 went with the old
   right-hand cluster), left from before Bootstrap
   was removed. They are NOT on the app's 640/1024/1400 scale. The fractional part is
   load-bearing here rather than cosmetic — each pairs with a `min-width: 768px` rule, and
   rounding one side without the other opens a gap where neither matches.

   Deliberately not consolidated in the 2026-08-22 design-token pass: the nav is the one
   surface where a wrong breakpoint is immediately visible (the hamburger appearing at the
   wrong width), and that pass had no way to verify the result in a browser. Fix it with the
   E2E-UI suite open. */
/* Short label forms exist for the phone bar only. */
.navmenu__link-text--short[b-vmilrheswj],
.navmenu__user-initial[b-vmilrheswj] {
    display: none;
}

@media (max-width: 767.98px) {
    /* ── One row, no drawer ──────────────────────────────────────────────────
       The bar was brand + hamburger with everything in a drawer (two taps for a list of
       four), then brand + session on one row and the links on a second — 100px of every phone
       screen. Now it is one 3.5rem row: the brand's mark without its wordmark, the four links
       with short forms for the two long labels, and the identity badge as an initial. That is
       ~324px of content, so it fits a 360px phone with its 16px gutters. */
    .navmenu__inner[b-vmilrheswj] {
        gap: var(--space-xs);
    }

    .navmenu__collapse[b-vmilrheswj] {
        display: contents;
    }

    [b-vmilrheswj] .navmenu__brand .brand-mark__label,
    .navmenu__link-text--long[b-vmilrheswj],
    .navmenu__user-badge-name[b-vmilrheswj],
    .navmenu__user-badge-domain[b-vmilrheswj] {
        display: none;
    }

    .navmenu__link-text--short[b-vmilrheswj] {
        display: inline;
    }

    .navmenu__links[b-vmilrheswj] {
        flex: 1 1 auto;
        justify-content: space-between;
        gap: 0;
        /* A bar too narrow for all four (a MOCK chip on a 360px phone) scrolls the row rather
           than clipping a link. */
        overflow-x: auto;
        scrollbar-width: none;
    }

    [b-vmilrheswj] .navmenu__link {
        justify-content: center;
        min-block-size: 2.75rem;
        padding: 0 var(--space-xs);
        font-size: var(--text-sm);
    }

    .navmenu__link-text[b-vmilrheswj] {
        font-size: var(--text-sm);
    }

    /* A fixed slot for the session control while signed in (or not yet known), so the links
       are laid out once: as a flexible box, the badge arriving took width from them and every
       link shifted. 3rem is the disc plus its caret. */
    .navmenu__inner:not(:has(.navmenu__auth > :not(.navmenu__menu))) .navmenu__auth[b-vmilrheswj] {
        flex: 0 0 3rem;
    }

    /* The badge becomes a 28px disc holding the initial. */
    .navmenu__user-badge[b-vmilrheswj] {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        inline-size: 1.75rem;
        block-size: 1.75rem;
        padding: 0;
    }

    .navmenu__user-initial[b-vmilrheswj] {
        display: inline;
    }

    /* Signed out, the session controls are Sign in, Guest and the gear — too wide to share the
       row — so the links drop to a second one as they used to. This bar is then 6.25rem, taller
       than --nav-height; the only page it shows on is the sign-in card, which pins nothing. */
    .navmenu__inner:has(.navmenu__auth > :not(.navmenu__menu))[b-vmilrheswj] {
        flex-wrap: wrap;
    }

    .navmenu__inner:has(.navmenu__auth > :not(.navmenu__menu)) .navmenu__links[b-vmilrheswj] {
        order: 3;
        flex: 1 0 100%;
        border-top: 1px solid var(--border);
    }
}

/* ── USING MOCK DATA chip (standards §7) ────────────────────────────────────
   A compact status tint beside the brand, not a full-width row: the full sentence is in the
   title and a visually-hidden span. Opaque --danger-* tokens, so --accent-red text keeps the
   same contrast on it in both themes. */
.navmenu__mock-chip[b-vmilrheswj] {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    padding: var(--space-2xs) var(--space-sm);
    border-radius: var(--radius-full);
    border: 1px solid var(--danger-border);
    background: var(--danger-surface);
    color: var(--accent-red);
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.05em;
    cursor: help;
}

/* ── Auth / GUEST identity (standards §6) ──────────────────────────────────── */
/* Takes the rest of the row and packs its controls to the right edge. With margin-left: auto
   it was only as wide as its content, so when the session resolved and the badge arrived the
   box grew leftwards — a layout shift on every route. Now its left edge never moves. */
.navmenu__auth[b-vmilrheswj] {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-sm);
    flex: 1 0 auto;
}

/* The buttons keep their intrinsic width; only the identity badge below is allowed to
   give ground, because it is the only child that truncates cleanly. */
.navmenu__auth > .po-btn[b-vmilrheswj] {
    flex: 0 0 auto;
    white-space: nowrap;
}

.navmenu__user-badge[b-vmilrheswj] {
    font-size: var(--text-sm);
    font-weight: 600;
    padding: var(--space-xs) var(--space-sm);
    border-radius: 999px;
    letter-spacing: 0.03em;
    white-space: nowrap;
    flex: 0 1 auto;
    min-width: 0;
    max-width: clamp(120px, 22vw, 260px);
    /* Ellipsis is the LAST resort now, not the first. The badge sheds its domain span
       (below) before it clips, so an identity that used to render as
       "punkouter26@gmail…." now renders as "punkouter26" — a whole word, and the half
       that identifies the account. The full value is in the title attribute regardless. */
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The '@domain' half. Present on wide screens, dropped when the bar gets tight — dropping
   a complete token reads as a deliberate short form, where a mid-string ellipsis reads as
   a bug. Guest identities have no '@' and render nothing here. */
.navmenu__user-badge-domain[b-vmilrheswj] {
    opacity: 0.75;
}

@media (max-width: 1024px) {
    .navmenu__user-badge-domain[b-vmilrheswj] {
        display: none;
    }

    /* With the domain gone the badge no longer needs a viewport-proportional cap — a local
       part is short, so let it size to its content and stop competing for the row. */
    .navmenu__user-badge[b-vmilrheswj] {
        max-width: none;
    }
}

.navmenu__user-badge--guest[b-vmilrheswj] {
    background: var(--warn-surface);
    color: var(--accent-yellow);
    border: 1px solid var(--warn-border);
}

.navmenu__user-badge--oauth[b-vmilrheswj] {
    background: var(--success-surface);
    color: var(--accent-green);
    border: 1px solid var(--success-border);
}

/* ── User menu (<details>) ─────────────────────────────────────────────────────
   Settings, Catalog and Logout behind the identity badge. Signed out, the gear is a plain
   link to /settings (.navmenu__settings-link) — a menu of one item is a detour.
   Focus rings and the 24px/44px target floor for button and summary come from app.css;
   only the NavLink anchor needs the floor restated. */
.navmenu__menu[b-vmilrheswj] {
    position: relative;
    flex: 0 1 auto;
    min-width: 0;
}

.navmenu__menu-trigger[b-vmilrheswj] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    max-width: 100%;
    padding: var(--space-2xs);
    border-radius: var(--radius-full);
    color: var(--text);
    cursor: pointer;
    list-style: none;           /* drops the disclosure triangle (Firefox) */
}

.navmenu__menu-trigger[b-vmilrheswj]::-webkit-details-marker {
    display: none;              /* ...and in Chromium/WebKit */
}

.navmenu__menu-trigger:hover[b-vmilrheswj] {
    background: var(--surface-1);
}

/* The signed-out gear is an anchor, which app.css's 24px floor for button and summary
   does not cover. */
.navmenu__settings-link[b-vmilrheswj] {
    justify-content: center;
    min-block-size: var(--target-min);
    min-inline-size: var(--target-min);
    text-decoration: none;
}

.navmenu__menu-caret[b-vmilrheswj] {
    font-size: var(--text-xs);
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

.navmenu__menu[open] .navmenu__menu-caret[b-vmilrheswj] {
    transform: rotate(180deg);
}

.navmenu__menu-panel[b-vmilrheswj] {
    position: absolute;
    inset-inline-end: 0;
    inset-block-start: calc(100% + var(--space-xs));
    z-index: 110;
    display: flex;
    flex-direction: column;
    min-width: max-content;
    padding: var(--space-xs);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--glass-shadow);
}

/* ::deep because the Catalog entry is a NavLink, whose anchor renders outside this
   component's scope id; the buttons match it too. */
[b-vmilrheswj] .navmenu__menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    width: 100%;
    min-block-size: var(--target-min);
    padding: var(--space-sm) var(--space-smd);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font: inherit;
    font-size: var(--text-md);
    text-align: start;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}

[b-vmilrheswj] .navmenu__menu-item:hover {
    background: var(--surface-2);
    text-decoration: none;
}

[b-vmilrheswj] .navmenu__menu-item.active {
    color: var(--accent-green);
    font-weight: 600;
}

.navmenu__menu-item--logout[b-vmilrheswj] {
    border-top: 1px solid var(--border);
    border-start-start-radius: 0;
    border-start-end-radius: 0;
}

.navmenu__menu-glyph[b-vmilrheswj] {
    font-size: var(--text-lg);
    line-height: 1;
}

/* ── Tablet crowding ────────────────────────────────────────────────────────
   Between 768 and 992px the bar carries brand + four links + the menu trigger on one row.
   The wordmark drops out; the
   anchor's name comes from its aria-label, so hiding the text costs a screen reader nothing.
   The theme-label, link-padding and badge-width trims that used to live here up to 1280px
   went with the right-hand cluster, which is now a single trigger. */
@media (min-width: 768px) and (max-width: 991.98px) {
    [b-vmilrheswj] .navmenu__brand .brand-mark__label {
        display: none;
    }
}

/* /Pages/Archive.razor.rz.scp.css */
/* ── Page shell ─────────────────────────────────────────────────────────────
   Geometry comes from .po-page--wide on the element, the same as every other route. The
   page scrolls normally; a flex column only so one gap spaces the blocks. */
.archive[b-395elwk7vy] {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

/* PageHeader brings its own bottom margin; here the flex gap already spaces the header from
   the search box, and the two added together doubled it. ::deep because the header's root is
   PageHeader's markup, which this file's scope id never reaches. */
.archive[b-395elwk7vy]  .page-header {
    margin-block-end: 0;
}

/* .archive__title carries NO styling — E2E-UI selector hook only, and .po-title on the same
   element is the size. It had a --text-2xl and a raw `margin-bottom: 24px` (the only raw px
   length left in this file) layered over that primitive. */

/* Lines the two row buttons up. */
.archive__row-actions[b-395elwk7vy] {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-xs);
}

/* Layout only — the look is .po-btn .po-btn--sm's. They were a private 32px button with raw
   px sizes and their own hover colour; a square minimum keeps the glyph-only pair from
   reading as two different widths. */
.archive__row-btn[b-395elwk7vy] {
    flex: 0 0 auto;
    min-width: var(--target-min);
}

.archive__verdict[b-395elwk7vy] {
    display: inline-block;
    padding: var(--space-2xs) var(--space-sm);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
}

/* A judged tie is an outcome, so it reads as one — coloured and bordered like a win rather
   than greyed out like the "nothing happened yet" chip beside it. */
.archive__verdict--tie[b-395elwk7vy] {
    background: var(--info-surface, var(--surface-2));
    color: var(--accent-blue);
    border: 1px solid var(--accent-blue);
}

/* A voided duel is terminal with no evidence — muted so it does not compete with real wins or ties. */
.archive__verdict--voided[b-395elwk7vy] {
    background: var(--surface-2);
    color: var(--text-muted);
    border: 1px solid var(--border-strong);
}

/* Layout only — the button's look is .po-btn's. Sized to sit inside a table cell without
   stretching the row, and it never wraps mid-label. */
.archive__judge-btn[b-395elwk7vy] {
    white-space: nowrap;
}

.archive__model[b-395elwk7vy] {
    color: var(--text);
}

/* The winner reads by weight AND by the trophy beside it, and the markup states it in text
   as well — no single cue carries it (SC 1.4.1). */
.archive__model--winner[b-395elwk7vy] {
    font-weight: var(--weight-bold);
}

.archive__matchup[b-395elwk7vy] {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs) var(--space-xs);
}

.archive__vs[b-395elwk7vy] {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.archive__model--unresolved[b-395elwk7vy] {
    color: var(--text-muted);
    font-style: italic;
}

/* No margin-top: .archive's flex gap already spaces it from the table. */
.archive__pagination[b-395elwk7vy] {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-md);
}

/* ── Command bar: search · outcome chips · count ─────────────────────────── */
.archive__bar[b-395elwk7vy] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm) var(--space-md);
}

.archive__filters[b-395elwk7vy] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
}

.archive__chip-count[b-395elwk7vy] {
    margin-inline-start: var(--space-2xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.archive__count[b-395elwk7vy] {
    margin-inline-start: auto;
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
}

/* Glyph plus word on a desktop row: ⚔ and ⬇ alone were labelled for a screen reader and a
   guess for everyone else. The word is aria-hidden because the aria-label already names the
   action; on a phone it gives way and the glyph stands alone in a 44px target. */
.archive__row-btn-label[b-395elwk7vy] {
    margin-inline-start: var(--space-xs);
}

.archive__search[b-395elwk7vy] {
    flex: 1 1 14rem;
    max-width: 24rem;
    padding: var(--space-xs) var(--space-sm);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface-1);
    color: var(--text);
    font: inherit;
}

.archive__date[b-395elwk7vy] {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* The prompt is the row's link to the duel. As bare inline text it was 17px tall, under the
   24px SC 2.5.8 asks of a target that is not inside a sentence — twenty of them a page. */
.archive__prompt[b-395elwk7vy] {
    display: inline-flex;
    align-items: center;
    min-block-size: 1.5rem;
}

/* ── Responsive ──────────────────────────────────────────────────────────────
   This file had no media query at all. The table itself degraded (.po-table--cards
   in app.css turns each cell into a labelled row below 640px), but nothing around
   it did — the row-action buttons and the timestamp column all
   kept their desktop shape inside a card that is now a third of the width. */

@media (max-width: 640px) {
    /* ── Compact card ────────────────────────────────────────────────────────
       .po-table--cards gives one labelled line per cell, plus a button row: four lines and
       ~140px a duel, so a page of twenty ran to 3.9 phone screens. Every value here is
       self-evident without its label (a prompt, a date, "A vs B", two buttons), so the
       labels go and the card is a grid: the prompt across the full width, then matchup
       over date with the two actions beside them. The prompt used to share its row with the
       date, which took ~110px of a 358px card and broke an 80-character summary over three
       lines. Still a real table underneath — same reasoning as the profile's kill list. */
    .archive__grid tr[b-395elwk7vy] {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
        gap: var(--space-2xs) var(--space-sm);
    }

    .archive__row-btn-label[b-395elwk7vy] {
        display: none;
    }

    .archive__count[b-395elwk7vy] {
        margin-inline-start: 0;
    }

    .archive__grid td[b-395elwk7vy] {
        width: auto;
        padding-block: 0;
        border-bottom: none;
        justify-content: flex-start;
        text-align: left;
    }

    .archive__grid td[data-label][b-395elwk7vy]::before {
        content: none;
    }

    /* Placed explicitly. The DOM order is date, prompt, matchup, actions (the desktop column
       order), and auto-placement followed it: the date took the wide column and the prompt
       the `auto` one, which then grew to the prompt's full width and crushed the matchup. */
    td.archive__prompt-cell[b-395elwk7vy] {
        grid-column: 1 / -1;
        grid-row: 1;
        min-width: 0;
        font-weight: var(--weight-semibold);
    }

    td.archive__matchup-cell[b-395elwk7vy] {
        grid-column: 1;
        grid-row: 2;
        min-width: 0;
    }

    td.archive__date[b-395elwk7vy] {
        grid-column: 1;
        grid-row: 3;
        color: var(--text-muted);
        font-size: var(--text-xs);
    }

    .archive__grid td.po-cell--bare[b-395elwk7vy] {
        grid-column: 2;
        grid-row: 2 / span 2;
    }

    /* --target-min is 44px here (pointer: coarse), so the pair is finger-sized. */
    .archive__row-actions[b-395elwk7vy] {
        flex-wrap: nowrap;
    }

    /* "Judge" is two syllables; letting it wrap mid-word to save 8px is the trade
       nobody wants. It keeps nowrap — the row it sits in is what wraps now. */
    .archive__judge-btn[b-395elwk7vy] {
        flex: 0 0 auto;
    }
}
/* /Pages/Arena.razor.rz.scp.css */
/* ── Page shell ─────────────────────────────────────────────────────────────
   Geometry from .po-page--wide on the element, like every other route. */
.arena[b-8jvk7wfnhb] {
    color: var(--text);
}

/* ── One column, as wide as the two panels ───────────────────────────────────
   The title, prompt, status band and actions used to span the whole 1400px page while the
   two panels sat centred at their own width, so a voided duel's banner ran 24px from the
   gutter and the failure cards under it started at 135px — two left edges, one above the
   other. The page is now exactly the panels' width, so every block shares their edges.
   --preview-scale lives here (not on .arena__viewports) so this width can be computed from
   it; the viewports and SandboxedViewport inherit it. Below 641px the panels stack and the
   page keeps its full width. */
@media (min-width: 641px) {
    .arena[b-8jvk7wfnhb] {
        max-width: calc(2 * (320px * var(--preview-scale, 1) + 2px) + var(--space-md));
    }
}

.arena__prompt-text--open[b-8jvk7wfnhb] {
    display: block;
    -webkit-line-clamp: unset;
    line-clamp: unset;
}

.arena__prompt-toggle[b-8jvk7wfnhb] {
    flex: 0 0 auto;
    padding-inline: var(--space-xs);
}

/* .arena__title carries NO styling — E2E-UI selector hook only, on PageHeader's h1. */

/* The prompt is one clamped line with the owner at its end. The owner never shrinks, so a
   long prompt ellipsises rather than pushing the owner off the line. The title attribute and
   the unclamped DOM text carry the full prompt. .arena__prompt-label is also the E2E-UI hook
   proving the duel deserialized. */
.arena__prompt-label[b-8jvk7wfnhb] {
    display: flex;
    align-items: baseline;
    gap: var(--space-xs);
    min-width: 0;
    margin: 0 0 var(--space-sm);
    color: var(--text-muted);
    font-size: var(--text-md);
}

.arena__prompt-text[b-8jvk7wfnhb] {
    flex: 1 1 auto;
    min-width: 0;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    line-clamp: 1;
    overflow: hidden;
    overflow-wrap: anywhere;
}

.arena__owner[b-8jvk7wfnhb] {
    flex: 0 0 auto;
    font-size: var(--text-sm);
}

/* The duel's total API cost, once, under both panels. */
.arena__cost[b-8jvk7wfnhb] {
    margin: 0 0 var(--space-sm);
    text-align: center;
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
}

.arena__cost strong[b-8jvk7wfnhb] {
    color: var(--accent-yellow);
}

.arena__error[b-8jvk7wfnhb] {
    margin: 0;
    color: var(--accent-red);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
    line-height: var(--leading-normal);
}

.arena__viewports[b-8jvk7wfnhb] {
    display: grid;
    /* Sized to the content, not to the page. The preview is a fixed 320px card, so a 1fr 1fr
       grid across a desktop width left each card marooned in the middle of a 672px column with
       the telemetry strip stretched twice as wide as the thing it describes. The cap keeps the
       metric grid readable without letting the panel run away from the preview. */
    grid-template-columns: repeat(2, minmax(0, 22rem));
    justify-content: center;
    /* start, not the default stretch: the panels are content-height now, and stretching them
       to match each other is what left a tall empty region under the shorter one. */
    align-items: start;
    gap: var(--space-md);
    margin-bottom: var(--space-md);
    /* The containing block for the VS slam, which is absolutely positioned and so takes no
       grid cell. */
    position: relative;
}

/* ── The versus slam ──────────────────────────────────────────────────
   Once, as a live duel opens: the mark drops in from three times its size out of a blur,
   overshoots, settles, holds and fades. The impact frame (~560 ms) is where playVersus puts
   its thump. scale, opacity and filter only — all compositor-animatable. */
.arena__vs[b-8jvk7wfnhb] {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 3;
    translate: -50% -50%;
    color: var(--accent-cyan);
    font-size: var(--text-4xl);
    font-weight: var(--weight-extrabold);
    letter-spacing: 0.04em;
    pointer-events: none;
    filter: drop-shadow(0 0 18px color-mix(in srgb, var(--accent-cyan) 60%, transparent));
    animation: arena-vs-slam-b-8jvk7wfnhb 1500ms cubic-bezier(.2, .8, .3, 1) 250ms both;
}

@keyframes arena-vs-slam-b-8jvk7wfnhb {
    from { opacity: 0; scale: 3; filter: blur(10px); }
    20%  { opacity: 1; scale: 0.9; filter: blur(0); }
    28%  { scale: 1.06; }
    36%  { scale: 1; }
    72%  { opacity: 1; scale: 1; }
    to   { opacity: 0; scale: 1.15; }
}

/* Side by side is the whole point of the page — the two outputs only mean anything read
   against each other — so the panels stay in one row down to the app's small breakpoint
   rather than the 1024 this used to collapse at. That figure was calibrated when a preview
   filled its column; at 320px a pair needs about 700px, so 1024 was stacking them on any
   window narrower than full screen, which is most of them.

   The inner scroller went with it: it existed because two full-height previews could not fit
   on a phone, and at 180px tall they now can. An `overflow-y: auto; max-height: 80vh` region
   nested inside the page scroller is a trap on touch anyway — the outer page stops moving
   when your thumb happens to land inside it. */
@media (max-width: 640px) {
    .arena__viewports[b-8jvk7wfnhb] {
        grid-template-columns: minmax(0, 22rem);
    }
}

/* ── Desktop enlargement ────────────────────────────────────────────
   At 320x180 a pair of previews used ~700px of a 1440 desktop and left the rest empty. Above
   1024 the preview is shown larger, but the document inside still lays out at 320x180 — the
   size the models designed for and the judge screenshots at — because SandboxedViewport
   applies --preview-scale as a transform (compositor-only) on a stage sized to the scaled box.
   It inherits from here; every other host gets its fallback of 1.

   The 320px below is SandboxedViewport's --preview-w: that property is declared on the child's
   own element, so it cannot be read up here. +2px is the card's border.

   1.4 rather than 1.5 at 1024: two 1.5x columns plus the gap measured ~980px against ~970px
   of content width at a 1024 window. At 1400+ 2x (640x360) fits, and at 1440x900 both
   previews sit above the fold with the sticky vote row under them. */
@media (min-width: 1024px) {
    .arena[b-8jvk7wfnhb] {
        --preview-scale: 1.4;
    }

    .arena__viewports[b-8jvk7wfnhb] {
        grid-template-columns: repeat(2, minmax(0, calc(320px * var(--preview-scale) + 2px)));
    }
}

@media (min-width: 1400px) {
    .arena[b-8jvk7wfnhb] {
        --preview-scale: 2;
    }
}

/* Height steps, after the width steps so they win. Measured with a decided duel (the tallest
   band): 2x left the page 25px taller than 1440x900 and 1.4x 49px taller than 1024x768, so
   the action row sat just under the fold. */
@media (min-width: 1400px) and (max-height: 960px) {
    .arena[b-8jvk7wfnhb] {
        --preview-scale: 1.8;
    }
}

@media (min-width: 1024px) and (max-height: 820px) {
    .arena[b-8jvk7wfnhb] {
        --preview-scale: 1.1;
    }
}

/* ── Mobile focus switcher ──────────────────────────────────────────
   Desktop never sees this: the two panels fit side by side above 640px, so a control
   that hides one of them would only be a way to make the page worse. It is display:none
   here and revealed in the 640 block below — the opposite of the usual direction, and
   deliberate, because "both panels" is the correct default at every width. */
.arena__focus[b-8jvk7wfnhb] {
    display: none;
}

.arena__viewport-slot[b-8jvk7wfnhb] {
    min-width: 0;
}

@media (max-width: 640px) {
    .arena__focus[b-8jvk7wfnhb] {
        display: flex;
        gap: var(--space-xs);
        justify-content: center;
        margin-bottom: var(--space-sm);
    }

    /* Hiding the slot, not unmounting the panel: the component keeps streaming, keeps its
       SignalR handlers and keeps its srcdoc, so switching back is instant and a browser
       model's inference is never interrupted by a layout choice. */
    .arena__viewports--focus-left .arena__viewport-slot--right[b-8jvk7wfnhb],
    .arena__viewports--focus-right .arena__viewport-slot--left[b-8jvk7wfnhb] {
        display: none;
    }
}

/* Explains pre-first-token silence for a browser model. Deliberately low-key: it reports
   that work is happening, so it must not read as an error. */
.arena__stall-note[b-8jvk7wfnhb] {
    margin: 0 0 var(--space-sm);
    padding: var(--space-sm) var(--space-smd);
    font-size: var(--text-sm);
    color: var(--text-muted);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    text-align: center;
}



/* ── Compare → Arena morph ──────────────────────────────────────────
   Shared-element names matching .home__slot--left/right on Home, so the two model slots fly
   into the two viewport panels when a duel starts (wow.js navigate()). */
.arena__viewport-slot--left[b-8jvk7wfnhb] {
    view-transition-name: po-fighter-left;
}

.arena__viewport-slot--right[b-8jvk7wfnhb] {
    view-transition-name: po-fighter-right;
}

/* ── The judge's reticle ────────────────────────────────────────────
   While the grace window runs, corner brackets frame each panel and a scan beam sweeps it.
   Transform and opacity only — and inference has finished by the time this can show. */
.arena__viewports--judging .arena__viewport-slot[b-8jvk7wfnhb] {
    position: relative;
    --reticle: var(--accent-cyan);
}

.arena__viewports--judging .arena__viewport-slot[b-8jvk7wfnhb]::before {
    content: "";
    position: absolute;
    inset: calc(-1 * var(--space-xs));
    pointer-events: none;
    background:
        linear-gradient(var(--reticle) 0 0) top left / 1.25rem 2px no-repeat,
        linear-gradient(var(--reticle) 0 0) top left / 2px 1.25rem no-repeat,
        linear-gradient(var(--reticle) 0 0) top right / 1.25rem 2px no-repeat,
        linear-gradient(var(--reticle) 0 0) top right / 2px 1.25rem no-repeat,
        linear-gradient(var(--reticle) 0 0) bottom left / 1.25rem 2px no-repeat,
        linear-gradient(var(--reticle) 0 0) bottom left / 2px 1.25rem no-repeat,
        linear-gradient(var(--reticle) 0 0) bottom right / 1.25rem 2px no-repeat,
        linear-gradient(var(--reticle) 0 0) bottom right / 2px 1.25rem no-repeat;
    animation: arena-reticle-b-8jvk7wfnhb 1s ease-in-out infinite alternate;
}

.arena__viewports--judging .arena__viewport-slot[b-8jvk7wfnhb]::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 14%;
    pointer-events: none;
    border-radius: var(--radius-sm);
    background: linear-gradient(to bottom,
        transparent,
        color-mix(in srgb, var(--reticle) 35%, transparent) 70%,
        color-mix(in srgb, var(--reticle) 85%, transparent) 96%,
        transparent);
    animation: arena-scan-b-8jvk7wfnhb 1.6s cubic-bezier(.45, .05, .55, .95) infinite;
}

/* The second panel scans half a beat behind the first, so the judge reads as comparing. */
.arena__viewports--judging .arena__viewport-slot--right[b-8jvk7wfnhb]::after {
    animation-delay: 0.8s;
}

/* The AI judge itself deciding: it reads the pair in both orders, so the beams run in
   opposite directions, in step — and the panels frost under the reticle. The frost is the
   one backdrop-filter here, so it stands down whenever the GPU lease is held. */
.arena__viewports--deliberating .arena__viewport-slot--right[b-8jvk7wfnhb]::after {
    animation-delay: 0s;
    animation-direction: reverse;
}

.arena__viewports--deliberating .arena__viewport-slot[b-8jvk7wfnhb]::before {
    border-radius: var(--radius-md);
    background-color: color-mix(in srgb, var(--glass-bg) 30%, transparent);
    backdrop-filter: blur(1.5px) saturate(1.2);
}

html[data-gpu="busy"] .arena__viewports--deliberating .arena__viewport-slot[b-8jvk7wfnhb]::before {
    backdrop-filter: none;
}

@keyframes arena-reticle-b-8jvk7wfnhb {
    from { opacity: 0.45; }
    to   { opacity: 1; }
}

@keyframes arena-scan-b-8jvk7wfnhb {
    from { transform: translateY(-100%); opacity: 0; }
    15%  { opacity: 1; }
    85%  { opacity: 1; }
    to   { transform: translateY(620%); opacity: 0; }
}

/* ── The judge's eye ────────────────────────────────────────────────
   The screenshots the vision judge is actually sent, at their true 320×180. Shown only while
   the judge is deciding; the verdict-time framing (pick badge, gold frame, dimmed loser) went
   with the strip's post-verdict appearance — the panels' badges mark the winner. */
.arena__judge-eye[b-8jvk7wfnhb] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-xs);
    margin: var(--space-md) 0;
    animation: arena-eye-in-b-8jvk7wfnhb 320ms ease-out both;
}

.arena__judge-eye-label[b-8jvk7wfnhb] {
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.arena__judge-eye-shots[b-8jvk7wfnhb] {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-sm);
}

.arena__judge-eye-shot[b-8jvk7wfnhb] {
    width: min(10rem, 42vw);
    height: auto;
    aspect-ratio: 16 / 9;
    border: 1px solid var(--accent-cyan);
    border-radius: var(--radius-sm);
    box-shadow: 0 0 18px color-mix(in srgb, var(--accent-cyan) 30%, transparent);
    background: var(--surface-2);
}

@keyframes arena-eye-in-b-8jvk7wfnhb {
    from { opacity: 0; transform: translateY(6px) scale(0.98); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .arena__viewports--judging .arena__viewport-slot[b-8jvk7wfnhb]::before,
    .arena__judge-eye[b-8jvk7wfnhb] {
        animation: none;
    }

    .arena__vs[b-8jvk7wfnhb] {
        display: none;
    }

    /* The beam IS motion; the static corner brackets still say "being judged". */
    .arena__viewports--judging .arena__viewport-slot[b-8jvk7wfnhb]::after {
        display: none;
    }
}
/* /Pages/Catalog.razor.rz.scp.css */
/* ── The browser-model list ──────────────────────────────────────────────────
   A bounded window, the same shape as .home__picker: a list you scan within to pick one row,
   so a second scroll area is the ordinary case. The page itself scrolls normally, because it
   has two independent lists and the Ollama one must stay reachable.

   The height must be DEFINITE, not a max: <Virtualize> measures against this box, and with
   only a max-height it sees no bound and renders all ~150 builds. It is the one list on
   screen now (the others are behind the switch), so it takes what is left of the viewport
   under the header, bar and hint (~16rem with the nav) rather than stopping at 34rem with
   empty page beneath it; the floor keeps a short window usable. */
.catalog__grid-wrap[b-ce20bb95we] {
    height: max(16rem, calc(100dvh - var(--nav-height) - 16rem));
    overflow-y: auto;
}

/* Fixed column widths above the card breakpoint. Auto layout sized the columns from whatever
   rows <Virtualize> had rendered so far, so the header and every row re-flowed as its second
   pass arrived — CLS 0.094 at 768px, the largest left in the app. */
@media (min-width: 641px) {
    .catalog__grid-wrap > table[b-ce20bb95we] {
        table-layout: fixed;
    }

    /* The id column is the widest on purpose: a build id that wraps makes its row taller than
       the 44px <Virtualize> assumes, and the rows below it then shift as it corrects. */
    .catalog__grid-wrap th:nth-child(1)[b-ce20bb95we] { width: 46%; }
    .catalog__grid-wrap th:nth-child(2)[b-ce20bb95we] { width: 8%; }
    .catalog__grid-wrap th:nth-child(3)[b-ce20bb95we] { width: 10%; }
    .catalog__grid-wrap th:nth-child(4)[b-ce20bb95we] { width: 20%; }
    .catalog__grid-wrap th:nth-child(5)[b-ce20bb95we] { width: 16%; }
}

/* Loading: the bar's row plus the list window, so what replaces it lands in place. ::deep
   because the list and its rows are <SkeletonGrid>'s markup. */
.catalog__loading[b-ce20bb95we]  .catalog__skeleton-list {
    display: grid;
    align-content: start;
    gap: var(--space-xs);
    height: max(16rem, calc(100dvh - var(--nav-height) - 16rem));
    margin-top: var(--space-lg);
    overflow: hidden;
}

.catalog__loading[b-ce20bb95we]  .catalog__skeleton {
    height: 2.75rem;
    border-radius: var(--radius-sm);
    animation-delay: var(--stagger, 0ms);
}

@media (max-width: 640px) {
    /* In card rows the actions read left to right under the values, like every other card. */
    .catalog__row-actions[b-ce20bb95we] {
        justify-content: flex-start;
    }
}

.catalog__input.catalog__search[b-ce20bb95we] {
    inline-size: auto;
    min-inline-size: 14rem;
}

/* The page's one toolbar: the list switch, then the active list's own filters. */
.catalog__bar[b-ce20bb95we] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm) var(--space-md);
    margin-block: var(--space-sm) var(--space-md);
}

.catalog__tabs[b-ce20bb95we] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
}

/* The bar above already spaces the list; .po-section's own top margin doubled it. */
.catalog__section[b-ce20bb95we] {
    margin-top: 0;
}

/* Add and Rename, side by side at the row's end. */
.catalog__row-actions[b-ce20bb95we] {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-xs);
}

.catalog__name[b-ce20bb95we] {
    font-size: var(--text-sm);
}

/* Sits opposite the Low-VRAM chip and reports what the chip is hiding. */
.catalog__count[b-ce20bb95we] {
    margin: 0;
    margin-inline-start: auto;
}

.catalog__input[b-ce20bb95we] {
    inline-size: 100%;
    padding: var(--space-xs) var(--space-sm);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-1);
    color: var(--text);
    font: inherit;
    font-size: var(--text-sm);
}


/* Build ids are long and unbroken ("Llama-3.2-1B-Instruct-q4f32_1-MLC"); let them wrap on
   the hyphen-less stretches rather than widen the table past the viewport. */
.catalog__id[b-ce20bb95we] {
    overflow-wrap: anywhere;
    font-family: 'JetBrains Mono', monospace;
    font-size: var(--text-sm);
}

.catalog__hub[b-ce20bb95we] {
    /* inline-flex + a floor: as bare 12px text this link was 15px tall, under the 24px
       SC 2.5.8 asks of a target outside a sentence, once per row. */
    display: inline-flex;
    align-items: center;
    min-block-size: 1.5rem;
    margin-inline-start: var(--space-sm);
    font-size: var(--text-xs);
}

.catalog__num[b-ce20bb95we] {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* A build this device's GPU will refuse. Text plus the warn tint pair, never colour alone. */
.catalog__unfit[b-ce20bb95we] {
    display: inline-block;
    margin-inline-start: var(--space-sm);
    padding: 0 var(--space-xs);
    border: 1px solid var(--warn-border);
    border-radius: var(--radius-sm);
    background: var(--warn-surface);
    color: var(--accent-yellow);
    font-size: var(--text-2xs);
    white-space: nowrap;
}
/* /Pages/Home.razor.rz.scp.css */
/* ── Page shell ─────────────────────────────────────────────────────────
   .po-page--wide carries the width, the gutter and the vertical rhythm, exactly as it does
   on /catalog, /tournament and /model. This used to be a shell of its own at max-width 1360
   with its own padding, which is how four routes came to have three different content
   widths (1200 / 1360 / 1400 on a desktop) and three different phone gutters. The class is
   kept because the E2E-UI suite and the view-transition hooks point at it, but it no longer
   competes with the primitive for geometry. */
.home[b-qweh3dst78] {
    color: var(--text);
}

/* ── Workbench layout ───────────────────────────────────────────────────
   Picker left, console right. `align-items: start` is load-bearing — a grid item stretches
   to the row height by default, and a stretched item cannot stick. */
.home__layout[b-qweh3dst78] {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 26rem;
    grid-template-rows: auto minmax(0, 1fr);
    column-gap: var(--space-mlg);
    align-items: start;
}

/* The page's <PageHeader /> lives in the left grid item. The console spans both rows, so
   its sticky box starts level with the title rather than below it — without that the foot,
   and therefore Compare, opened below the fold and only came into view after a scroll. */
.home__header[b-qweh3dst78] {
    grid-column: 1;
    grid-row: 1;
}

.home__picker[b-qweh3dst78] {
    grid-column: 1;
    grid-row: 2;
}

.home__console[b-qweh3dst78] {
    grid-column: 2;
    grid-row: 1 / span 2;
}

.home__picker[b-qweh3dst78] {
    min-width: 0;
    padding: var(--space-mlg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface-1);
}

/* The nav bar is sticky, so the console has to clear it: `top` is --nav-height plus the
   --space-md the article contributes above the page, and the height budget subtracts the
   same again at the foot. Both read the token rather than restating 3.5rem, and the 6.5rem
   magic number that used to be here is gone with them.

   max-height, NOT height — and that is the fix, not a tidy-up. As a fixed height this box
   was 796px on a 1440x900 desktop whatever was in it, and what is in it before you open the
   prompt library is two slot chips, a textarea and a disclosure button: measured, ~278px of
   empty glass sat between the library button and the Compare footer, about 35% of the panel,
   and it was the most conspicuous piece of dead space in the app. A maximum keeps every
   property the fixed height was there for — the foot stays pinned, only the middle scrolls,
   and the panel can never grow past the fold — while letting it shrink to its content when
   there is less of it. */
.home__console[b-qweh3dst78] {
    position: sticky;
    top: calc(var(--nav-height) + var(--space-md));
    /* Home scrolls this into view when the second model is picked; clear the sticky nav. */
    scroll-margin-block-start: var(--sticky-nav-clearance);
    /* dvh — this is the sticky console's height, so with vh the Compare button it exists
       to keep on screen was pushed below the fold by exactly the mobile toolbar height. */
    max-height: calc(100dvh - var(--nav-height) - var(--space-xl));
    /* border-box, because there is no global reset: without it the 1.25rem padding and the
       border are added to the height and the foot hangs 42px below the fold. */
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    min-width: 0;
    padding: var(--space-mlg);
    /* Border, background and blur come from .po-glass on the element: the console is the one
       large surface that sits over the shader backdrop, so it is where glass reads as glass. */
    border-radius: var(--radius-lg);
}

.home__console-scroll[b-qweh3dst78] {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    /* Keeps the scrollbar off the textarea's focus ring. */
    padding-right: var(--space-xs);
}

.home__console-foot[b-qweh3dst78] {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-smd);
}

/* Single column below 1080px, in the original reading order: pick models, then write the
   prompt and press Compare. The console stops sticking and stops scrolling internally —
   a nested scroll area inside a scrolling page is a trap on touch. */
/* The one breakpoint in the app that is not on the 640/1024/1400 scale, deliberately: the
   picker-plus-console grid is genuinely tight below about 1080px, and CLAUDE.md records this
   collapse as a load-bearing layout decision rather than an incidental number. */
@media (max-width: 1080px) {
    .home__layout[b-qweh3dst78] {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: none;
        row-gap: var(--space-mlg);
    }

    .home__header[b-qweh3dst78],
    .home__picker[b-qweh3dst78],
    .home__console[b-qweh3dst78] {
        grid-column: 1;
        grid-row: auto;
    }

    .home__console[b-qweh3dst78] {
        position: static;
        max-height: none;
    }

    .home__console-scroll[b-qweh3dst78] {
        overflow-y: visible;
        padding-right: 0;
    }

    /* ── Compare is a bar pinned to the bottom of the viewport ─────────────────
       In one column the console sits under the whole picker, so Compare — the action the page
       exists for — measured at y≈1050 on a 390×844 phone, below the fold before a single
       scroll. Making the foot sticky could not fix that: a sticky box never leaves its own
       containing block, and the console's top is itself below the fold. Fixed can.

       The console drops its backdrop-filter here because a filtered ancestor becomes the
       containing block for position: fixed — the bar would pin to the console, not the
       viewport. Below 1080px the console is not over the aurora in a way the blur adds to. */
    /* !important because app.css's liquid-glass rule, `html.po-liquid:not([data-gpu="busy"])
       .po-glass`, is specificity (0,3,1) against this scoped rule's (0,2,0) — measured, without
       it the bar computed position: fixed and still sat at y=909 on an 844px screen. */
    .home__console[b-qweh3dst78] {
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
    }

    .home__console-foot[b-qweh3dst78] {
        position: fixed;
        inset-inline: 0;
        bottom: 0;
        z-index: 90; /* under the nav (100) and its menu (110) */
        padding:
            var(--space-sm)
            max(var(--space-md), env(safe-area-inset-right))
            max(var(--space-sm), env(safe-area-inset-bottom))
            max(var(--space-md), env(safe-area-inset-left));
        background: var(--surface-1);
        border-top: 1px solid var(--border);
        box-shadow: var(--glass-shadow);
    }

    .home__compare[b-qweh3dst78] {
        border: 0;
        background: transparent;
    }

    .home__compare-row[b-qweh3dst78] {
        flex-wrap: nowrap;
        gap: var(--space-smd);
        padding: 0;
    }

    .home__compare-copy[b-qweh3dst78] {
        font-size: var(--text-sm);
    }

    .home__error[b-qweh3dst78] {
        padding: var(--space-xs) 0 0;
    }

    /* Room under the last card for the bar, so nothing ends up permanently beneath it. */
    .home[b-qweh3dst78] {
        padding-bottom: calc(var(--space-2xl) * 2);
    }

    /* No bounded picker here any more. It was a 52dvh scroll box inside the scrolling page —
       the thumb trap this block's own header warns about — kept because a catalog of ~115px
       cards put the prompt a page and a half down. With 44px rows the list is a third of that,
       Compare is the fixed bar below, and picking the second model scrolls the prompt into
       view, so the page scrolls once and nothing scrolls inside it. */
}

/* ── Desktop: the whole workbench fits the viewport ─────────────────────────
   Two columns, and until now only one of them was bounded: the console stuck, but the picker
   ran the page to 1.37 screens on a 1440x900 desktop for a 27-model catalog. The picker is a
   list you scan within — the same call already made for it below 1080px — so here the grid
   takes exactly the height left under the nav (--space-md is the article's top padding,
   --space-lg the page's bottom one) and the picker scrolls inside its row.

   max-height, not height: a filtered list of two Ollama models should be a short box, not a
   tall empty one. The floor keeps a very short window usable; below it the page scrolls. */
@media (min-width: 1081px) {
    .home__layout[b-qweh3dst78] {
        height: calc(100dvh - var(--nav-height) - var(--space-md) - var(--space-lg));
        min-height: 26rem;
    }

    .home__picker[b-qweh3dst78] {
        max-height: 100%;
        overflow-y: auto;
        overscroll-behavior: contain;
    }
}

/* ── Header ───────────────────────────────────────────────────────────
   .home__title carries NO styling — it is an E2E-UI selector hook and nothing else.
   PageHeader provides the h1 geometry. */

/* ── Health pill ────────────────────────────────────────────────────── */
.home__health[b-qweh3dst78] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding: var(--space-xs) var(--space-smd);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    border-radius: var(--radius-full);
    border: 1px solid var(--border);
    background: var(--surface-1);
    white-space: nowrap;
}

.home__health--ok[b-qweh3dst78] {
    color: var(--accent-green);
    border-color: var(--success-border);
    background: var(--success-surface);
}

.home__health--pending[b-qweh3dst78] {
    visibility: hidden;
}

.home__health--warn[b-qweh3dst78] {
    color: var(--accent-yellow);
    border-color: var(--warn-border);
    background: var(--warn-surface);
}

/* ── Section headings (shared by both columns) ────────────────────────
   A row so the picker's "Load starter duel" and "Add model" sit beside its heading. */
.home__picker-actions[b-qweh3dst78] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
}

.home__section-head[b-qweh3dst78] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
}

/* ── Unavailable models (held back behind a toggle) ─────────────────────── */
.home__unavailable-toggle[b-qweh3dst78] {
    margin-top: var(--space-md);
}

.home__unavailable-why[b-qweh3dst78] {
    margin-top: var(--space-sm);
}

.home__hint[b-qweh3dst78] {
    margin: 0;
    font-size: var(--text-md);
    color: var(--text-muted);
}

/* ── Slots ──────────────────────────────────────────────────────────── */
.home__slots[b-qweh3dst78] {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: stretch;
    gap: var(--space-smd);
}

/* Shared-element names for the Compare → Arena view transition (wow.js): each slot morphs
   into the Arena viewport panel for the same side, which carries the same name. */
.home__slot--left[b-qweh3dst78] {
    view-transition-name: po-fighter-left;
}

.home__slot--right[b-qweh3dst78] {
    view-transition-name: po-fighter-right;
}

/* One line: "L · name". */
.home__slot[b-qweh3dst78] {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-width: 0;
    padding: var(--space-sm) var(--space-smd);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    transition: border-color var(--transition-fast), background var(--transition-fast);
}

.home__slot--set[b-qweh3dst78] {
    border-color: var(--success-border);
    background: var(--success-surface);
}

.home__slot-tag[b-qweh3dst78] {
    flex: 0 0 auto;
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    padding: var(--space-2xs) var(--space-sm);
    border-radius: var(--radius-full);
}

.home__slot-tag--left[b-qweh3dst78] {
    background: var(--cyan-surface);
    border: 1px solid var(--cyan-border);
    color: var(--accent-cyan);
}

.home__slot-tag--right[b-qweh3dst78] {
    background: var(--warn-surface);
    border: 1px solid var(--warn-border);
    color: var(--accent-yellow);
}

/* Two lines, never an ellipsis, at every width. It was nowrap + ellipsis above 640px, which
   was fine until each filled slot gained a clear button: in the 26rem console that left
   ~70px for the name and "DeepSeek V4 Flash" became "DeepSee…" on a 1440px desktop. */
.home__slot-name[b-qweh3dst78] {
    min-width: 0;
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    color: var(--text-muted);
    overflow: hidden;
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: var(--leading-tight);
}

.home__slot--set .home__slot-name[b-qweh3dst78] {
    color: var(--text);
}

/* The slot's own clear button, pushed to its far edge. Size comes from the global target
   floor on <button> (24px, 44px on touch). */
.home__slot-clear[b-qweh3dst78] {
    flex: 0 0 auto;
    margin-inline-start: auto;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-size: var(--text-sm);
    line-height: 1;
    cursor: pointer;
}

.home__slot-clear:hover[b-qweh3dst78] {
    color: var(--text);
    background: var(--surface-2);
}

.home__slot-vs[b-qweh3dst78] {
    display: flex;
    align-items: center;
    font-size: var(--text-xs);
    font-weight: var(--weight-extrabold);
    color: var(--text-dim);
    letter-spacing: 0.15em;
    padding: 0 var(--space-xs);
}

.home__filters[b-qweh3dst78] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-bottom: var(--space-smd);
}

.home__group-header[b-qweh3dst78] {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-2xs);
    margin: var(--space-smd) 0 var(--space-sm);
    border-bottom: 1px solid var(--border);
}

.home__group-name[b-qweh3dst78] {
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    letter-spacing: 0.05em;
    color: var(--text);
}

/* ::deep because two of the elements carrying these classes are rendered by <SkeletonGrid>,
   a child component that Home's scope id never reaches. Without it the loading state was one
   full-width 0-height bar instead of six card-shaped tiles. */
.home__picker[b-qweh3dst78]  .home__grid {
    display: grid;
    /* 20rem keeps "name · rating · size · price" on one line in a row; two columns on a
       desktop picker and on a tablet, one on a phone. At 17rem a 1440px picker took three
       columns and the figures wrapped under every long name. */
    grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 1fr));
    gap: var(--space-xs) var(--space-sm);
}

.home__card-disabled[b-qweh3dst78] {
    position: relative;
    opacity: 0.55;
    pointer-events: none;
    cursor: not-allowed;
}

.home__card-badge[b-qweh3dst78] {
    position: absolute;
    top: var(--space-sm);
    right: var(--space-sm);
    background: var(--accent-red);
    color: var(--on-accent);
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    padding: var(--space-2xs) var(--space-sm);
    border-radius: var(--radius-sm);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    z-index: 1;
}

/* ── Slots-full hint ────────────────────────────────────────────────────
   Empty until a third model is clicked with both slots taken. Always in the DOM (it is a live
   region), so it must cost nothing while it has nothing to say. */
.home__full-hint[b-qweh3dst78] {
    margin: 0 0 var(--space-sm);
    color: var(--accent-yellow);
}

.home__full-hint:empty[b-qweh3dst78] {
    margin: 0;
}

/* ── Prompt (console, upper half) ───────────────────────────────────── */
.home__prompt[b-qweh3dst78] {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    min-width: 0;
}

.home__prompt .home__section-head[b-qweh3dst78] {
    margin-bottom: 0;
}

/* ── Textarea + char counter (Finding #6) ───────────────────────────── */
.home__textarea[b-qweh3dst78] {
    width: 100%;
    box-sizing: border-box;
    min-height: 8.5rem;
    padding: var(--space-smd) var(--space-md);
    font-family: inherit;
    font-size: var(--text-lg);
    line-height: 1.5;
    color: var(--text);
    background: var(--surface-1);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    resize: vertical;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

/* :focus-visible with the ring INTACT. This was `:focus { outline: none }` replacing the
   app's ring with `0 0 0 3px rgba(18,184,207,0.18)` — an 18%-alpha shadow that cannot reach
   the 3:1 SC 1.4.11 asks of a focus indicator, on the primary input of the page the app
   exists for. Two separate faults: the outline was removed for keyboard users as well as
   pointer ones (`:focus`, not `:focus-visible`), and .home__textarea:focus outranks the
   global `:focus-visible` rule on specificity, so nothing restored it.

   The cyan border is kept — it is the surface's own affordance and reads on hover-into-focus
   — but it is now additive to the ring rather than a replacement for it. */
.home__textarea:focus-visible[b-qweh3dst78] {
    outline: 2px solid var(--accent-blue);
    outline-offset: var(--focus-ring-offset);
    box-shadow: var(--focus-ring);
    border-color: var(--accent-cyan);
}

/* Pointer focus keeps the quieter treatment, which is what the removed rule was really for. */
.home__textarea:focus:not(:focus-visible)[b-qweh3dst78] {
    border-color: var(--accent-cyan);
}

.home__textarea--warn[b-qweh3dst78] {
    border-color: var(--accent-yellow);
}

.home__textarea[b-qweh3dst78]::placeholder {
    color: var(--text-dim);
}

.home__textarea-meta[b-qweh3dst78] {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.home__textarea-counter[b-qweh3dst78] {
    font-variant-numeric: tabular-nums;
}

.home__textarea-counter--warn[b-qweh3dst78] {
    color: var(--accent-yellow);
    font-weight: var(--weight-semibold);
}

.home__textarea-hint[b-qweh3dst78] {
    color: var(--accent-yellow);
}

/* ── Compare CTA (console, foot — never scrolls away) ────────────────── */
.home__compare[b-qweh3dst78] {
    border: 1px solid var(--success-border);
    border-radius: var(--radius-lg);
    background: var(--success-surface);
}

/* One row: the readiness copy, then the button pinned right. */
.home__compare-row[b-qweh3dst78] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-mlg);
}

.home__compare-copy[b-qweh3dst78] {
    flex: 1 1 10rem;
    min-width: 0;
    margin: 0;
}

.home__error[b-qweh3dst78] {
    display: inline-block;
    padding: 0 var(--space-mlg) var(--space-smd);
    color: var(--accent-red);
    font-size: var(--text-md);
}

/* Loading placeholders in the model grid. Only the stagger is dynamic — the markup passes
   it as --stagger; everything else lives here rather than in an inline style. */
.home__picker[b-qweh3dst78]  .home__skeleton {
    height: 2.75rem;
    border-radius: var(--radius-md);
    animation-delay: var(--stagger, 0ms);
}

/* ── Phone ────────────────────────────────────────────────────────────────────
   The picker flows with the page (see the 1080px block), so every row here is about height:
   the filters and the two slots each take one row instead of two. */
@media (max-width: 640px) {
    .home__picker[b-qweh3dst78] {
        padding: var(--space-md) var(--space-sm);
    }

    /* Side by side again, without the "vs". Stacked cost a row for nothing the L/R tags do
       not already say; the 2-line name clamp keeps "DeepSeek V4 Flash" whole in ~150px. */
    .home__slots[b-qweh3dst78] {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--space-xs);
    }

    .home__slot[b-qweh3dst78] {
        padding: var(--space-xs) var(--space-sm);
        gap: var(--space-xs);
    }

    .home__slot-vs[b-qweh3dst78] {
        display: none;
    }

    /* One row of four. The old single row was a scroller that clipped "Ollam"; four equal
       columns cannot clip, and the labels are short enough to fit 360px whole. */
    .home__filters[b-qweh3dst78] {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, auto));
        gap: var(--space-2xs);
    }

    .home__filters > .po-btn[b-qweh3dst78] {
        min-width: 0;
        padding-inline: var(--space-xs);
        white-space: nowrap;
        font-size: var(--text-xs);
    }

    .home__console-foot[b-qweh3dst78] {
        padding:
            var(--space-xs)
            max(var(--space-sm), env(safe-area-inset-right))
            max(var(--space-xs), env(safe-area-inset-bottom))
            max(var(--space-sm), env(safe-area-inset-left));
    }

    .home__compare-row[b-qweh3dst78] {
        gap: var(--space-xs);
    }

}

/* ── Compare: readiness edge ─────────────────────────────────────────────────
   The same registered-colour hairline as the Arena's status band (--po-edge-a/-b, @property
   in app.css): it warms from the border colour to green/cyan as the Compare button becomes
   pressable, so "ready" is visible on the panel and not only on the button's disabled state.
   One transition per change of readiness. */
.home__compare[b-qweh3dst78] {
    position: relative;
}

.home__compare[b-qweh3dst78]::before {
    content: "";
    position: absolute;
    inset-inline: var(--space-md);
    top: -1px;
    height: 2px;
    border-radius: var(--radius-full);
    --po-edge-a: var(--success-border);
    --po-edge-b: var(--success-border);
    background: linear-gradient(90deg, transparent, var(--po-edge-a), var(--po-edge-b), var(--po-edge-a), transparent);
    transition: --po-edge-a 600ms ease, --po-edge-b 600ms ease;
    pointer-events: none;
}

.home__compare:has(.po-btn:not(:disabled))[b-qweh3dst78]::before {
    --po-edge-a: var(--accent-green);
    --po-edge-b: var(--accent-cyan);
}

@media (prefers-reduced-motion: reduce) {
    .home__compare[b-qweh3dst78]::before {
        transition: none;
    }
}

/* ── Sharpen prompt ──────────────────────────────────────────────────────────
   The button, its Undo, and one line saying what happened, under the textarea. */
.home__sharpen[b-qweh3dst78] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs) var(--space-sm);
}

.home__sharpen-note[b-qweh3dst78] {
    flex: 1 1 8rem;
    min-width: 0;
    margin: 0;
}
/* /Pages/Leaderboard.razor.rz.scp.css */
/* ── Page shell ─────────────────────────────────────────────────────────────
   Geometry comes from .po-page on the element. This was its own shell, and one of the four
   that between them gave the app three desktop content widths and three phone gutters. */
.leaderboard[b-ixkkb6bq4p] {
    color: var(--text);
}

/* .leaderboard__title carries NO styling — E2E-UI selector hook only, with .po-title on the
   same element supplying the size. It was --text-3xl at weight 800, which made it the
   largest h1 in the app (measured 43px against 31px on /archive and /tournament at the same
   1440px width) and, worse, requested a weight the app does not load: the @import in app.css
   tops out at 700 for Space Grotesk, so 800 was a browser-synthesised faux bold. */


/* Layout-only, per the .po-btn convention: the disclosure button has to read as the table
   cell it replaced, not as a button sitting inside one. */
.leaderboard__row-btn[b-ixkkb6bq4p] {
    inline-size: 100%;
    justify-content: flex-start;
    gap: var(--space-sm);
    padding-inline: 0;
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    text-align: start;
}

/* Rank medals — size and glow. */
.leaderboard__rank[b-ixkkb6bq4p] { font-size: var(--text-xl); }
/* Glow colours are mixed from the theme's accents (the same color-mix(…, transparent) form
   app.css uses for its own glows) rather than frozen rgba literals. Bronze has no accent of
   its own, so it is yellow pulled toward red. */
.leaderboard__rank--gold[b-ixkkb6bq4p]   { filter: drop-shadow(0 0 var(--space-xs) color-mix(in srgb, var(--accent-yellow) 70%, transparent)); }
.leaderboard__rank--silver[b-ixkkb6bq4p] { filter: drop-shadow(0 0 var(--space-xs) color-mix(in srgb, var(--text-dim) 60%, transparent)); }
.leaderboard__rank--bronze[b-ixkkb6bq4p] { filter: drop-shadow(0 0 var(--space-xs) color-mix(in srgb, color-mix(in srgb, var(--accent-yellow) 60%, var(--accent-red)) 50%, transparent)); }

/* Spacing-only hook for the ModelTypeBadge that follows a model name in the row. ::deep for
   the same reason as the medals: the badge's element belongs to ModelTypeBadge's scope, so
   as a bare selector this rule never applied. */
.leaderboard__grid[b-ixkkb6bq4p]  .leaderboard__type-badge-spacing {
    margin-left: var(--space-sm);
}

.leaderboard__grid[b-ixkkb6bq4p] {
    margin-bottom: var(--space-md);
}

/* `leaderboard__grid--scrollable` used to sit here — an `overflow-x: auto` wrapper that
   duplicated what .po-table-wrap already provides and contradicted what .po-table--cards
   switches off at 640px. Removed: horizontal behaviour is owned in exactly one place.
   See the table-strategy note in app.css. */

/* At card size (≤640px) the three derived columns — avg $/duel, first-token ms and the
   Value score — are dropped from each row rather than stacked, via .po-cell--secondary.
   Why: a phone row was eight labelled lines per model, and with eleven models the board
   ran 2602px tall. You could see the rank and ELO, or the cost, but never both — so
   comparing two models always meant a scroll. All three are *derived* numbers (cost and
   speed are the inputs to Value; Value is ELO per dollar), so a row still answers "who is
   winning, and by how much" without them, and the full figures remain on the model's own
   profile one tap away. Desktop keeps every column; nothing is lost, only deferred. */

/* Placeholder shown in a grid cell that has no value yet. */
.leaderboard__no-data[b-ixkkb6bq4p] {
    color: var(--text-muted);
}

/* Value = ELO earned per dollar of API spend. The figure can be in the millions, so we
   render it as a thousands-separated integer ("1,250,000") instead of a decimal — easier
   to read and compare across rows. Tabular numerics keep the digits aligned. Right-
   aligned so the eye can scan the column for the leader without re-reading each number. */
.leaderboard__value[b-ixkkb6bq4p] {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: var(--weight-semibold);
}


.leaderboard__ttft[b-ixkkb6bq4p] {
    font-variant-numeric: tabular-nums;
}

/* ELO plus its uncertainty. The ± is secondary information, so it is smaller and muted —
   the rating stays the thing the eye lands on. Wraps under the number in a narrow cell. */
.leaderboard__elo[b-ixkkb6bq4p] {
    white-space: nowrap;
}

.leaderboard__ci[b-ixkkb6bq4p] {
    margin-inline-start: var(--space-2xs);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
}

.leaderboard__provisional[b-ixkkb6bq4p] {
    display: inline-block;
    margin-inline-start: var(--space-xs);
    padding: 0 var(--space-xs);
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.04em;
    vertical-align: middle;
}

/* Records read as figures to compare down the column, so they get tabular figures and sit
   right-aligned against each other rather than ragged. */
.leaderboard__record[b-ixkkb6bq4p] {
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* ── Responsive ────────────────────────────────────────────────────────────── */

@media (max-width: 640px) {
    /* The medal glyph is the row's anchor in card mode — keep it at heading size while
       everything around it shrinks. */
    .leaderboard__rank[b-ixkkb6bq4p] {
        font-size: var(--text-2xl);
    }

    /* ── Two-line card ────────────────────────────────────────────────
       .po-table--cards gives every cell its own labelled line, which here was the medal on
       one line and Model / ELO / Record under it — ~110px per model, 3 screens for the board
       on a 390px phone. Each field is self-evident in this layout (a name, a rating with its
       ±, a W–L), so the row becomes a grid instead:

           🥇 │ GPT-5.4 Nano [REMOTE]
              │ 1262 ±432 provisional · 4–0

       Still a real <table> with its <th scope> headers — only the boxes move. The medal
       spans both lines; the rank-shift badge stacks under it. Secondary cells stay hidden
       by .po-cell--secondary. */
    .leaderboard__grid tr[b-ixkkb6bq4p] {
        display: grid;
        grid-template-columns: auto auto minmax(0, 1fr);
        align-items: center;
        column-gap: var(--space-sm);
    }

    .leaderboard__grid td[b-ixkkb6bq4p] {
        padding-block: 0;
        border-bottom: none;
        gap: 0;
        justify-content: flex-start;
        text-align: left;
    }

    /* The labels restate what the layout already says. */
    .leaderboard__grid td[data-label][b-ixkkb6bq4p]::before {
        content: none;
    }

    /* td.-qualified so they outrank the `.leaderboard__grid td` reset above. */
    td.leaderboard__rank-cell[b-ixkkb6bq4p] {
        grid-column: 1;
        grid-row: 1 / span 2;
        flex-direction: column;
        align-items: center;
        gap: 0;
    }

    td.leaderboard__name-cell[b-ixkkb6bq4p] {
        grid-column: 2 / -1;
        grid-row: 1;
        min-width: 0;
    }

    td.leaderboard__elo[b-ixkkb6bq4p] {
        grid-column: 2;
        grid-row: 2;
    }

    td.leaderboard__record-cell[b-ixkkb6bq4p] {
        grid-column: 3;
        grid-row: 2;
        gap: var(--space-xs);
        color: var(--text-muted);
    }

    /* Separator between rating and record. Alt text "" (the `/ ""` form) keeps it out of the
       accessible name where supported; the first declaration is the fallback for engines
       that reject that syntax and drop the second. */
    .leaderboard__grid td.leaderboard__record-cell[data-label][b-ixkkb6bq4p]::before {
        content: "·";
        content: "·" / "";
    }
}


/* ── Since you last looked ──────────────────────────────────────────
   Rank movement against this viewer's previous visit (wow.js keeps it). The rows themselves
   FLIP into place from their old positions; these badges are the part that stays. */
.leaderboard__shift[b-ixkkb6bq4p] {
    display: inline-flex;
    align-items: center;
    margin-inline-start: var(--space-2xs);
    padding: 0 var(--space-2xs);
    border-radius: var(--radius-sm);
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    font-variant-numeric: tabular-nums;
    animation: leaderboard-shift-in-b-ixkkb6bq4p 900ms cubic-bezier(.2, .9, .25, 1.2) both;
}

/* The opaque tint tokens, not a translucent mix — see the note beside them in app.css. A
   `%, transparent` mix composites against whatever is behind the badge, so the same badge
   measured differently in a table row than in a card row. */
.leaderboard__shift--up[b-ixkkb6bq4p] {
    color: var(--accent-green);
    background: var(--success-surface);
}

.leaderboard__shift--down[b-ixkkb6bq4p] {
    color: var(--accent-red);
    background: var(--danger-surface);
}

/* The ± band got tighter: more evidence behind the number. A one-shot pulse, not a loop. */
.leaderboard__ci--narrowed[b-ixkkb6bq4p] {
    display: inline-block;
    color: var(--accent-cyan);
    animation: leaderboard-ci-narrow-b-ixkkb6bq4p 1.1s ease-out both;
}

@keyframes leaderboard-shift-in-b-ixkkb6bq4p {
    from { opacity: 0; transform: scale(0.4); }
    to   { opacity: 1; transform: none; }
}

@keyframes leaderboard-ci-narrow-b-ixkkb6bq4p {
    0%   { transform: scaleX(1.6); opacity: 0.4; }
    60%  { transform: scaleX(0.92); opacity: 1; }
    100% { transform: none; }
}

/* ── Scroll-driven reveal ───────────────────────────────────────────
   Rows fade up as they enter the viewport, on the scroll timeline — no JS, no observer.
   Uses the individual `translate` property so it composes with the FLIP's `transform`
   instead of fighting it. Where animation-timeline is unsupported, rows simply appear. */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        .leaderboard__grid tbody tr[b-ixkkb6bq4p] {
            animation: leaderboard-reveal-b-ixkkb6bq4p linear both;
            animation-timeline: view();
            animation-range: entry 0% entry 70%;
        }
    }
}

@keyframes leaderboard-reveal-b-ixkkb6bq4p {
    from { opacity: 0; translate: 0 14px; }
    to   { opacity: 1; translate: none; }
}

@media (prefers-reduced-motion: reduce) {
    .leaderboard__shift[b-ixkkb6bq4p],
    .leaderboard__ci--narrowed[b-ixkkb6bq4p] {
        animation: none;
    }
}

/* ── Table beside the chart ──────────────────────────────────────────────────
   Stacked, the value chart used 720 of 1,392px and put the first ranked row at y=569 on a
   1440x900 desktop: the page is called Leaderboard and the board started below the fold.
   From 1400px the table (798px at its natural width) takes the left column and the chart a
   sticky right one. Both are placed explicitly so the DOM order — chart, then table — is
   still the stacked order below this width. */
.leaderboard__main[b-ixkkb6bq4p] {
    min-width: 0;
}

@media (min-width: 1400px) {
    .leaderboard__layout[b-ixkkb6bq4p] {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 34rem);
        align-items: start;
        gap: var(--space-lg);
    }

    .leaderboard__main[b-ixkkb6bq4p] {
        grid-column: 1;
        grid-row: 1;
    }

    .leaderboard__chart[b-ixkkb6bq4p] {
        grid-column: 2;
        grid-row: 1;
        position: sticky;
        top: calc(var(--nav-height) + var(--space-md));
    }
}

/* ── Profile beside the board ────────────────────────────────────────────────
   From 1024px a picked model's profile takes the right column (the chart's, from 1400px).
   It sticks under the nav and scrolls inside its own height, so the table stays where you
   were reading it — the profile is the detail of one row, not a page you travel to. Between
   1024 and 1400 the three derived columns give way to make room: cost, first token and value
   are all restated in the panel's own stat tiles. */
@media (min-width: 1024px) {
    .leaderboard__layout--panel[b-ixkkb6bq4p] {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(24rem, 34rem);
        align-items: start;
        gap: var(--space-lg);
    }

    .leaderboard__layout--panel .leaderboard__main[b-ixkkb6bq4p] {
        grid-column: 1;
        grid-row: 1;
    }

    .leaderboard__panel[b-ixkkb6bq4p] {
        grid-column: 2;
        grid-row: 1;
        position: sticky;
        top: calc(var(--nav-height) + var(--space-md));
        max-height: calc(100dvh - var(--nav-height) - var(--space-xl));
        overflow-y: auto;
        overscroll-behavior: contain;
        padding: var(--space-md);
        border-radius: var(--radius-lg);
    }
}

/* The type badge and the provisional tag go too: at 1024 they left the name column ~70px and
   "GPT-OSS 120B" broke over three lines. The panel's header states both. */
@media (min-width: 1024px) and (max-width: 1399.98px) {
    .leaderboard__layout--panel .leaderboard__derived[b-ixkkb6bq4p],
    .leaderboard__layout--panel .leaderboard__provisional[b-ixkkb6bq4p],
    .leaderboard__layout--panel[b-ixkkb6bq4p]  .leaderboard__type-badge-spacing {
        display: none;
    }
}

.leaderboard__panel-bar[b-ixkkb6bq4p] {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-xs);
    margin-bottom: var(--space-sm);
}

/* The row whose profile is open: the selected-surface tint, and aria-current on its link. */
.leaderboard__row--picked td[b-ixkkb6bq4p] {
    background: var(--success-surface);
}

/* ── Not yet played ──────────────────────────────────────────────────────── */
.leaderboard__unplayed[b-ixkkb6bq4p] {
    margin-top: var(--space-sm);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* list-item, not flex: a flex summary loses its disclosure triangle in Chromium. */
.leaderboard__unplayed > summary[b-ixkkb6bq4p] {
    cursor: pointer;
    padding-block: var(--space-xs);
    font-weight: var(--weight-semibold);
}

.leaderboard__unplayed-list[b-ixkkb6bq4p] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-md);
    margin: var(--space-xs) 0 0;
    padding: 0;
    list-style: none;
}

.leaderboard__unplayed-list a[b-ixkkb6bq4p] {
    display: inline-flex;
    align-items: center;
    min-block-size: 1.5rem;
}
/* /Pages/Login.razor.rz.scp.css */
/* Namespaced .login__ like every other surface. The buttons are .po-btn now, so
   only the brand-mandated Microsoft field is restated below — and it now lives
   inside MicrosoftSignInButton.razor.css so Login and NavMenu share the rule. */
.login[b-28u9gmgwix] {
    /* dvh — see app.css .content; 100vh overshoots the visible viewport on mobile. */
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--surface-1);
    padding: var(--space-md);
}

.login__card[b-28u9gmgwix] {
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: var(--space-xl) var(--space-xl);
    width: 100%;
    max-width: 360px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-mlg);
    box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
}

.login__divider[b-28u9gmgwix] {
    display: flex;
    align-items: center;
    width: 100%;
    gap: var(--space-smd);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.login__divider[b-28u9gmgwix]::before,
.login__divider[b-28u9gmgwix]::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--surface-2);
}

/* ── Responsive ──────────────────────────────────────────────────────────────
   The card is max-width:360px, so it fits a phone by luck rather than design: at
   360px viewport the card is exactly the viewport and its --space-xl (2rem) inline
   padding leaves 296px of content, with the page's own --space-md eating 32px more.
   The Microsoft button's label is the first thing to wrap. */
@media (max-width: 640px) {
    .login[b-28u9gmgwix] {
        /* Top-align rather than centre: a centred card jumps when the on-screen
           keyboard opens and halves the visible viewport. */
        align-items: flex-start;
        padding: var(--space-xl) var(--space-sm)
                 max(var(--space-md), env(safe-area-inset-bottom));
    }

    .login__card[b-28u9gmgwix] {
        padding: var(--space-lg) var(--space-mlg);
        gap: var(--space-md);
    }
}
/* /Pages/ModelProfile.razor.rz.scp.css */
/* ── Container ────────────────────────────────────────────────────────
   The profile is a page and also the Leaderboard's side panel, so its breakpoints ask how
   wide the profile is, not how wide the window is: a 34rem panel on a 1440px desktop must
   lay out like a phone. 60rem is where a page split at 1024px used to; 38rem is the content
   width at a 640px window. */
.profile[b-tvbqdy93zq] {
    container: profile / inline-size;
}

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

.profile__header[b-tvbqdy93zq] {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: end;
    gap: var(--space-sm) var(--space-md);
    padding-bottom: var(--space-md);
    border-bottom: 1px solid var(--border);
}

.profile__identity[b-tvbqdy93zq] {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-sm);
}

/* The type badge moved to ModelTypeBadge.razor (.profile__type--* rules removed). */

.profile__standing[b-tvbqdy93zq] {
    display: flex;
    align-items: baseline;
    gap: var(--space-smd);
    font-variant-numeric: tabular-nums;
}

.profile__rank[b-tvbqdy93zq] {
    font-size: var(--text-2xl);
    font-weight: var(--weight-bold);
    color: var(--accent-green);
}

.profile__rank--unranked[b-tvbqdy93zq] {
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    color: var(--text-muted);
}

.profile__elo[b-tvbqdy93zq] {
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    color: var(--text);
}

/* ── Stat strip ─────────────────────────────────────────────────────── */

.profile__stats[b-tvbqdy93zq] {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
    gap: var(--space-smd);
    margin: var(--space-mlg) 0 0;
    padding: 0;
}

.profile__stat[b-tvbqdy93zq] {
    padding: var(--space-smd) var(--space-smd);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-1);
}

.profile__stat dt[b-tvbqdy93zq] {
    margin: 0 0 var(--space-xs);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.profile__stat dd[b-tvbqdy93zq] {
    margin: 0;
    color: var(--text);
    font-size: var(--text-xl);
    font-weight: var(--weight-bold);
    font-variant-numeric: tabular-nums;
}

/* ── Overview: stats beside the curve ───────────────────────────────
   On the --wide page a single column put four stat tiles and the chart each across 1400px.
   From 1024px they share a row: stats as a 2x2 block on the left, the curve given the rest.
   --split is set only when there IS a history section — a model without one keeps the
   full-width stat strip. */
@container profile (min-width: 60rem) {
    .profile__overview--split[b-tvbqdy93zq] {
        display: grid;
        grid-template-columns: minmax(16rem, 1fr) minmax(0, 2.5fr);
        /* The chart spans both rows on the right; stats then upsets stack on the left. */
        grid-template-areas:
            "stats   history"
            "upsets  history";
        grid-template-rows: auto 1fr;
        align-items: start;
        gap: var(--space-md) var(--space-lg);
        margin-top: var(--space-mlg);
    }

    .profile__overview--split .profile__stats[b-tvbqdy93zq] {
        grid-area: stats;
    }

    .profile__overview--split .profile__history[b-tvbqdy93zq] {
        grid-area: history;
    }

    .profile__overview--split .profile__upsets-block[b-tvbqdy93zq] {
        grid-area: upsets;
        margin-top: 0;
    }

    .profile__overview--split .profile__stats[b-tvbqdy93zq] {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        margin-top: 0;
    }

    .profile__overview .profile__history[b-tvbqdy93zq] {
        margin-top: 0;
    }
}

/* The ± and the provisional tag follow the ELO in the header, one step quieter. */
.profile__ci[b-tvbqdy93zq] {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.profile__provisional[b-tvbqdy93zq] {
    padding: 0 var(--space-xs);
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.04em;
}

/* The highlights caption sits under the chart, one line rather than a heading and three tiles. */
.profile__highlights[b-tvbqdy93zq] {
    margin: var(--space-sm) 0 0;
}

/* Recent rating changes: shift · opponent link · rating and date, one line each. */
.profile__changes-title[b-tvbqdy93zq] {
    margin: var(--space-md) 0 0;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text-muted);
}

.profile__changes[b-tvbqdy93zq] {
    display: grid;
    gap: var(--space-xs);
    margin: var(--space-xs) 0 0;
    padding: 0;
    list-style: none;
}

.profile__change[b-tvbqdy93zq] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
}

.profile__shift[b-tvbqdy93zq] {
    min-width: var(--space-2xl);
    font-variant-numeric: tabular-nums;
    font-weight: var(--weight-semibold);
}

.profile__shift--up[b-tvbqdy93zq] {
    color: var(--accent-green);
}

.profile__shift--down[b-tvbqdy93zq] {
    color: var(--accent-red);
}

.profile__change-meta[b-tvbqdy93zq] {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.profile__upsets-block[b-tvbqdy93zq] {
    margin-top: var(--space-md);
}

.profile__upsets[b-tvbqdy93zq] {
    display: grid;
    gap: var(--space-sm);
    margin: var(--space-sm) 0 0;
    padding-inline-start: var(--space-lg);
}

.profile__upset-meta[b-tvbqdy93zq] {
    display: block;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.profile__no-data[b-tvbqdy93zq] {
    color: var(--text-muted);
    font-weight: var(--weight-semibold);
}

.profile__opponent[b-tvbqdy93zq] {
    /* inline-flex + min-height so the hit area clears the WCAG 2.2 SC 2.5.8 minimum of 24x24:
       18px tall as bare inline text. These are the links that make the kill list navigable. */
    display: inline-flex;
    align-items: center;
    min-height: 1.75rem;
    color: var(--text);
    text-decoration: none;
    border-bottom: 1px dotted var(--border);
}

.profile__opponent:hover[b-tvbqdy93zq],
.profile__opponent:focus-visible[b-tvbqdy93zq] {
    color: var(--accent-green);
}

/* ── Kill list, phone ───────────────────────────────────────────────
   .po-table--cards stacks one labelled line per cell — OPPONENT, then RECORD, then the
   Rematch button — ~110px per opponent. Name, record and button fit one line, and each is
   self-evident there, so the labels go and the row stays a single flex line. Last duel is
   already dropped as .po-cell--secondary. Still a real table underneath. */
@media (max-width: 640px) {
    .profile__killlist tr[b-tvbqdy93zq] {
        flex-wrap: nowrap;
        align-items: center;
        gap: var(--space-sm);
    }

    .profile__killlist td[b-tvbqdy93zq] {
        flex: 0 0 auto;
        width: auto;
        padding-block: 0;
        border-bottom: none;
    }

    .profile__killlist td[data-label][b-tvbqdy93zq]::before {
        content: none;
    }

    td.profile__killlist-name[b-tvbqdy93zq] {
        flex: 1 1 auto;
        min-width: 0;
        justify-content: flex-start;
        text-align: left;
    }

    td.profile__killlist-record[b-tvbqdy93zq] {
        color: var(--text-muted);
        font-variant-numeric: tabular-nums;
        white-space: nowrap;
    }
}

/* ── Rating chart ───────────────────────────────────────────────────── */

/* Value range on the left, the curve, dates underneath. */
.profile__chart-wrap[b-tvbqdy93zq] {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-2xs) var(--space-sm);
    padding: var(--space-smd);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface-1);
}

.profile__chart-axis[b-tvbqdy93zq] {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
}

.profile__chart[b-tvbqdy93zq] {
    inline-size: 100%;
    block-size: 12rem;
    border-block-end: 1px solid var(--border);
}

.profile__chart-line[b-tvbqdy93zq] {
    fill: none;
    stroke: var(--accent-blue);
    stroke-width: 2;
}

.profile__chart-base[b-tvbqdy93zq] {
    stroke: var(--text-muted);
    stroke-width: 1;
    stroke-dasharray: 4 4;
}

.profile__chart-dates[b-tvbqdy93zq] {
    grid-column: 2;
    display: flex;
    justify-content: space-between;
    color: var(--text-muted);
    font-size: var(--text-xs);
}

/* ── Scouting report ────────────────────────────────────────────────
   Two tinted columns side by side once the profile is wide enough, stacked below that. The
   label colours are the leaderboard's outcome pairings (green on --success-surface, red on
   --danger-surface), already checked in both themes; the list text stays --text. */

.profile__scout-summary[b-tvbqdy93zq] {
    margin: var(--space-sm) 0 0;
    color: var(--text);
    line-height: var(--leading-snug);
}

.profile__scout-lists[b-tvbqdy93zq] {
    display: grid;
    gap: var(--space-smd);
    margin-top: var(--space-smd);
}

.profile__scout-col[b-tvbqdy93zq] {
    padding: var(--space-smd);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.profile__scout-col--good[b-tvbqdy93zq] {
    border-color: var(--success-border);
    background: var(--success-surface);
}

.profile__scout-col--bad[b-tvbqdy93zq] {
    border-color: var(--danger-border);
    background: var(--danger-surface);
}

.profile__scout-label[b-tvbqdy93zq] {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.profile__scout-col--good .profile__scout-label[b-tvbqdy93zq] {
    color: var(--accent-green);
}

.profile__scout-col--bad .profile__scout-label[b-tvbqdy93zq] {
    color: var(--accent-red);
}

.profile__scout-list[b-tvbqdy93zq] {
    display: grid;
    gap: var(--space-xs);
    margin: var(--space-sm) 0 0;
    padding-inline-start: var(--space-lg);
    color: var(--text);
    font-size: var(--text-sm);
}

@container profile (min-width: 40rem) {
    .profile__scout-lists[b-tvbqdy93zq] {
        grid-template-columns: 1fr 1fr;
    }
}

/* ── Gallery ────────────────────────────────────────────────────────── */

.profile__gallery[b-tvbqdy93zq] {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-md);
}

/* Below the 15rem minimum the grid collapses to one column, which on a phone turned twelve
   thumbnails into twelve full-width rows — 2116px, more than half of a 4073px page, for a
   section that is meant to be skimmed. Two columns halves that, and a 4:3 thumbnail at ~170px
   wide is still perfectly legible as a preview. Explicit 1fr columns rather than auto-fill,
   because auto-fill honours the 15rem minimum and would keep giving one column. */
@container profile (max-width: 38rem) {
    /* One column. `1fr auto` puts the standing beside the name, which is right on a desktop
       and wrong on a 358px card: the rank, rating, interval and provisional chip together are
       ~180px of `auto`, leaving the name under 180px, so "Kimi K2.7 Code" broke across three
       lines to sit next to a row that had plenty of space of its own. The name is what the
       page is about, so it gets the width and the standing goes underneath it. */
    .profile__header[b-tvbqdy93zq] {
        grid-template-columns: minmax(0, 1fr);
        align-items: start;
    }

    .profile__gallery[b-tvbqdy93zq] {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--space-sm);
    }

    .profile__gallery-item[b-tvbqdy93zq] {
        padding: var(--space-xs);
    }

    /* Four stats in a 2×2 grid, and the value steps down one notch — these are glanceable
       figures, not prose. Explicit columns rather than a smaller minmax: auto-fit honours the
       minimum, so it would not reliably land on two. */
    .profile__stats[b-tvbqdy93zq] {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--space-sm);
        margin-top: var(--space-md);
    }

    .profile__stat[b-tvbqdy93zq] {
        padding: var(--space-sm);
    }

    .profile__stat dd[b-tvbqdy93zq] {
        font-size: var(--text-lg);
    }
}

.profile__gallery-item[b-tvbqdy93zq] {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding: var(--space-sm);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-1);
    color: var(--text);
    text-decoration: none;
    /* Hover lift and its transition come from the shared .po-lift utility in app.css; only
       the accent border is per-surface. A local copy of the same transform would fight it. */
    transition: border-color 140ms ease;
}

.profile__gallery-item:hover[b-tvbqdy93zq],
.profile__gallery-item:focus-visible[b-tvbqdy93zq] {
    border-color: var(--accent-green);
}

/* A 16:9 window the width of the tile, and a size container so the frame inside can read
   that width. */
.profile__gallery-frame[b-tvbqdy93zq] {
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    container-type: inline-size;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}

/* The document lays out at 320x180 — the size every model is told to design for, and the
   Arena's frame size — then scales to exactly the tile's width. It used to lay out at
   1000x750 under a fixed scale(0.25): a 320x180 design became an 80x45 speck somewhere in a
   mostly empty page, and on a phone the 250px-wide scaled frame ran past its 165px tile
   (the x=458 overflow). tan(atan2(a, b)) is a / b as a plain number — CSS cannot divide two
   lengths directly, and scale() needs a number. An engine without trig functions drops the
   declaration and shows the frame unscaled, cropped to the tile. */
.profile__gallery-iframe[b-tvbqdy93zq] {
    position: absolute;
    top: 0;
    left: 0;
    width: 320px;
    height: 180px;
    border: 0;
    /* White in both themes, as in SandboxedViewport: the frame renders a model's own
       document, which assumes a white canvas. */
    background: #fff;
    transform: scale(tan(atan2(100cqw, 320px)));
    transform-origin: top left;
    pointer-events: none;
}

.profile__gallery-cover[b-tvbqdy93zq] {
    position: absolute;
    inset: 0;
}

.profile__gallery-prompt[b-tvbqdy93zq] {
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
    color: var(--text);
}

.profile__gallery-meta[b-tvbqdy93zq] {
    color: var(--text-muted);
    font-size: var(--text-xs);
}

@media (prefers-reduced-motion: reduce) {
    .profile__gallery-item[b-tvbqdy93zq] {
        transition: none;
    }
}


/* The leaderboard → profile morph's shared element (view-transition-name). */
.profile__card[b-tvbqdy93zq] {
    padding: var(--space-md) var(--space-mlg);
    border-radius: var(--radius-lg);
    view-transition-name: po-hero;
}
/* /Pages/Settings.razor.rz.scp.css */
/* ── Settings ───────────────────────────────────────────────────────────────
   Two grouped lists of rows: what it is on the left, its control on the right. A whole row is
   the <label> for its checkbox, so the target is the row, not the 24px box. */

/* Narrower than .po-page: on a wide screen a checkbox 1200px from its label is lost. */
.settings[b-xnz4xpy83s] {
    max-width: var(--measure-form);
}

.settings__list[b-xnz4xpy83s] {
    display: flex;
    flex-direction: column;
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    overflow: hidden;           /* keeps a hovered first/last row inside the rounded corners */
}

.settings__row[b-xnz4xpy83s] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-sm) var(--space-md);
    padding: var(--space-smd) var(--space-md);
}

.settings__row + .settings__row[b-xnz4xpy83s] {
    border-top: 1px solid var(--border);
}

.settings__toggle[b-xnz4xpy83s] {
    flex-wrap: nowrap;
    cursor: pointer;
}

.settings__toggle:hover[b-xnz4xpy83s] {
    background: var(--surface-2);
}

.settings__text[b-xnz4xpy83s] {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
    min-width: 0;
}

.settings__label[b-xnz4xpy83s] {
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.settings__hint[b-xnz4xpy83s] {
    margin: 0;
}

.settings__theme[b-xnz4xpy83s] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
}

/* The "Try it" row under an extra that is on: indented under the row it samples. */
.settings__preview[b-xnz4xpy83s] {
    padding-inline-start: var(--space-lg);
    background: var(--surface-2);
}

.settings__quiet[b-xnz4xpy83s] {
    display: flex;
    justify-content: flex-end;
    margin-top: var(--space-sm);
}

/* Native checkbox, sized up from the 13px default so it reads as the row's control. */
.settings__switch[b-xnz4xpy83s] {
    flex: 0 0 auto;
    width: var(--space-mlg);
    height: var(--space-mlg);
    margin: 0;
    cursor: pointer;
}
/* /Pages/Tournament.razor.rz.scp.css */
.tourney__prompt[b-k123894075] {
    max-width: 60rem;
    margin: var(--space-sm) 0 0;
    color: var(--text-muted);
    font-size: var(--text-md);
    font-style: italic;
    line-height: 1.5;
}

.tourney__status[b-k123894075] {
    padding: var(--space-2xs) var(--space-sm);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.tourney__status--running[b-k123894075] {
    background: var(--accent-green);
    color: var(--on-accent-green);
}

.tourney__status--complete[b-k123894075] {
    color: var(--accent-green);
}

.tourney__status--abandoned[b-k123894075] {
    color: var(--accent-red);
}

/* ── Run banners ────────────────────────────────────────────────────── */

.tourney__champion[b-k123894075],
.tourney__abandoned[b-k123894075],
.tourney__progress[b-k123894075] {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-top: var(--space-md);
    padding: var(--space-smd) var(--space-md);
    border-radius: var(--radius-md);
    font-size: var(--text-md);
}

.tourney__champion[b-k123894075] {
    border: 1px solid var(--accent-green);
    background: var(--surface-2);
    color: var(--text);
}

.tourney__champion-text[b-k123894075] {
    flex: 1;
    min-width: 12rem;
}

.tourney__champion-crown[b-k123894075] {
    font-size: var(--text-2xl);
}

.tourney__abandoned[b-k123894075] {
    border: 1px solid var(--accent-red);
    background: var(--surface-1);
    color: var(--text);
}

.tourney__progress[b-k123894075] {
    border: 1px solid var(--border);
    background: var(--surface-1);
    color: var(--text-muted);
}

.tourney__watch[b-k123894075] {
    margin-left: var(--space-sm);
    color: var(--accent-green);
    font-weight: 600;
    text-decoration: none;
}

.tourney__watch:hover[b-k123894075],
.tourney__watch:focus-visible[b-k123894075] {
    text-decoration: underline;
}

/* ── Bracket ────────────────────────────────────────────────────────── */

/* Scrolls inside its own container rather than pushing the page sideways: three rounds of
   cards is wider than a phone whatever the card width. */
.tourney__bracket-wrap[b-k123894075] {
    margin-top: var(--space-lg);
    overflow-x: auto;
}

.tourney__bracket[b-k123894075] {
    display: grid;
    grid-template-columns: repeat(var(--rounds, 3), minmax(13rem, 1fr));
    gap: var(--space-md);
    min-width: min-content;
}

/* The champion's road: each match it won lights up gold, round by round (--path-step). */
.tourney__match--champion-path[b-k123894075] {
    position: relative;
}

.tourney__match--champion-path[b-k123894075]::after {
    content: "";
    position: absolute;
    inset: -3px;
    border: 2px solid var(--accent-yellow);
    border-radius: inherit;
    box-shadow: 0 0 18px color-mix(in srgb, var(--accent-yellow) 45%, transparent);
    pointer-events: none;
    opacity: 0;
    animation: tourney-path-b-k123894075 700ms ease-out calc(var(--path-step, 0) * 280ms + 400ms) forwards;
}

@keyframes tourney-path-b-k123894075 {
    from { opacity: 0; transform: scale(1.06); }
    to   { opacity: 1; transform: none; }
}

/* A glint runs across each lit card just after it lights — the light travelling the road,
   round by round. Its own clipping box, so the card keeps no overflow: hidden that would cut
   off the gold ring above, which sits 3px outside it. Transform only. */
.tourney__glint[b-k123894075] {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: inherit;
    pointer-events: none;
}

.tourney__glint[b-k123894075]::before {
    content: "";
    position: absolute;
    inset-block: 0;
    left: 0;
    width: 40%;
    background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--accent-yellow) 45%, transparent), transparent);
    transform: translateX(-110%);
    animation: tourney-glint-b-k123894075 900ms ease-out calc(var(--path-step, 0) * 280ms + 650ms) both;
}

@keyframes tourney-glint-b-k123894075 {
    from { transform: translateX(-110%); }
    to   { transform: translateX(260%); }
}

@media (prefers-reduced-motion: reduce) {
    .tourney__match--champion-path[b-k123894075]::after {
        opacity: 1;
        animation: none;
    }

    .tourney__glint[b-k123894075] {
        display: none;
    }
}

.tourney__round[b-k123894075] {
    display: flex;
    flex-direction: column;
}

.tourney__round-title[b-k123894075] {
    margin: 0 0 var(--space-sm);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

/* space-around is what aligns each match against the pair feeding it: every round has half as
   many cards in the same height, so each one centres on the gap between its two sources. */
.tourney__round-matches[b-k123894075] {
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: space-around;
    gap: var(--space-smd);
}

.tourney__match[b-k123894075] {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: var(--space-sm);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-1);
    color: inherit;
    text-decoration: none;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
}

/* [href]: a match with no duel yet is not a link, so it must not hover like one. */
.tourney__match[href]:hover[b-k123894075],
.tourney__match:focus-visible[b-k123894075] {
    border-color: var(--accent-green);
}

.tourney__match:focus-visible[b-k123894075] {
    outline: 2px solid var(--accent-blue);
    outline-offset: var(--focus-ring-offset);
}

.tourney__match--running[b-k123894075] {
    border-color: var(--accent-green);
    box-shadow: 0 0 0 1px var(--accent-green);
}

.tourney__match--pending[b-k123894075] {
    opacity: 0.6;
}

.tourney__match--failed[b-k123894075] {
    border-color: var(--accent-red);
}

/* A decided match recedes so the bracket's live edge is what the eye lands on — by the final
   round most of the board is history. (A ready match gets no modifier: it looks like the base
   card, and a rule restating the default is dead weight.) */
.tourney__match--decided[b-k123894075] {
    background: var(--surface-2);
}

.tourney__slot[b-k123894075] {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    border-radius: var(--radius-sm);
    font-size: var(--text-md);
}

.tourney__slot--won[b-k123894075] {
    background: var(--surface-2);
    color: var(--text);
    font-weight: 700;
}

.tourney__slot--lost[b-k123894075] {
    color: var(--text-muted);
    text-decoration: line-through;
    text-decoration-thickness: 1px;
}

.tourney__slot--empty[b-k123894075] {
    color: var(--text-muted);
}

.tourney__slot-seed[b-k123894075] {
    flex: 0 0 1.1rem;
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

.tourney__slot-name[b-k123894075] {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tourney__slot-badge[b-k123894075] {
    flex: 0 0 auto;
}

/* Shown only on the running match — the card itself is the link. */
.tourney__match-live[b-k123894075] {
    align-self: flex-start;
    margin-top: var(--space-xs);
    padding: var(--space-2xs) var(--space-sm);
    border: 1px solid var(--success-border);
    border-radius: var(--radius-full);
    background: var(--success-surface);
    color: var(--accent-green);
    font-size: var(--text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* ── Swiss table ────────────────────────────────────────────────────── */

.tourney__standings[b-k123894075] {
    margin-top: var(--space-lg);
}

.tourney__standings-num[b-k123894075] {
    font-variant-numeric: tabular-nums;
}

/* The crowned row: tint plus weight, so the leader reads without relying on colour alone. */
.tourney__standings-row--champion td[b-k123894075] {
    background: var(--success-surface);
    font-weight: 700;
}

.tourney__tiebreak[b-k123894075],
.tourney__match-error[b-k123894075] {
    margin-top: var(--space-xs);
    font-size: var(--text-2xs);
    line-height: 1.4;
}

.tourney__tiebreak[b-k123894075] {
    color: var(--text-muted);
    font-style: italic;
}

.tourney__match-error[b-k123894075] {
    color: var(--accent-red);
}

.tourney__section-head[b-k123894075] {
    margin-bottom: var(--space-sm);
}

/* Matches .home__textarea exactly — the same control, written twice, had drifted on border
   token (--border vs --border-strong), on size (--text-md vs --text-lg) and on focus
   treatment. Same prompt, same box. */
.tourney__textarea[b-k123894075] {
    width: 100%;
    box-sizing: border-box;
    min-height: 7rem;
    padding: var(--space-smd) var(--space-md);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface-1);
    color: var(--text);
    font-family: inherit;
    font-size: var(--text-lg);
    line-height: 1.5;
    resize: vertical;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.tourney__textarea:focus-visible[b-k123894075] {
    outline: 2px solid var(--accent-blue);
    outline-offset: var(--focus-ring-offset);
    box-shadow: var(--focus-ring);
    border-color: var(--accent-cyan);
}

.tourney__textarea:focus:not(:focus-visible)[b-k123894075] {
    border-color: var(--accent-cyan);
}

.tourney__textarea[b-k123894075]::placeholder {
    color: var(--text-dim);
}


/* ── Actions and history ────────────────────────────────────────────── */

/* Bracket / Swiss, beside Start. */
.tourney__format[b-k123894075] {
    display: flex;
    gap: var(--space-xs);
}

/* Its own line under the button. Beside it, the hint read as a caption for whatever was to its
   right, and the one sentence explaining the button was the easiest thing on the page to miss. */
.tourney__start-hint[b-k123894075] {
    flex-basis: 100%;
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-align: center;
}

/* Start was pressed with no usable prompt: the hint and the box it points at both say so. */
.tourney__start-hint--blocked[b-k123894075] {
    color: var(--accent-red);
    font-weight: var(--weight-semibold);
}

.tourney__textarea[aria-invalid="true"][b-k123894075] {
    border-color: var(--accent-red);
}

.tourney__recent[b-k123894075] {
    margin: 0;
    padding: 0;
    list-style: none;
}

.tourney__recent-item[b-k123894075] {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-sm) 0;
    border-bottom: 1px solid var(--border);
    color: var(--text);
    font-size: var(--text-md);
    text-decoration: none;
}

.tourney__recent-item:hover .tourney__recent-prompt[b-k123894075],
.tourney__recent-item:focus-visible .tourney__recent-prompt[b-k123894075] {
    color: var(--accent-green);
}

.tourney__recent-prompt[b-k123894075] {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tourney__recent-meta[b-k123894075] {
    flex: 0 0 auto;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.tourney__recent-champ[b-k123894075] {
    color: var(--text);
    font-weight: 600;
}

@media (max-width: 640px) {
    .tourney__bracket[b-k123894075] {
        grid-template-columns: repeat(var(--rounds, 3), minmax(11rem, 1fr));
    }

    /* The bracket is the one thing on the page that legitimately scrolls sideways — three
       rounds cannot be stacked without destroying the shape that makes it a bracket. Make
       that scroll discoverable and finger-friendly rather than accidental. */
    .tourney__bracket-wrap[b-k123894075] {
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
        padding-bottom: var(--space-xs);
    }

    .tourney__round[b-k123894075] {
        scroll-snap-align: start;
    }
}


/* Reserves vertical space for the entrant list so the prompt box and Start button below it do
   not slide down the page when the models resolve. Sized to a typical eight-model list rather
   than to the maximum — overshooting would leave a visible gap on a short list, which trades a
   layout shift for dead space. */
/* Reserves the drawn bracket's height while it loads, so the prompt box and the Start button
   do not slide down the page as the models arrive — a runtime trace measured that shift at
   CLS 0.289 on a phone, past the 0.1 "needs improvement" mark and the only route in the app
   that moved at all. Sized to the bracket (two matches a side), not to the eight-row list it
   replaced, which left a dead half-screen under it. */
.tourney__entrants-slot[b-k123894075] {
    min-height: 11rem;
}

@media (max-width: 640px) {
    .tourney__entrants-slot[b-k123894075] {
        /* The halves stack below this width, so the reserve is the full four matches. */
        min-height: 21rem;
    }
}

/* Shown only while this tab is driving a WebGPU match. The page is otherwise a viewer of a
   server-side run, so the one case where closing it has a consequence needs to say so. */
.tourney__local-notice[b-k123894075] {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
    padding: var(--space-xs) var(--space-smd);
    border: 1px solid var(--warn-border);
    border-radius: var(--radius-md);
    background: var(--warn-surface);
    color: var(--accent-yellow);
    font-size: var(--text-sm);
}

/* ── First-round draw ────────────────────────────────────────────────────
   Four seeds a side converging on the final, the way a bracket is normally read. The two
   halves mirror: the right half reverses its rows so its ties point inward at the centre,
   which is what makes the shape legible rather than just two lists with a word between them. */
.tourney__draw[b-k123894075] {
    display: grid;
    /* minmax(0, 1fr), not bare 1fr: a 1fr track's automatic minimum is its content's
       min-content width, so a long model name forced the track (and with it the whole page
       container) to 375px+ on a 390px phone — the page rendered clipped with no scrollbar to
       recover it. Explicit zero minimums let the track shrink and the name ellipsize instead. */
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: var(--space-md);
    margin-bottom: var(--space-sm);
}

.tourney__draw-half[b-k123894075] {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    min-width: 0;
}

.tourney__draw-half--right[b-k123894075] {
    /* Mirrored so the seed number sits on the inside edge on both sides. */
    direction: rtl;
}

.tourney__draw-half--right .tourney__draw-slot[b-k123894075] {
    direction: ltr;
}

.tourney__draw-match[b-k123894075] {
    display: flex;
    flex-direction: column;
    gap: 1px;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--border);
}

.tourney__draw-slot[b-k123894075] {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-smd);
    background: var(--surface-2);
}

.tourney__draw-seed[b-k123894075] {
    min-width: 1.5rem;
    flex: 0 0 auto;
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-variant-numeric: tabular-nums;
    text-align: center;
}

.tourney__draw-name[b-k123894075] {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
    font-size: var(--text-sm);
}

.tourney__draw-elo[b-k123894075] {
    flex: 0 0 auto;
    color: var(--text-muted);
    font-size: var(--text-2xs);
    font-variant-numeric: tabular-nums;
}

.tourney__draw-centre[b-k123894075] {
    display: flex;
    justify-content: center;
}

.tourney__draw-final[b-k123894075] {
    padding: var(--space-xs) var(--space-smd);
    border: 1px dashed var(--border);
    border-radius: var(--radius-full);
    color: var(--text-muted);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.tourney__field-actions[b-k123894075],
.tourney__entrant-picker-head[b-k123894075] {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
}

.tourney__entrant-picker[b-k123894075] {
    margin-top: var(--space-sm);
    padding: var(--space-md);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.tourney__entrant-picker-head[b-k123894075] {
    justify-content: space-between;
    margin-bottom: var(--space-sm);
}

.tourney__entrant-picker-head p[b-k123894075] {
    margin: 0;
}

.tourney__entrant-options[b-k123894075] {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-xs);
    max-height: 22rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.tourney__entrant-option[b-k123894075] {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    align-items: center;
    gap: var(--space-xs);
    min-height: var(--target-min);
    padding: var(--space-xs) var(--space-sm);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.tourney__entrant-option--selected[b-k123894075] {
    border-color: var(--accent-green);
    background: var(--success-surface);
}

.tourney__entrant-option:has(input:focus-visible)[b-k123894075] {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

.tourney__entrant-name[b-k123894075] {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: var(--weight-semibold);
}

.tourney__entrant-elo[b-k123894075] {
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
}

/* Below the small breakpoint the mirror stops helping — two columns of four are wider than
   the screen — so the halves stack and read top to bottom. */
@media (max-width: 640px) {
    .tourney__draw[b-k123894075] {
        /* Same zero-minimum reasoning as the three-column rule above. */
        grid-template-columns: minmax(0, 1fr);
    }

    .tourney__draw-half--right[b-k123894075] {
        direction: ltr;
    }

    .tourney__entrant-options[b-k123894075] {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ── Phone: no ellipsis ──────────────────────────────────────────────────────
   Three names were cut mid-word at 390px. A model name is the one thing a bracket cannot
   abbreviate — "GPT-5.4 M…" and "GPT-5.4 N…" are different entrants — so below 640px the
   names (and the recent list's prompts) wrap onto a second line instead. */
@media (max-width: 640px) {
    .tourney__slot-name[b-k123894075],
    .tourney__draw-name[b-k123894075],
    .tourney__recent-prompt[b-k123894075] {
        overflow: visible;
        text-overflow: clip;
        white-space: normal;
        overflow-wrap: anywhere;
    }
}

/* ── Setup: field beside the prompt ──────────────────────────────────────────
   From 1024px the draw takes the wider left column and the prompt, library and Start the
   right, the same shape as Home's picker and console. Stacked, the three ran down a 1440x900
   desktop to y≈620 and left the rest of the page empty. 1.4fr keeps a draw half ~230px wide
   at 1024, enough for a model name, its seed and its rating on one line. */
@media (min-width: 1024px) {
    .tourney__setup[b-k123894075] {
        display: grid;
        grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
        align-items: start;
        gap: var(--space-lg);
    }

    .tourney__setup-side[b-k123894075] {
        position: sticky;
        top: calc(var(--nav-height) + var(--space-md));
    }

    /* The section rhythm restarts in each column: the first block of each sits level. */
    .tourney__setup > .po-section[b-k123894075],
    .tourney__setup-side > .po-section:first-child[b-k123894075] {
        margin-top: 0;
    }
}

.tourney__setup-field[b-k123894075],
.tourney__setup-side[b-k123894075] {
    min-width: 0;
}
