/* SPEC-32 — shared profile SECTION chrome, as GLOBAL (non-isolated) CSS.

   WHY GLOBAL (S6, #190): these rules were in ProfileRenderer.razor.css under
   `.profile-render ::deep …`, so they only applied to sections that had a <ProfileRenderer>
   ancestor (its scope roots the ::deep). The admin modal + kiosk wrap the sections in
   ProfileRenderer, so they got the styling; the portal /me composes <ProfileRatingSection> /
   <ProfileHistorySection> DIRECTLY (per the OQ1 ruling — /me is multi-club/account-level and
   can't host the single-member orchestrator), so with no `.profile-render` ancestor NONE of the
   `::deep` rules matched and the sections rendered unstyled (ux round-2 Finding 1).

   The section components are meant to be reusable whether or not they're wrapped, so this shared
   card/hero/pill chrome belongs in ONE global stylesheet (a single move — no wrapper `::deep`, no
   per-section duplication, no double-application). ProfileRenderer.razor.css now holds ONLY the
   wrapper's own container layout (the inter-section stacked gap) + its own "Tonight" block. The
   classnames here are unique to SPEC-32 (the legacy modal uses `.member-profile-*`), so plain
   global selectors don't collide; the two genuinely-cross-context classes (`.table-responsive`,
   `.member-settling-chip-icon` from the shared SettlingChip) are scoped under a `.profile-*`
   ancestor so they never leak to other surfaces. Wrapped (admin/kiosk/S7) and composed (/me) both
   pick these up identically. */

/* Overflow guard (Bl165): flex-column items + block section bodies default to min-width:auto (=
   max-content), so a wide child — the session-history table on phones — would force the stack wider
   than its container and scroll horizontally. Re-assert min-width:0 and let the table's own
   `.table-responsive` (overflow-x:auto) scroll INSIDE the section. */
.profile-section,
.profile-section-body {
    min-width: 0;
}

.profile-section .table-responsive {
    overflow-x: auto;
}

/* ---- identity header ---- */
.profile-identity {
    display: flex;
    gap: 0.85rem;
    align-items: center;
}

.profile-identity-photo,
.profile-identity-photo-fallback {
    width: 3.25rem;
    height: 3.25rem;
    border-radius: 50%;
    flex: 0 0 auto;
    object-fit: cover;
}

.profile-identity-photo-fallback {
    display: grid;
    place-items: center;
    color: #fff;
    font-weight: 700;
    font-size: 1.15rem;
    background: linear-gradient(135deg, #2f6f9c, #14a0a0);
}

.profile-identity-photo-fallback.male { background: linear-gradient(135deg, #2f6f9c, #3a86c9); }
.profile-identity-photo-fallback.female { background: linear-gradient(135deg, #9c4f8a, #c96fb0); }

.profile-identity-name {
    font-size: 1.2rem;
    font-weight: 700;
    color: #253f5b;
    letter-spacing: -0.01em;
}

.profile-identity-meta {
    color: #5c6a7d;
    font-size: 0.85rem;
    margin-top: 0.1rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    align-items: baseline;
}

.profile-identity-sep { color: #b0bccb; }

/* ---- section cards ---- */
.profile-section {
    border: 1px solid #d6e0ea;
    border-radius: 0.6rem;
    overflow: hidden;
    background: #ffffff;
}

.profile-section-head {
    padding: 0.5rem 0.75rem;
    background: #eef4fa;
    border-bottom: 1px solid #d6e0ea;
}

.profile-section-title {
    margin: 0;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #5c6a7d;
    font-weight: 700;
}

/* SPEC-32 I2-S1b (#314), AC-I2-S1-11: the section sub-label that names a block's GRAIN — "Night by
   night" on History, "Most recent, game by game" on Matches. Global (not component-scoped) precisely
   because it is a PAIR: two different components emit it, and a scoped rule would style one and
   silently not the other.

   CONTRAST. #5f6f83 on the #eef4fa section head = 4.64:1, clearing WCAG AA (4.5:1) for this 0.72rem
   text. The first value shipped in review was #7b8a9c = 3.18:1, which failed — on FOUR hosts
   including the shared kiosk screen, the one read at distance. Do not lighten it.

   The quieter-than-the-title effect is therefore carried by WEIGHT AND CASE (500, sentence case) and
   not by colour: the sibling .profile-section-title is 700 and uppercase at 4.97:1, so the two are
   close in luminance by necessity. On a background this light there is no room for a "muted" grey
   that also passes AA at this size, and legibility wins over the hierarchy cue. */
.profile-section-sub {
    margin: 0.15rem 0 0;
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.01em;
    color: #5f6f83;
}

.profile-section-body {
    padding: 0.75rem;
}

/* ---- section helper + failure lines (SPEC-32 I2-S1b, #314) ----

   THESE HAD NO REACHABLE DEFINITION AT ALL, and that is why they are here.

   `.club-help-text` is the class all three history-group sections emit for their empty and error
   lines, and the ONLY files defining it are Members / Settings / MemberEditor / MemberAccountStatusHeader
   .razor.css — component-scoped stylesheets that do not apply on the profile render path. So on a
   profile the line rendered as Bootstrap body text: rgb(33,37,41), 16px, no background, no border,
   indistinguishable from any other paragraph on the page. Verified by reading getComputedStyle on the
   rendered page, not by reading the class list — the whole lesson of this increment.

   That was pre-existing for History and Partners, but I2-S1b is what makes it REACHABLE on two
   player-facing pages for the first time (this slice moved a failed read from a whole-page shell to
   per-section states), so it is this slice's to fix on the same principle as the sub-label.

   THE COMPOSED FAILURE IS THE CASE THAT MATTERS. All four error flags are set from ONE `history.Failed`
   value, so they always fail together: without a treatment the reader gets four back-to-back plain
   sentences, which reads as a broken page rather than a considered state. `.profile-section-error`
   therefore uses the shipped empty-state panel token (`.my-empty`'s tint/border/radius) so each
   failure reads as a deliberate panel, and the four stack as four panels rather than as loose text.

   GLOBAL, not component-scoped: three different components emit these, and a scoped rule would style
   one and silently not the others. Anchored under `.profile-section` so nothing outside a profile
   section is touched. */
.profile-section .club-help-text {
    margin: 0;
    font-size: 0.9rem;
    color: #47586b;
}

.profile-section-body .profile-section-error {
    display: block;
    font-size: 0.9rem;
    color: #47586b;
    background: #f6f9fc;
    border: 1px solid #d9e6f2;
    border-radius: 0.6rem;
    padding: 0.75rem 0.85rem;
}

/* The retry link inside a failure panel.
   RECORD CORRECTION: the I2-S1b rework commit said this rule had been "promoted to global". It had
   not — I added it, then deleted it again in an edit meant only to reorder a comment, and wrote the
   claim without re-checking. The link merely inherited the page's default anchor blue. It exists now. */
.profile-section-retry {
    color: #006bb7;
    font-weight: 600;
    text-decoration: underline;
}

.profile-section-retry:hover,
.profile-section-retry:focus-visible {
    text-decoration: none;
}

/* ---- the viewer's own head-to-head (SPEC-32 I2-S2, #314) ----

   GLOBAL, NOT COMPONENT-SCOPED, and that is the whole lesson of I2-S1b rather than a preference:
   `.club-help-text` and the retry link were both defined only in component-scoped stylesheets that do
   not apply on the profile render path, so they rendered as unstyled Bootstrap body text while every
   class list looked correct. Blazor CSS isolation lets a rule silently not apply. Presence is not
   application — these values are verified with getComputedStyle on the rendered page.

   NO NEW VISUAL LANGUAGE. This is the shipped `.profile-section` card — same border, radius and head
   band. It is distinguished by (a) a title beginning "Your", (b) second-person copy, and (c) the
   app's blue as a left edge. THE COLOUR IS THE THIRD CUE AND NEVER THE ONLY ONE: strip it and the
   copy still says whose fact this is. #006bb7 is the token already in this file (the retry link), not
   a new one. */
.profile-section-h2h {
    border-left: 4px solid #006bb7;
}

/* The sentences. Slightly larger than `.club-help-text` (0.9rem) because this is a headline rather
   than a helper line. #253f5b on #ffffff MEASURES 10.82:1 (WCAG 2.1 relative luminance, computed —
   my first draft of this comment said "10.2:1" from memory and was wrong, which is exactly why the
   rule is to measure). Every number in these sentences sits in the same phrase as the word "you", so
   the type never has to carry the attribution on its own. */
.profile-section-h2h .profile-h2h-line {
    margin: 0;
    font-size: 0.95rem;
    line-height: 1.45;
    color: #253f5b;
}

.profile-section-h2h .profile-h2h-line + .profile-h2h-line {
    margin-top: 0.35rem;
}

/* R-I2-17's scope line, rendered only in the hidden composition. Quieter than the sentences it
   qualifies — it is a note about what the reader is looking at, not part of the record. #5f6f83 on
   #ffffff MEASURES 5.14:1, clearing AA (4.5:1) at this size. (Also corrected from an unmeasured
   "5.9:1"; it still passes, but a passing guess is still a guess.) */
.profile-section-h2h .profile-h2h-scope {
    margin: 0.5rem 0 0;
    font-size: 0.85rem;
    color: #5f6f83;
}

/* ---- fields grid (contact + account) ---- */
.profile-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.6rem 1.1rem;
    margin: 0;
}

.profile-fields dt {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #8493a6;
    font-weight: 650;
}

.profile-fields dd {
    margin: 0.1rem 0 0;
    font-size: 0.95rem;
    color: #253f5b;
}

.profile-field-muted {
    color: #8493a6;
    font-style: italic;
}

/* ---- rating ---- */
.profile-rating-hero {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    flex-wrap: wrap;
}

.profile-rating-value {
    font-size: 2rem;
    font-weight: 750;
    letter-spacing: -0.02em;
    color: #253f5b;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.profile-settling {
    font-size: 0.8rem;
}

.profile-settling-summary {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    cursor: pointer;
    color: #9a6700;
    background: #f6ecd6;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    font-weight: 650;
    list-style: none;
    width: fit-content;
}

.profile-settling-summary::-webkit-details-marker { display: none; }

/* Scoped under .profile-settling so this NEVER re-sizes the shared SettlingChip glyph on other
   surfaces (organiser Rating tab, etc.) — it only sizes the glyph inside a profile settling pill. */
.profile-settling .member-settling-chip-icon {
    width: 0.9rem;
    height: 0.9rem;
}

.profile-settling-copy {
    margin: 0.5rem 0 0;
    font-size: 0.85rem;
    color: #5c6a7d;
    max-width: 60ch;
}

.profile-rating-chart {
    margin-top: 0.6rem;
    height: 180px;
    position: relative;
}

/* ---- players ---- */
.profile-players-group { margin-bottom: 0.4rem; }

.profile-players-subhead {
    font-size: 0.8rem;
    font-weight: 650;
    color: #253f5b;
    margin: 0 0 0.4rem;
    cursor: default;
}

.profile-players-opponents > summary {
    cursor: pointer;
    margin-top: 0.6rem;
}

/* ---- players tables (Partners + Opponents) ----
   MOVED here from MemberProfileModal.razor.css (SPEC-32 fix). These were scoped
   (non-::deep) selectors carrying that modal's CSS-isolation attribute, but SPEC-32 S2
   moved the <table class="members-table"> into the child ProfilePlayersSection, which
   never gets that scope attribute — so the rules matched on NO lens and the columns
   collapsed to zero padding. As GLOBAL rules here they reach ProfilePlayersSection on
   every lens (admin modal, kiosk, clubmate static-SSR; /me composes only Rating+History).

   ANCHORED under `.profile-section-players` (ProfilePlayersSection's root <section>) — NOT
   bare `.members-table` — because the class is NOT unique to this section: the shared
   MemberSessionHistoryTable also carries `class="members-table member-session-history-table"`.
   A bare global `.members-table tbody td { white-space: nowrap }` would tie on specificity
   with that table's own scoped `.member-session-history-table tbody td` and (by source order)
   force its Session/Results columns to stop wrapping — a cross-lens regression. Anchoring to
   the players section confines these rules to the Partners/Opponents tables and never touches
   the history table (which is never inside .profile-section-players). Same "scope cross-context
   classes under a .profile-* ancestor" convention as .table-responsive / the settling chip above. */
.profile-section-players .members-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.88rem;
}

.profile-section-players .members-table thead th {
    text-align: left;
    color: #60778f;
    font-size: 0.76rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    padding: 0.4rem 0.6rem;
    border-bottom: 1px solid #d8e0ea;
    white-space: nowrap;
}

.profile-section-players .members-table tbody td {
    padding: 0.4rem 0.6rem;
    border-bottom: 1px solid #e6edf4;
    color: #24384f;
    vertical-align: middle;
    /* Data cells (games / W / L / win% / date) stay on one line so adjacent
       columns keep clear separation; the name column below opts back into
       wrapping for long names. */
    white-space: nowrap;
}

/* The first column is the partner / opponent NAME — let it wrap rather than
   force the table wider than the modal (the .table-responsive wrapper still
   scrolls as a last resort on very narrow phones). */
.profile-section-players .members-table tbody td:first-child {
    white-space: normal;
}

.profile-section-players .members-table th.numeric,
.profile-section-players .members-table td.numeric {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* ---- sortable column headers (SPEC-32 fix CHANGE 2; removed at I2-S13, RESTORED at I2-S13c) ----

   Click-to-sort via vanilla-JS progressive enhancement (sortableTable.js), so it works identically on
   the static-SSR portal lenses that have NO Blazor circuit. The header control is a real <button> so
   it is natively focusable + keyboard-operable (Enter/Space); the ▲/▼/↕ indicator is driven purely
   from aria-sort so JS only toggles that one attribute.

   WHY IT WENT, AND WHY IT IS BACK. I2-S13 paged these two lists, and a DOM sort over page 1 of 13
   presents a lie about the reader's own data — R-I2-30. The Owner un-paged both views on 2026-08-31
   (counterparts saturate at club size: measured median 12, max 265 across 3,750 member-club pairs), so
   every row is in the DOM again, the DOM sort IS the whole set, and the requirement stops biting. The
   removal had also reached the admin modal and the kiosk, which were never paged and where the sort
   was honest throughout; they get it back too.

   THESE RULES ARE NOT ENOUGH ON THEIR OWN, and the markup is where the honesty lives: the section
   withholds `data-sortable`, the header <button>s and `aria-sort` entirely when the counterpart read
   ceiling truncates the list (ProfilePlayersSection.Sortable). A rule that styled a control the
   markup no longer emits would simply be inert, which is why the guard is server-side and not here. */
.profile-section-players .members-table-sort {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    width: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    cursor: pointer;
}

.profile-section-players .members-table th.numeric .members-table-sort {
    justify-content: flex-end;
}

.profile-section-players .members-table-sort:focus-visible {
    outline: 2px solid #0d6efd;
    outline-offset: 2px;
    border-radius: 2px;
}

.profile-section-players .members-table-sort-ind {
    font-size: 0.7em;
    line-height: 1;
}

.profile-section-players .members-table th[aria-sort="none"] .members-table-sort-ind::after {
    content: "\2195"; /* ↕ neutral: signals the column is sortable */
    opacity: 0.35;
}

.profile-section-players .members-table th[aria-sort="ascending"] .members-table-sort-ind::after {
    content: "\25B2"; /* ▲ */
}

.profile-section-players .members-table th[aria-sort="descending"] .members-table-sort-ind::after {
    content: "\25BC"; /* ▼ */
}

/* ---- SPEC-32 I2-S13 (#314): the counterpart table's VIEW shape ----

   `-fit` is present ONLY on the portal's named views. The stacked hosts (admin member modal, kiosk)
   render into a much wider panel, keep all six columns and keep their `.table-responsive` wrapper;
   they are untouched by everything below.

   NO SCROLL WRAPPER, AND THAT IS THE POINT. The view shape's wrapper is `.profile-players-table-wrap`
   — deliberately NOT `.table-responsive`, whose global `overflow-x: auto` is what let this table grow
   its own horizontal scrollbar inside the 504px content column. That is #614's defect and ux-plan §8
   forbids it as a fallback: the fix for overflow is fewer or narrower columns, never a wrapper. A
   horizontal scrollbar is invisible on touch until somebody happens to swipe sideways on a page that
   scrolls vertically, which is exactly why it went unnoticed. Six columns became five (the
   last-played column left the view shape in the markup, not in CSS), and the name column wraps. */
.profile-players-table-wrap {
    /* Named for symmetry with .table-responsive and to give the geometry gate something to measure.
       Explicitly `visible`: if a future rule ever reintroduces a scroll wrapper here it has to
       overwrite a declaration that says why it must not. */
    overflow-x: visible;
    min-width: 0;
}

.profile-section-players .profile-players-table-fit {
    /* Take the container, do not exceed it. The cells below opt into wrapping, so rows get taller
       rather than the table getting wider — §8's first allowed fallback, which costs vertical space
       the page has and loses nothing. */
    width: 100%;
    table-layout: fixed;
}

/* The name column takes the room the four numeric ones do not. Fixed layout means these are the
   whole allocation, so they are stated rather than left to the browser's content-based guess — which
   is what produced a 524px table in a 478px box on the sibling block. */
.profile-section-players .profile-players-table-fit th:first-child,
.profile-section-players .profile-players-table-fit td:first-child {
    width: auto;
    white-space: normal;
    overflow-wrap: break-word;
}

.profile-section-players .profile-players-table-fit th.numeric,
.profile-section-players .profile-players-table-fit td.numeric {
    width: 4.2rem;
}

/* ---- SPEC-32 I3-S3 (#670): the last-game date is A COLUMN AGAIN, on all four hosts ----

   IT USED TO BE AN INLINE SPAN HIDDEN AT EVERY TABLE WIDTH, and the rule that hid it stood here.
   I2-S13b could not make it a column: the view shape had five at 504px, and a sixth hidden by a media
   query would have failed the geometry gate's `thead th` check on the stated ground that the drop was
   SERVER-side. R-I2-40 (I3-S0, #677) widened the container, so the column fits and the span is gone —
   one cell, two presentations, no element that exists only for one tier.

   THE WIDTH IS STATED BECAUSE THE LAYOUT IS FIXED. `table-layout: fixed` takes its allocation from the
   first row, so an unstated width here is the browser's guess — which is what produced a 524px table
   in a 478px box on the sibling block.

   8rem IS MEASURED, AND WHAT IT HOLDS IS THE HEADER RATHER THAN THE DATE. `25 Aug 2026` needs 94.88px;
   the SORT HEADER `Last partnered` plus its ↕ indicator needs 125px, and it does not wrap — the button
   is `display: inline-flex`, so the label is a flex item that overflows its container rather than
   breaking. 6.4rem was tried first, sized from the date, and measured 805px of content in a 782px
   wrapper at 1920px on the Partners view (Opponents, whose `Last played` is shorter, sat exactly at
   782/782 and hid the defect). That is #614's shape arriving through a column allocation, so the
   allocation is sized from the WIDEST thing in the column. `nowrap` keeps a future longer value
   failing visibly instead of silently costing a line.

   The header takes the same class from `Column.CellClass`, so the allocation and the cell are one
   declaration and cannot be given to different columns by a later insertion. */
.profile-section-players .profile-players-table-fit th.profile-players-date,
.profile-section-players .profile-players-table-fit td.profile-players-date {
    width: 8rem;
    white-space: nowrap;
}

/* ---- SPEC-32 I3-S4 (#670): the `vs expected` column, ON THE VIEWS ONLY ----

   THE ALLOCATION IS STATED FOR THE SAME REASON THE DATE'S IS: `table-layout: fixed` takes its
   allocation from the first row, so an unstated width here is the browser's guess. The numeric rule
   above allocates 4.2rem, and this column is NOT one of the four counts — it shares their alignment
   and not their width, which is why `Column.HeadClass` had to start composing `numeric` with a cell
   class instead of choosing between them.

   SIZED FROM THE HEADER, NOT FROM THE VALUE — I3-S3's own finding, applied rather than rediscovered.
   The widest value is `+123` (~32px); the sort header `vs expected` plus its ↕ indicator is what needs
   the room. It is TWO WORDS and `thead th` below sets `white-space: normal`, so unlike `Last
   partnered` it may break between them — measured, and the width below is what holds it on two lines
   without overflowing its own cell.

   NO `nowrap`. The date column takes it because its value is one unbreakable string and a longer one
   must fail visibly; here the value is four characters and the HEADER is what fills the column, so
   forbidding the break would recreate exactly the 805px-in-a-782px-wrapper overflow I3-S3 measured. */
.profile-section-players .profile-players-table-fit th.profile-players-vsexp,
.profile-section-players .profile-players-table-fit td.profile-players-vsexp {
    width: 5.4rem;
}

/* ---- SPEC-32 I4-S2 (#670): the cell's CONFIDENCE, and NOT its colours ----

   NOTHING HERE SETS A COLOUR, AND THAT IS THE POINT. The delta's over/under/even ink and the meter's
   bar and label colours are defined ONCE, in app.css, and this file must not hold a second copy of
   them (R-I2-44.1, AC-I4-S2-15 — asserted by ConfidenceInkDefinitionGuardTests over the tree). What
   is here is LAYOUT, which is genuinely per-context: the report renders this cell in a wide
   auto-layout table with the confidence in a column of its own, and the profile renders it inside a
   5.4rem fixed allocation with the confidence stacked under the number.

   IT DOES NOT MEET THE CASCADE TRAP I4-S1 SHIPPED, AND THE REASON IS WORTH STATING BECAUSE THE NEXT
   INK ADDED TO THIS CELL WILL. `.profile-section-players .members-table tbody td` — (0,2,2) — sets
   the cell's colour, and I4-S1's `td.gender-m` at (0,2,1) lost to it with the class list correct and
   the rule present. That was a rule ON THE td. `.vsexp .delta.over` is a rule on a CHILD span, and a
   direct declaration always beats an inherited value regardless of specificity — the td's colour is
   never in competition with it. Different mechanism, same evidence standard: AC-I4-S2-4 is read off
   a rendered page's computed colour, never off the class list.

   THE STACKED HOSTS ARE NOT REACHED BY ANY OF THIS. Every rule below is under
   `.profile-players-table-fit`, the view shape's class. The admin member modal and the kiosk render
   `.table-responsive` + `.members-table` with no `vs expected` cell at all (AC-I4-S2-13). */
.profile-section-players .profile-players-table-fit td.profile-players-vsexp .vsexp {
    /* app.css already stacks the cell (`flex-direction: column`, `align-items: flex-end`), which is
       what this tier wants; restated here so the table tier keeps its shape if the shared rule is
       ever re-based for the report's own layout. The gap is this tier's: the report's cell has a
       0.66rem inputs line under the delta and no meter, and 14px of bar wants a little more air than
       `line-height: 1.15` leaves. */
    gap: 0.1rem;
    max-width: 100%;
}

.profile-section-players .profile-players-table-fit td.profile-players-vsexp .conf {
    /* The meter's own gap, tightened for a 5.4rem allocation. The bars are 22px (4 x 4px + 3 x 2px)
       and the widest word is `Very low` — unreachable on this surface, because a row only renders a
       delta at or above PartnershipSettling.MinGamesTogether and the ladder cannot return VeryLow
       there even demoted, but sized for anyway rather than sized to the values a fixture happens to
       produce (R-I2-39.1's longest-value discipline). */
    gap: 0.25rem;
}

/* ---- #669: THE LABEL TAKES THE LEVEL WORD'S SLOT — it does not get a slot of its own ----

   THE OWNER'S DEFECT. `▮▮▮▯ Good` inside the `vs expected` cell reads as how GOOD the partnership
   was. On /reports/partnerships the identical meter reads correctly, because there it sits under a
   column header saying CONFIDENCE; here it was put inside another column's cell and lost its header.

   WHY THE WORD GOES AND THE LABEL COMES, RATHER THAN BOTH. Measured on the rendered page, at the
   meter's own 0.68rem:

     table tier, 1280px    the cell allows 66.8px of content (5.4rem − 2 × 9.6px padding).
                           bars 22 + gap 4 + `Good` 26.63 = 52.63 today.
                           + `Confidence` 54.28 + gap  →  110.9px, 1.66× the allocation.
                           + `Conf` 22.67 + gap        →   79.3px, still 12.5px over.
     card tier, 360px      the figures line runs 268.88px inside 273.2px of row —  4.32px of slack.
                           ANY added item wraps the date to a fourth line; the card is held at three.

   So no form of label-AND-word fits, at either tier, and widening the column cannot save the card
   (its cells are inline-flex and never read the 5.4rem allocation at all). Swapping them does fit:
   `Conf` is 3.95px NARROWER than `Good`, so the card's line gets shorter and the table cell gains
   18.13px of headroom. The Owner's fallback, chosen on the measurement rather than in place of one.

   NOTHING IS LOST TO A SCREEN READER. The word stays in the DOM and is only taken out of the flow —
   the meter still renders `<small>Good</small>` for every host, which is also what keeps
   AC-I4-S2-1's shared-word assertion true here. Reading order is `Confidence` then `Good`.

   THE RULE IS SCOPED TO `.profile-players-table-fit`, the VIEW shape's class, exactly like every rule
   around it — so /reports/partnerships, the admin member modal and the kiosk are not reached. The
   report keeps its word beside its bars, under its own header, unchanged.

   NO COLOUR IS SET HERE (R-I2-44.1, AC-I4-S2-15). This is layout, which is the one thing this file is
   allowed to say about this cell. */
.profile-section-players .profile-players-table-fit td.profile-players-vsexp .conf small {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.profile-section-players .profile-players-table-fit thead th {
    /* The base rule sets `white-space: nowrap` for the stacked shape's wide panel. Here the headers
       are four short words and one two-word one; letting "Win %" wrap costs a line and buys width. */
    white-space: normal;
}

/* ---- SPEC-32 I2-S13d (#314): the CARD TIER's "Sort by" control ----

   Everything here is the control at rest: HIDDEN. The single rule that reveals it lives inside the
   card block below, beside the rule that hides `thead` — see that block for why the two must share
   one declaration site.

   THE CONTROL IS `.cs-seg` / `.cs-seg-btn`, the shipped segmented control, reused whole by class
   name exactly as the view strip reuses it. Nothing below redefines any of that control's own rules;
   these are a modifier, in the same relationship to it as `.cs-seg-sub` (the view strip's sub-strip).
   Two departures, both stated because both are load-bearing:

     * SMALLER TYPE AND TIGHTER SIDE PADDING. The strip holds four one-word labels; this holds SIX
       (Name · Games · W · L · Win % · Date) in the same 360px — five until SPEC-32 I3-S3 (#670) added
       the date column, whose control label is `Date` for exactly this reason. 0.88rem is the
       sub-strip's own size, so this is the control's existing denser setting rather than a new one.
     * `min-width: 0`. A flex item's default `min-width: auto` refuses to shrink below its content,
       so five segments that do not quite fit push the container wider and it grows a horizontal
       scroller — #614's defect exactly, on the tier that hides it best. This lets the segment shrink
       instead.

   THE BUTTON RESET'S SELECTOR IS `.profile-players-sortbar button`, UNSCOPED, AND THAT IS THE POINT.
   It is (0,1,1): it BEATS `.cs-seg-btn` (0,1,0), so the two overrides above apply, and it LOSES to
   `.cs-seg-btn.on` (0,2,0), so `background-color: transparent` neutralises the UA button chrome
   WITHOUT clobbering the white fill that marks the active segment.

   DO NOT ADD THE `.profile-section-players` PREFIX TO IT. Every other rule in this file carries that
   prefix and this one deliberately does not — with it the selector is (0,2,1), which OUTRANKS
   `.cs-seg-btn.on`, and the active segment silently loses its background while the markup keeps
   emitting `class="cs-seg-btn on"` and every render assertion stays green. That is exactly what was
   written first here and it was caught by counting, not by looking. The rules that DO carry the
   prefix (`-wrap`, `-label`, the seg's own margin) each override a `.cs-seg` (0,1,0) declaration or
   nothing at all, and none of them touches a property `.on` sets. */
.profile-section-players .profile-players-sortbar-wrap {
    display: none;
}

/* The caption's treatment is the CARD's own field label — same size, weight, case, tracking and ink
   as the `td::before` labels a few rules down — so the control reads as part of the card list rather
   than as chrome bolted above it. */
.profile-section-players .profile-players-sortbar-label {
    margin: 0 0 0.25rem;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: #60778f;
}

/* The control sits directly above the cards it orders, so it carries a card-sized gap rather than
   the strip's 1.5rem, which is spacing for a page-level choice. */
.profile-section-players .profile-players-sortbar {
    margin-bottom: 0.6rem;
}

.profile-players-sortbar button {
    /* SPEC-32 I3-S3 (#670): THE SEGMENTS SIZE TO THEIR LABELS INSTEAD OF SHARING THE BAR EQUALLY, and
       this is what pays for the sixth one.

       `.cs-seg-btn`'s `flex: 1 1 0` gives every segment the same width. That is right for the view
       strip — four labels of similar length — and it is wrong here, where the labels are `W` (13.61px)
       and `Games` (43.2px). Equal sixths at 360px are 44px each, of which 37.6px is content: measured,
       `Games` needed 49px and `Win %` broke across two lines, which took the whole control from 54px
       to 71.41px tall and put a label's overflow under its neighbour. Sized to content the same six
       need 256px of the 292px available, because `W` and `L` stop holding 44px each to say one letter.

       `nowrap` is the pair to it: under shrink, `Win %` is the one label with a break opportunity, and
       breaking it after `Win` is the failure above arriving by a different route.

       `min-width: 0` STAYS, and it now matters more, not less. Content-sized items still shrink when
       the bar is narrower than their total; without it they would refuse, push the container wider and
       grow a horizontal scroller — #614's defect on the tier that hides it best.

       `1 1 auto` AND NOT `0 1 auto`: the basis is the label, so the segments are proportional to what
       they say, and the grow factor is what still spreads them across the whole bar. With `0` they
       would pack left and leave 220px of empty control at 720px, which reads as a broken strip. */
    flex: 1 1 auto;
    white-space: nowrap;
    appearance: none;
    -webkit-appearance: none;
    border: 0;
    background-color: transparent;
    font-family: inherit;
    font-size: 0.88rem;
    padding: 0.6rem 0.2rem;
    min-width: 0;
    cursor: pointer;
}

/* The strip's segments are links and take the UA's link focus ring; these are buttons whose UA ring
   `appearance: none` can suppress, and they REORDER THE READER'S LIST — so the ring is stated, and
   it is the one the sibling header control already uses. */
.profile-players-sortbar button:focus-visible {
    outline: 2px solid #0d6efd;
    outline-offset: 2px;
}

/* The indicator, driven from `data-sort-state` — which the script writes from the header's
   `aria-sort` and nowhere else, so this glyph and the announced state are one fact. The header's
   neutral ↕ has no counterpart here on purpose: it answers "is this clickable at all", which inside
   a group captioned `Sort by` is already answered, and five copies of it would spend width this tier
   has least of.

   No `display` is set on the span, deliberately. It is a flex item of the button, so any inline
   value would be blockified and a rule asserting one could never hold. */
.profile-section-players .profile-players-sortbar-ind {
    margin-left: 0.2rem;
    font-size: 0.7em;
    line-height: 1;
}

.profile-players-sortbar button[data-sort-state="ascending"] .profile-players-sortbar-ind::after {
    content: "\25B2"; /* ▲ */
}

.profile-players-sortbar button[data-sort-state="descending"] .profile-players-sortbar-ind::after {
    content: "\25BC"; /* ▼ */
}

/* ---- The phone tier: stacked cards, no sideways scrolling ----

   The shipped idiom, verbatim from MemberSessionHistoryTable's own reflow: the header row is hidden
   and every cell carries its own label via data-label. It is scoped to the VIEW shape, so the modal
   and the kiosk keep the table they have.

   IT WAS 720px AND SPEC-32 I3-S3 (#670) RAISED IT TO 820px, IN THE SAME EDIT THAT ADDED THE SIXTH
   COLUMN. R-I2-40.2: a table's reflow breakpoint tracks its own min-content and is raised — never
   lowered — by the slice that adds a column to it. The reason 720 was right at five columns and wrong
   at six is the SHELL, not the table: `.my-shell` widens at 821px, so between 721px and 820px these six
   columns render in the un-widened 504px column. Measured there (qa-perf-club-mk, 760px, 2026-09-02):
   478px of wrapper, 268.76px of it taken by the four numeric columns and 128px by the date, leaving
   81.24px for the name — which put `Perf Member 023` on THREE lines and took the row from 35px to 78px.
   No overflow, and still the band R-I2-39 says should not exist: the columns did not fit, they merely
   did not overflow.

   820 IS NOT A NEW NUMBER. It is the nights block's reflow and the shell's own widening breakpoint
   (`MyPerformance.razor.css` / `ClubmateProfile.razor.css`, `@media (min-width: 821px)`), so this table
   is now a table exactly where its container is wide enough to hold it and cards everywhere else. The
   principle in the sentence this replaces is unchanged and still governs — each block reflows where ITS
   OWN columns stop fitting, and the match block keeps 720px because its columns still do. What changed
   is which side of the line six columns sit on. */
@media (max-width: 820px) {
    /* SPEC-32 I2-S13d (#314): THE HEADERS GO AND THE CONTROL ARRIVES, IN ONE DECLARATION SITE.

       These two rules are a pair and are deliberately adjacent. The rule below is what takes the
       sort's only trigger away on a phone in portrait — that is the gap the Owner reported — and
       this one is what puts a trigger back. Splitting them across two media blocks, or moving either
       to a different breakpoint, produces a width with two controls or a width with none; keeping
       them here makes that impossible to do by accident.

       It drives the SAME sort: the control's buttons call sortableTable.sortBy with the same
       arguments a header click passes, so there is one engine and one order at every width. */
    .profile-section-players .profile-players-sortbar-wrap {
        display: block;
    }

    .profile-section-players .profile-players-table-fit thead {
        display: none;
    }

    .profile-section-players .profile-players-table-fit,
    .profile-section-players .profile-players-table-fit tbody,
    .profile-section-players .profile-players-table-fit tbody tr {
        display: block;
        width: 100%;
    }

    .profile-section-players .profile-players-table-fit tbody tr {
        border: 1px solid #ccdae8;
        border-radius: 0.5rem;
        background: #ffffff;
        padding: 0.45rem 0.65rem;
        margin-bottom: 0.5rem;
    }

    /* The NAME is the card's title line: full width, no label, larger. It is the scan target, and
       labelling it "PARTNER" on a card inside a view already headed "Partners" is a third statement
       of the same word. */
    .profile-section-players .profile-players-table-fit tbody td:first-child {
        display: block;
        width: 100%;
        border-bottom: none;
        padding: 0 0 0.2rem;
        font-weight: 600;
        font-size: 0.95rem;
    }

    /* The four figures flow inline on the card's second line rather than stacking into four rows of
       their own — one card of three lines instead of five, which is what keeps 25 cards a page a
       reader can scroll rather than a document. */
    .profile-section-players .profile-players-table-fit tbody td:not(:first-child) {
        display: inline-flex;
        align-items: baseline;
        gap: 0.25rem;
        width: auto;
        border-bottom: none;
        padding: 0 0.75rem 0 0;
        white-space: nowrap;
        text-align: left;
        font-size: 0.85rem;
    }

    .profile-section-players .profile-players-table-fit tbody td:not(:first-child)::before {
        content: attr(data-label);
        color: #60778f;
        font-size: 0.7rem;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.03em;
    }

    /* SPEC-32 I2-S13b (#314), re-homed by I3-S3 (#670): the date sits at the END of the figure line —
       §5.6's `58 won, 34 lost · 12 Aug` — and it is BARE AFTER A MIDDOT rather than labelled.

       THE SPLIT IS R-I2-37.1's, and it is the reason this cell is the one exception on the card. Every
       other cell here takes `data-label` as an uppercase field label, because those are figures a
       reader COMPARES down a column of cards. The date is a field a reader CONFIRMS once they have
       found the row — one date on the card, nothing to compare it against — so it takes the muted,
       unlabelled treatment ux-plan §5.6 draws. (The same split puts a LABEL on `vs expected` when
       I3-S4 adds it: a signed number in a line of numbers is a puzzle without one.)

       THE MIDDOT IS THE LABEL SLOT, NOT AN EXTRA ELEMENT. `content` overrides `attr(data-label)` from
       the rule above, so the card gets §5.6's separator, the table tier gets neither, and there is no
       markup that exists for one tier only. `data-label` stays on the cell: it is what the stacked
       hosts and any future tier would present, and dropping it would leave a cell that cannot label
       itself.

       FOUR OF THESE FIVE DECLARATIONS ARE OVERRIDES AND EVERY ONE IS LOAD-BEARING — WHICH INVERTS THE
       NOTE THAT STOOD HERE. When this was a `<span>` INSIDE the cell, `font-weight`/`text-transform`/
       `letter-spacing` were inert: the label typography lives on `td::before`, and a CHILD cannot
       inherit from a sibling pseudo-element. The middot IS that pseudo-element now, so the same three
       declarations are what stop the separator rendering as a bold, uppercase, letter-spaced 0.7rem
       glyph. Do not prune them by the old reasoning — check which element the rule is on first.

       The colour is load-bearing on the CELL as it was on the span, and gated the same way: the
       geometry probe reads the cell's inherited ink where the rule does not apply, so
       `rgb(96, 119, 143)` here is the rule and not the cell's default. */
    /* SPEC-32 I4-S2 (#670): ON THE CARD THE CELL IS A ROW, NOT A COLUMN — and that one declaration is
       the whole of what keeps the card at three lines.

       app.css stacks `.vsexp` (delta above its sub-line), which is right on the table tier and wrong
       here: the card's figures line is `display: inline-flex; align-items: baseline`, so a stacked
       cell would make ONE cell two lines tall and take the whole line with it. Laid out in a row, the
       confidence sits beside the number as `VS EXPECTED +12 ▮▮▯▯ Good`, and the line wraps as a line
       rather than growing as a box.

       LINES ARE THE SCARCE DIMENSION HERE (R-I2-39), on two UN-PAGED views where one line is one line
       per counterpart to a 500-row ceiling. § 4.1's ordered card fallback — confidence to the date
       line, then bars without the word, then a fourth line — is what applies if the measurement says
       this does not hold; the figures are recorded in spec § 2.3 either way. */
    .profile-section-players .profile-players-table-fit tbody td.profile-players-vsexp .vsexp {
        flex-direction: row;
        align-items: baseline;
        gap: 0.3rem;
    }

    .profile-section-players .profile-players-table-fit tbody td.profile-players-date {
        color: #60778f;
    }

    .profile-section-players .profile-players-table-fit tbody td.profile-players-date::before {
        content: "\00B7";
        color: inherit;
        font-size: inherit;
        font-weight: 400;
        text-transform: none;
        letter-spacing: normal;
    }
}

/* ---- account ---- */
/* Member ID spans the FULL account-card width — behaviour-preserved from the old tabbed layout's
   `.member-profile-id-field { grid-column: 1 / -1 }`. */
.profile-member-id-field {
    grid-column: 1 / -1;
}

/* SPEC-32 fix (CHANGE 3, Owner decision): de-emphasize the Member ID. Always shown, at the bottom of
   the (admin-only) Account section, but as QUIET reference data — not a headline. Render label + value
   on one small, muted, single line rather than the prominent dt/dd field the `.profile-fields` grid
   gives by default (0.68rem uppercase label over a 0.95rem #253f5b value). These overrides win on
   specificity over `.profile-fields dt|dd`. No visibility/lens change (this section is admin-only). */
.profile-account-fields .profile-member-id-field {
    display: flex;
    align-items: baseline;
    gap: 0.35rem;
    margin-top: 0.6rem;
}

.profile-account-fields .profile-member-id-field dt,
.profile-account-fields .profile-member-id-field dd {
    margin: 0;
    font-size: 0.75rem;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: #60778f; /* muted grey, same low-contrast family as the section subheads / help text */
}

.profile-account-fields .profile-member-id-field dd {
    font-variant-numeric: tabular-nums;
}

/* Section-heading chrome unification (ux-review Major 2): Account + Manage carry the SAME shared
   eyebrow-band header as every other section, so their legacy inner headings are redundant — hide
   them so the title isn't doubled. */
.profile-section-account .member-account-status-caption,
.profile-section-manage .member-manage-title {
    display: none;
}

/* #606: tap-to-enlarge. The trigger keeps the avatar's exact box — it wraps the <img>, so it must
   not introduce its own line-height gap or shrink under flex. It is an anchor (see
   js/photoLightbox.js for why), so the underline and inherited link colour are cleared. */
.profile-identity-photo-link {
    display: block;
    flex: 0 0 auto;
    line-height: 0;
    border-radius: 50%;
    text-decoration: none;
    cursor: zoom-in;
}

/* The focus ring is on the LINK, not the image, so it traces the circle rather than a square. */
.profile-identity-photo-link:focus-visible {
    outline: 3px solid #2f6f9c;
    outline-offset: 2px;
}

/* The lightbox itself. Built by js/photoLightbox.js and appended to <body>, so it is styled here
   globally rather than in any one component's scoped sheet. */
.photo-lightbox {
    padding: 0;
    border: none;
    background: transparent;
    max-width: min(92vw, 40rem);
    max-height: 92vh;
    overflow: visible;
}

.photo-lightbox::backdrop {
    background: rgba(12, 20, 30, 0.82);
}

/* The whole point of the item: the image is shown UNCROPPED. object-fit: contain inside a
   viewport-bounded box, never the cover-crop every other render site applies. */
.photo-lightbox-img {
    display: block;
    max-width: min(92vw, 40rem);
    max-height: 92vh;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: 0.75rem;
    background: #0c141e;
}

.photo-lightbox-close {
    position: absolute;
    top: -0.75rem;
    right: -0.75rem;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    border: none;
    background: #fff;
    color: #253f5b;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
}

.photo-lightbox-close:focus-visible {
    outline: 3px solid #2f6f9c;
    outline-offset: 2px;
}

/* On a phone the negative-offset close button would sit off-screen against the viewport edge. */
@media (max-width: 30rem) {
    .photo-lightbox-close {
        top: 0.5rem;
        right: 0.5rem;
    }
}

/* ---- the match record's paging chrome (SPEC-32 I2-S4, #314) ----

   GLOBAL, NOT COMPONENT-SCOPED, and this file is where the reason is already written twice: Blazor
   CSS isolation stamps a scope attribute on every rule, so a rule defined in a component stylesheet
   that is not on the profile render path silently does not apply while the class list looks perfect.
   `.club-help-text` and `.profile-section-retry` both shipped that way. Every value below is verified
   with getComputedStyle on the rendered page, at both breakpoints. Presence is not application.

   NO NEW VISUAL LANGUAGE. The tokens are the ones already in this file: #006bb7 (the retry link's
   blue, also the head-to-head accent), #47586b (the section body text), #5f6f83 (the sub-label). */

/* The total, in the section head's right-hand side. It renders ONLY where a route to the rest is also
   offered, so it is never a number the reader cannot act on. */
/* SPEC-32 I2-S9 (#314): THE NIGHTS BLOCK'S CHROME IS ADDED TO THESE SELECTOR LISTS, NOT COPIED BELOW
   THEM. One declaration block governs both blocks, so they cannot drift — the same reasoning R-I2-14
   applies to markup, applied to style. The nights chrome shipped its first build with NO rules at all:
   a default <p> total, a default blue underlined route and an unstyled pager, immediately above a fully
   styled Matches block on the same page, on all four hosts. Caught at review, not by our own geometry
   gate, which read computed style for the pre-existing reveal control only. */
/* SPEC-32 I2-S13 (#314): the two COUNTERPART blocks join the same selector lists, for the reason the
   nights block did — one declaration governs three blocks, so they cannot drift.

   I2-S13c (#314): the count line is the ONLY piece of this chrome the counterpart blocks still wear.
   Their position line and their pager left with the paging, so `.profile-players-position`,
   `.profile-players-pager`, `.profile-players-newer/-older` and `.profile-players-caret` have been
   removed from the selector lists below rather than left to style markup nothing emits. It no longer
   states the ORDERING either: the column sort is back (R-I2-30 stops biting once every row renders),
   so the order is the reader's to change and a fixed sentence claiming one would go false on the
   first click. `aria-sort` carries it now. */
.profile-section-head .profile-matches-total,
.profile-section-head .profile-history-total,
.profile-section-head .profile-players-count {
    margin: 0.15rem 0 0;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    color: #5f6f83;
    font-variant-numeric: tabular-nums;
}

/* "Showing 51-100 of 412". ABOVE the rows deliberately: after a full document reload this is the
   first thing under the reader's eye, confirming the round-trip did what they asked.

   THE TWO PAGED BLOCKS ONLY. The counterpart views render every row (I2-S13c), so they have no
   position to state and their selector is gone from this list. */
.profile-section-body .profile-matches-position,
.profile-section-body .profile-history-position {
    margin: 0 0 0.6rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: #47586b;
    font-variant-numeric: tabular-nums;
}

.profile-section-body .profile-matches-actions,
.profile-section-body .profile-history-actions {
    margin: 0.75rem 0 0;
}

/* SPEC-32 I2-S9 rework 2 — ONE CONTROL AT A TIME ON THE STACKED TIER, AND ONE TAP MUST MOVE SOMETHING.

   The admin modal and the kiosk carry BOTH the table's in-place reveal ("Show 10 more", the bordered
   pill) and this host's widen button ("Show more nights", plain blue text), 56px apart, from first
   load. They read as two ways to do one thing, and they are not: the reveal discloses rows ALREADY IN
   THE DOCUMENT, the widen fetches the NEXT BOUND from the server. Pressing the widen first — the more
   action-sounding of the two — widened the read from 12 to 24 and changed nothing the reader could
   see, because the reveal was still collapsed at two rows; the single observable effect was the OTHER
   button's label going from "Show 10 more" to "Show 22 more". Reaching more rows needed both, in an
   order nothing communicated.

   They are consecutive steps of one disclosure, so they are shown in that order. While the document
   still holds rows this reader has not been offered, the reveal is the route to more nights and this
   control is not rendered; once they are all disclosed the widen appears, and every press from then on
   fetches rows that become visible immediately (the toggle stays checked across the re-render).

   THE SELECTOR IS SAFE BY CONSTRUCTION, WHICH IS WHY IT IS EXPRESSED THIS WAY.
     * It is a SIBLING rule off the reveal's own checkbox, so it can only suppress the widen where a
       reveal actually exists to take its place. The argument is structural, not a matter of which
       row counts occur: a block whose rows do not exceed the window renders NO checkbox, and with no
       checkbox there is nothing for the combinator to match — the rule is inert and the widen stands.
       That holds for every possible pairing of delivered rows and total, so there is no state in
       which this strands a reader.
     * It names .profile-history-actions-route, NOT .profile-history-actions: the portal pages' "See
       all N nights" link wears the latter, is the only route those hosts have, and must never be
       suppressed. Two different controls in two branches of one component; only one of them competes
       with the reveal.
     * It lives HERE, in the global sheet, and not in MemberSessionHistoryTable.razor.css, because it
       crosses a component boundary: the checkbox is that component's and this <p> is
       ProfileHistorySection's, so under CSS isolation the scope attribute would be stamped on a
       selector no element carries and the rule would silently never apply. Presence is not
       application, and the geometry gate reads this one's computed display for that reason.
     * Stacked tier only, at the same 820px the reveal and the card reflow use. Above it the reveal is
       display:none, the response IS the bound, and this control is the only route there is. */
@media (max-width: 820px) {
    .member-session-history-more-toggle:not(:checked) ~ .profile-history-actions-route {
        display: none;
    }
}

.profile-matches-expand,
.profile-matches-collapse,
.profile-matches-clear,
.profile-matches-newer,
.profile-matches-older,
.profile-history-expand,
.profile-history-collapse,
.profile-history-newer,
.profile-history-older,
.profile-history-widen {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: #006bb7;
    text-decoration: none;
    /* A 44px-tall target on touch, without moving the text: the pager is the primary action of the
       expanded state and is used with a thumb. */
    min-height: 2.75rem;
    padding: 0.15rem 0;
}

.profile-matches-expand:hover,
.profile-matches-expand:focus-visible,
.profile-matches-collapse:hover,
.profile-matches-collapse:focus-visible,
.profile-matches-clear:hover,
.profile-matches-clear:focus-visible,
.profile-matches-newer:hover,
.profile-matches-newer:focus-visible,
.profile-matches-older:hover,
.profile-matches-older:focus-visible,
.profile-history-expand:hover,
.profile-history-expand:focus-visible,
.profile-history-collapse:hover,
.profile-history-collapse:focus-visible,
.profile-history-newer:hover,
.profile-history-newer:focus-visible,
.profile-history-older:hover,
.profile-history-older:focus-visible,
.profile-history-widen:hover,
.profile-history-widen:focus-visible {
    text-decoration: underline;
}

/* THE WIDEN CONTROL IS A <button>, so it needs the UA chrome removed before the shared rule above can
   describe it. It is the ONLY route to the withheld nights on the admin modal and the kiosk — both
   used on a tablet — so the 2.75rem (44px) floor it inherits above is load-bearing, not cosmetic, and
   the geometry gate now asserts its measured height the way it already asserted the reveal's. */
.profile-history-widen {
    background: none;
    border: 0;
    font-family: inherit;
    cursor: pointer;
    text-align: left;
}

/* Quieter than the pager: leaving the expanded state is a secondary action beside "Older matches". */
.profile-matches-collapse,
.profile-history-collapse {
    font-weight: 500;
    color: #47586b;
}

.profile-matches-caret,
.profile-history-caret {
    font-size: 1rem;
    line-height: 1;
}

/* The two directions sit at opposite ends, so each is where a reader reaches for it — and, because
   the absent one is not rendered at all (never a disabled control), `space-between` alone would push
   a lone "Older" to the left on the first page. `margin-left: auto` pins it right in both cases.

   TWO BLOCKS, NOT THREE, SINCE I2-S13c: the counterpart views are un-paged and render no pager. */
.profile-section-body .profile-matches-pager,
.profile-section-body .profile-history-pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-top: 0.75rem;
    border-top: 1px solid #e4edf5;
    padding-top: 0.35rem;
}

.profile-matches-pager .profile-matches-older,
.profile-history-pager .profile-history-older {
    margin-left: auto;
}

/* ── SPEC-32 I2-S5 (#314): filtering the record by an individual ────────────────────────────────

   IN THE GLOBAL SHEET, NOT A SCOPED ONE, for the reason recorded at the top of this file: a rule in
   a component-scoped stylesheet that is not on the profile render path silently does not apply while
   every class list looks perfect. Presence is not application. Every value below is read back with
   getComputedStyle on the rendered page.

   NO NEW VISUAL LANGUAGE. The tokens are this file's own — #006bb7 (the route/pager blue), #47586b
   (body text), #5f6f83 (sub-label), #e4edf5 (the pager's rule). The disclosure behaves like
   SessionRecap's shipped "Filter by player": marker suppressed, a `›` chevron that rotates on
   [open], hover/focus-visible underline. Same control, same page family, same gesture. */

/* The active-filter bar: the state indicator, and the heading AC-I2-S5-2/-3 require. Above the
   position line and never inside the disclosure — a reader arriving from a full reload must be told
   whose record this is before they read a row. */
.profile-section-body .profile-matches-filterbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* SPEC-32 I2-S6 (#314): the bar now carries a THIRD child — the AC-I2-S6-5 summary — and it
       belongs on its own line under the heading rather than competing with it for the row. Wrapping
       with a full-width basis on that child keeps the shipped two-item row EXACTLY as it was at every
       width (heading left, 44px clear right) and puts the sentence beneath it, instead of introducing
       a wrapper element and re-laying-out a bar that has already been geometry-gated. */
    flex-wrap: wrap;
    gap: 0.75rem;
    margin: 0 0 0.6rem;
    padding: 0.5rem 0.35rem 0.5rem 0.7rem;
    border-left: 3px solid #006bb7;
    border-radius: 0.35rem;
    background: #f2f7fc;
}

.profile-matches-filterbar-label {
    margin: 0;
    font-size: 0.95rem;
    font-weight: 700;
    line-height: 1.25;
    color: #16324f;
}

/* A real 44px target, because it is an icon and there is nothing else to hit. The glyph stays small;
   the box around it does not. */
.profile-matches-filterbar-clear {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    min-width: 2.75rem;
    min-height: 2.75rem;
    font-size: 1.35rem;
    line-height: 1;
    color: #47586b;
    text-decoration: none;
    border-radius: 0.35rem;
}

.profile-matches-filterbar-clear:hover,
.profile-matches-filterbar-clear:focus-visible {
    color: #006bb7;
    background: #e2edf8;
}

/* SPEC-32 I2-S6 (#314): the arrival summary — how many matches the two have played and the record in
   them (AC-I2-S6-5). Full-width basis so it takes the second row of the bar; the negative top margin
   cancels the bar's own 0.75rem gap, which is sized for the horizontal pair and is too airy for a
   sentence sitting directly under its own heading.

   Quieter than the heading and darker than the scope line: it is the substance of the arrival, not a
   note about it. #3d5266 on #f2f7fc MEASURES 7.31:1 — computed, not remembered (the same correction
   the head-to-head block's comments carry). */
.profile-matches-summary {
    flex: 1 0 100%;
    margin: -0.4rem 0 0;
    font-size: 0.9rem;
    line-height: 1.45;
    color: #3d5266;
}

/* SPEC-32 I2-S6 (#314): a counterpart's name as a control, in both aggregate tables (AC-I2-S6-1/-2).

   IT IS NOT STYLED AS A BUTTON AND IT KEEPS THE CELL'S SIZE. Making the name the control was chosen
   precisely so the tables keep their shape and their click-to-sort, and cost no horizontal room on a
   phone where they are already tight — so this adds colour and an underline on interaction, and
   nothing that changes the row's height or the column's width. A row whose person is a guest or has
   left renders the same text with none of this, and that difference is deliberately quiet: the reader
   has no rule to be told about (ux-plan §9.4). */
.profile-players-link {
    color: #006bb7;
    text-decoration: none;
}

.profile-players-link:hover,
.profile-players-link:focus-visible {
    text-decoration: underline;
}

/* SPEC-32 I4-S1 (#670), R-I2-43: the counterpart's name, inked by their gender.

   ONE DECLARATION SITE, FOUR HOSTS, BOTH TIERS (AC-I4-S1-1, R-I2-42). This is the GLOBAL profile
   stylesheet, linked once in App.razor's <head>, so the self profile, a clubmate's profile, the admin
   member modal and the shared-tablet kiosk all reach it — which is precisely what a component-scoped
   `.razor.css` could not do, and is why the report's own copies of these values were moved out to
   app.css rather than duplicated here (R-I2-44.1). There is no host branch and no lens branch: the
   Owner ruled "colour as gender is fine everywhere", and the visibility model had never gated it —
   gender is in the Identity group, default Public, no admin toggle, because it is the roster.

   THE CLASS IS ON THE CELL AND THE INK REACHES THE NAME BY INHERITANCE. A roster-visible member's
   name is an <a class="profile-players-link"> and everyone else's is a bare text node, so the cell is
   the only element both shapes have. The `inherit` rule below is what makes AC-I4-S1-5 true: without
   it the link rule above would win on the link and lose on the plain text, and COLOUR WOULD ENCODE
   MEMBERSHIP STATUS rather than gender — two facts on one channel, one of them inferred, neither
   legible (R-I2-43.3). The control affordance is carried by the things that already carry it: the
   href, the hover underline, the focus ring.

   NO RULE FOR THE UNSTATED CASE, AND THAT IS THE POINT (AC-I4-S1-4). `Gender.Unspecified` and a
   counterpart resolving to `Unknown player` emit NO class, so the name takes the table's own
   inherited ink and is byte-identical to what shipped before this slice. A third colour here would
   publish a hole in the club's record on every row that has one.

   THE TWO VALUES ARE NOT WRITTEN HERE. They are app.css :root tokens, read by this file and by
   /reports/partnerships, so the two surfaces cannot drift (R-I2-32, R-I2-44). Do not inline them, and
   do not repoint them at --gender-male / --gender-female: that is the FILL pair, and the swap halves
   the contrast — 9.0:1 and 9.8:1 become 5.3:1 and 5.0:1. See #680.

   ── THE FULL SELECTOR CHAIN IS LOAD-BEARING AND MUST NOT BE SHORTENED. ──────────────────────────

   `.profile-section-players td.gender-m` is the obvious way to write this and it renders NO COLOUR.
   Two classes and one element is (0,2,1); the cell's shipped ink is set by
   `.profile-section-players .members-table tbody td` — two classes and TWO elements, (0,2,2) — which
   is declared above and wins. The class list looks perfect in the markup and the name renders in the
   table's ordinary ink, silently.

   THIS IS NOT A HYPOTHETICAL: it is how this rule was first written, and the browser pass caught it.
   It is also exactly why AC-I4-S1-2 asks for the COMPUTED colour off a rendered page rather than a
   comparison of two literals in two stylesheets — a stylesheet-text check would have passed on the
   broken version, and so would every render test in the tree, because the markup was already right.
   Spec32GenderInkDefinitionGuardTests computes both selectors' specificity from this file so the
   shortening cannot come back without a named failure.

   The `color: inherit` on the link is under the same chain for the same reason, and additionally has
   to out-rank `.profile-players-link` (0,1,0) declared above — which is what stops the ink applying
   to a guest's plain name and not to a member's linked one (AC-I4-S1-5). */
.profile-section-players .members-table tbody td.gender-m {
    color: var(--gender-name-male);
}

.profile-section-players .members-table tbody td.gender-f {
    color: var(--gender-name-female);
}

.profile-section-players .members-table tbody td.gender-m .profile-players-link,
.profile-section-players .members-table tbody td.gender-f .profile-players-link {
    color: inherit;
}

/* SPEC-32 I2-S6 (#314): the head-to-head line's per-direction action (AC-I2-S6-4). One per sentence,
   directly under the number it opens, so which figure it belongs to is a matter of position rather
   than of reading. 2.75rem of vertical box on a link that renders as text: the touch floor is met by
   padding, not by making it look like a button on a card that is deliberately quiet. */
.profile-section-h2h .profile-h2h-actions {
    margin: 0.15rem 0 0;
}

.profile-h2h-action {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
    min-height: 2.75rem;
    font-size: 0.9rem;
    font-weight: 600;
    color: #006bb7;
    text-decoration: none;
}

.profile-h2h-action:hover,
.profile-h2h-action:focus-visible {
    text-decoration: underline;
}

.profile-h2h-caret {
    font-size: 1.05em;
    line-height: 1;
}

.profile-section-body .profile-matches-filter {
    margin: 0 0 0.7rem;
    font-size: 0.85rem;
}

.profile-matches-filter-summary {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    min-height: 2.75rem;
    padding: 0.15rem 0.1rem;
    font-weight: 600;
    color: #006bb7;
    cursor: pointer;
    list-style: none;
    border-radius: 0.3rem;
}

.profile-matches-filter-summary::-webkit-details-marker {
    display: none;
}

.profile-matches-filter-summary:hover,
.profile-matches-filter-summary:focus-visible {
    text-decoration: underline;
    outline: none;
}

.profile-matches-filter-caret {
    color: #7a93ab;
    font-size: 0.9em;
    transition: transform 0.15s ease;
}

.profile-matches-filter[open] .profile-matches-filter-caret {
    transform: rotate(90deg);
}

/* Bounded and scrollable: the busiest real member has hundreds of distinct counterparts, and an
   unbounded list would push the record itself off the screen the reader came for. */
.profile-matches-filter-panel {
    margin-top: 0.3rem;
    padding: 0.4rem;
    border: 1px solid #e4edf5;
    border-radius: 0.5rem;
    background: #fff;
    max-height: 18rem;
    overflow-y: auto;
}

.profile-matches-filter-group {
    margin: 0.5rem 0 0.15rem;
    padding: 0 0.5rem;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: #5f6f83;
}

.profile-matches-filter-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
}

.profile-matches-filter-opt {
    display: block;
    padding: 0.6rem 0.5rem;
    border-radius: 0.3rem;
    color: #16324f;
    text-decoration: none;
}

.profile-matches-filter-opt:hover,
.profile-matches-filter-opt:focus-visible {
    background: #eef4fb;
}

/* Weight AND colour, and `aria-current` in the markup beside them: colour alone is not a state
   signal a reader who cannot distinguish it can use. */
.profile-matches-filter-opt-on {
    font-weight: 700;
    color: #006bb7;
}

/* ── SPEC-32 I2-S12 (#314): the profile's VIEW STRIP ─────────────────────────────────────────────
   `.cs-seg` / `.cs-seg-btn` are the shipped segmented control from the player portal's Club Nights
   page (PlayerClubSessions.razor.css). These declarations are that control's, verbatim.

   WHY THEY ARE REPEATED HERE RATHER THAN REFERENCED. Blazor CSS isolation is per-component: the
   Club Nights copy compiles to `.cs-seg[b-xxxxxxxxxx]` and applies only inside that page's own
   markup. The profile's strip renders inside a SHARED component (ProfileViewTabs) hosted by two
   different pages, so it needs an unscoped rule — and this file is where the profile's unscoped
   rules live, for the reason stated at the top of it. Club Nights is unaffected either way: its
   scoped selector carries higher specificity than this one, and the declarations are identical, so
   the two can only ever agree. (Verified by reading getComputedStyle on BOTH strips.)

   Every property here is load-bearing, not decoration: `min-height: 2.75rem` is the 44px touch
   floor, `flex: 1 1 0` is what keeps four one-word labels equal at 360px without a horizontal
   scroller, and the `:hover` rule is PAIRED with `:focus-visible` over an `.on` class that carries
   the state — no hover-only affordance anywhere. */
.cs-seg {
    display: flex;
    gap: 0.25rem;
    background: #eef3f8;
    border: 1px solid #dde7f0;
    border-radius: 0.7rem;
    padding: 0.25rem;
    margin-bottom: 1.5rem;
}

.cs-seg-btn {
    flex: 1 1 0;
    text-align: center;
    padding: 0.6rem 0.4rem;
    min-height: 2.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 0.5rem;
    text-decoration: none;
    color: #47586b;
    font-weight: 600;
    font-size: 0.95rem;
    transition: background 0.12s ease, color 0.12s ease;
}

.cs-seg-btn:hover,
.cs-seg-btn:focus-visible {
    color: #16324f;
}

.cs-seg-btn.on {
    background: #fff;
    color: #0d2c55;
    box-shadow: 0 1px 3px rgba(13, 44, 85, 0.12);
}

/* The two strips sit together, so the top one loses its bottom margin and the sub-strip carries the
   gap to the content below. One step down in visual weight — smaller type, no filled container —
   so the reader can tell the choice they made from the choice inside it (ux-plan §5.1).

   `scroll-margin-top` IS KEPT THOUGH NO LINK TARGETS `#profile-views` ANY MORE (Owner, 2026-09-01,
   second report — every view link now carries NO fragment; ProfileViews.Href carries the reasoning).
   The `id` is still in the markup, so an arrival at it is still possible — a link made during the one
   day that behaviour was live, or a typed fragment — and PlayerShellLayout's top bar, which hosts BOTH
   profile pages, is `position: sticky; top: 0` with `min-height: 3.5rem`. Without the margin such an
   arrival parks the strip exactly underneath the bar. One declaration, correct for every arrival at an
   id that exists; removing it would leave the one landmark on the page that can still take focus as
   the one that lands hidden.

   IT IS IN `rem`, WHICH IS WHY IT SURVIVES BROWSER ZOOM. The bar's own height is 3.5rem, so both
   scale together and the clearance holds at 100% and 125% alike — a `px` value would go wrong in one
   direction at every zoom step.

   THE VALUE IS NOT SHARED WITH THE BAR AS A VARIABLE because the bar's height lives in
   PlayerShellLayout's SCOPED stylesheet, which this unscoped file cannot see; the coupling is stated
   here in words instead. If that bar's min-height changes, this changes with it. */
.profile-view-seg {
    margin-bottom: 0.5rem;
    scroll-margin-top: 4.5rem;
}

/* ── 2026-09-01: THE DEEP BOOKMARK, WHICH IS NOW THE ONLY FRAGMENT ARRIVAL THERE IS.
   `#history` and `#matches` are published links (AC-I2-S12-4 keeps them resolving) and the app's own
   links no longer use fragments at all, so every arrival at one of these ids is somebody's saved link.
   Those sections have never carried a scroll margin, so such an arrival has always landed tucked
   underneath the sticky top bar with the section's own heading hidden. Same bar, same 3.5rem, same
   ~1rem of breathing room, same reasoning as `.profile-view-seg` above — kept as a separate rule
   because the two are different elements that happen to share a number, not one fact.

   THE SELECTOR IS DERIVED, NOT A LIST OF FOUR IDS. A profile section carries an `id` exactly when it
   is a fragment target: ProfilePlayersSection emits none on the stacked hosts (`Only is null`), and a
   section that gains one later is covered without anyone remembering this rule exists. Listing
   `#history, #matches, #partners, #opponents` would be four strings duplicating four C# constants and
   would silently miss the fifth.

   IT IS INERT ON THE ADMIN MODAL AND THE KIOSK, which render these same sections: `scroll-margin`
   affects nothing until something scrolls the element into view, and on those two hosts nothing does —
   they have no URL to carry a fragment. */
.profile-section[id] {
    scroll-margin-top: 4.5rem;
}

.cs-seg-sub {
    background: transparent;
    border-color: transparent;
    padding: 0;
    gap: 0.15rem;
}

.cs-seg-sub .cs-seg-btn {
    font-size: 0.88rem;
    font-weight: 600;
    padding: 0.5rem 0.2rem;
}

.cs-seg-sub .cs-seg-btn.on {
    background: #eef3f8;
    box-shadow: none;
}

/* ── SPEC-32 I2-S12 (#314): the summary view's route into the record, and the ruled private lines.

   THE ROUTE IS THE MITIGATION FOR THE ONE THING THIS RESTRUCTURE COSTS: five views mean up to five
   reads where there was one, and that trade is only honest if the summary answers the question asked
   on most visits and offers a deliberate trip to the rest (R-I2-25.4). */
.profile-view-route {
    margin: 0.25rem 0 0;
}

.profile-view-route-link {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    min-height: 44px;
    color: #006bb7;
    text-decoration: none;
    font-weight: 600;
    font-size: 0.95rem;
}

.profile-view-route-link:hover,
.profile-view-route-link:focus-visible {
    text-decoration: underline;
}

/* Three absences, three sentences, and none reached by falling through from another (R-I2-12). This
   is the HIDDEN one — the club's own choice — and it is a statement, not an error, so it carries no
   role="alert" and none of the error styling. */
.profile-view-private {
    margin: 0 0 1rem;
    color: #5e748a;
    font-size: 0.95rem;
}

/* SPEC-32 I2-S12 (#314), AC-I2-S12-7: the route back out of a drill-down, inside the filter bar.
   Styled as the shipped back-link family (`.my-back`) rather than as a pager control: it leaves the
   view rather than moving within it, and it must read as the way back, not as another page. The 44px
   floor is the same one every other control on this surface carries. */
.profile-matches-back {
    margin: 0.5rem 0 0;
}

.profile-matches-back-link {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    min-height: 2.75rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: #006bb7;
    text-decoration: none;
}

.profile-matches-back-link:hover,
.profile-matches-back-link:focus-visible {
    text-decoration: underline;
}

/* ---- the summary view's figure cards + latest-night panel (SPEC-32 I2-S15, #314) ----

   GLOBAL, LIKE EVERY OTHER RULE IN THIS FILE, AND THE REASON IS THE I2-S1b LESSON RATHER THAN
   HOUSEKEEPING. Blazor CSS isolation is per component: a rule in ProfileOverviewSection.razor.css
   would apply only to markup that component emits, and the moment any of this is reused — or the
   markup moves one component up into ProfileRenderer — it silently stops applying while every class
   list and every DOM assertion still looks correct. `.club-help-text` and the retry link were both
   inert on this exact render path for that reason. Presence is not application; these values are
   verified with getComputedStyle on the rendered page, not by reading the class list.

   NO NEW VISUAL LANGUAGE. Each card is the shipped `.profile-section` box (same border, radius,
   background) with a compact body of its own — the label/value/sub stack, not the head+body band,
   because a 44px head band on a card whose whole content is three short lines is chrome for its own
   sake at 360px. */
.profile-figures {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.6rem;
    min-width: 0;
}

/* ux-plan §5.2: "above 720px the four cards become a single row of four". A long subject name WRAPS
   the label and the row gets taller, which is the allowed answer. It must never introduce a horizontal
   scroll (the #614 rule), which is what `minmax(0, 1fr)` above and `min-width: 0` here are for.

   THE "FOUR ~117px CARDS" THIS COMMENT USED TO PROMISE ARE GONE, AND THEY WERE A CONSEQUENCE, NEVER A
   DESIGN. It said the content column is "504px at EVERY viewport (`.my-shell { max-width: 34rem }`), so
   this is four ~117px cards and not a laptop-width row". SPEC-32 I3-S0 (#677) retires that premise on
   the Owner's width ruling: above 821px the profile's shell caps at 53rem, so the column is 808px and
   these are four 194.8px cards (measured at 1280px, 2026-09-02, rather than divided out — the `gap`
   is part of the answer).

   THE CAP IS A MEASUREMENT AND IT MOVES, SO DO NOT REST A NEW ARGUMENT ON THE NUMBER. It was 49rem for
   one slice: I3-S0 measured it against the Games table, and I3-S1 (#669) put `At Club` back on the
   nights table, which took that table past Games and the cap to 53rem. The figures below are quoted
   from whatever the two lens stylesheets say — MyPerformance.razor.css / ClubmateProfile.razor.css
   carry the sweep — and a slice that moves the cap re-quotes them here, because the paragraph beneath
   this one reasons FROM the column width.

   NO CAP IS ADDED HERE, AND THE CHOICE IS DELIBERATE (R-I2-40.6). The requirement says a summary element
   that should not be stretched to a data width constrains ITSELF. What actually breaks when this grid is
   given room is the COUNT — eight across, or a horizontal scroll — and `repeat(4, minmax(0, 1fr))` is
   already the rule that forbids both. The card SIZE does not break: `.profile-figure-value` was stepped
   down to 1.45rem because "four of these sit side by side in a 504px column, where 2rem digits on a
   1,047 would not fit", so more room is the constraint easing, not a distortion. A `max-width: 31.5rem`
   here would hold the cards at their old size at the price of a 504px grid left-aligned under an 808px
   chart card — a ragged right edge on the one view whose job is to be read at a glance. The cards grow;
   the shell stays one width on all five views, which is what R-I2-40.6 is actually protecting. */
@media (min-width: 720px) {
    .profile-figures {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

.profile-figure {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 0.6rem 0.7rem;
    min-width: 0;
}

/* The card label. Matches `.profile-fields dt` — the shipped small-label token on this surface —
   rather than inventing a second one. #6b7c90 on #ffffff measures 4.90:1, clearing WCAG AA (4.5:1)
   at this size; `.profile-fields dt`'s #8493a6 is 3.16:1 and is used there only beside a value in a
   2-column grid, whereas this label is the card's only heading and has to carry it alone. */
.profile-figure-label {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #6b7c90;
    font-weight: 650;
    overflow-wrap: break-word;
}

/* The figure itself. Deliberately smaller than `.profile-rating-value` (2rem): four of these sit
   side by side in a 504px column, where 2rem digits on a 1,047 would not fit. Same colour, same
   weight family, same tabular numerals, so it reads as the same typographic idea one step down. */
.profile-figure-value {
    font-size: 1.45rem;
    font-weight: 750;
    letter-spacing: -0.02em;
    color: #253f5b;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

/* The not-known form. The em dash is a value, not a missing one, so it keeps the value slot's size
   and loses only the emphasis. */
.profile-figure-value-none {
    color: #8493a6;
    font-weight: 600;
}

/* The supporting line. #5f6f83 on #ffffff measures 5.14:1 — the same token `.profile-h2h-scope`
   already uses on this surface for exactly this job. */
.profile-figure-sub {
    font-size: 0.78rem;
    color: #5f6f83;
    overflow-wrap: break-word;
}

/* The settling disclosure inside the rating card: it sits under the value rather than beside it,
   because there is no room for both on a ~117px card at the four-across breakpoint. */
.profile-figure .profile-settling {
    margin-top: 0.1rem;
}

/* ---- the recent-nights excerpt (SPEC-32 I3-S2, #668) ----

   IT HAS NO RULES OF ITS OWN, AND THAT ABSENCE IS THE SLICE'S RESULT RATHER THAN AN OMISSION.

   This block held eight rules — `.profile-night-results`, `.profile-night-result`, its win and loss
   colours, `.profile-night-line`, its stacking margin and `.profile-night-rating-arrow` — a parallel
   visual language for the one night the summary used to state in bespoke markup. Their comment said
   in as many words that they were "same values, own names, and the two must be kept identical" as
   MemberSessionHistoryTable's, because that component's stylesheet is Blazor-scoped and its classes
   would have rendered UNSTYLED here while every DOM assertion passed (the I2-S1b lesson).

   I3-S2 makes the excerpt the SHIPPED nights table, so there is no second copy left to keep in step:
   the squares, the record line and the rating journey are that component's own, drawn by its own
   scoped stylesheet, which applies wherever the component renders. The values that had to be kept
   identical by hand are now the same values by identity. Deleted rather than left behind — a dead
   rule whose comment says it must match another is an invitation to a future divergence between two
   things that no longer both exist.

   The excerpt's own container needs nothing: `.profile-section` gives it the box, and
   `.profile-section .table-responsive` (top of this file) gives the table its scroll containment,
   both already applying to every block on this page. */
