/*
 * Copyright (C) 2026 Jugendstil, SLU - All Rights Reserved
 *
 * Unauthorized copying of this file, via any medium is strictly prohibited. Proprietary and confidential.
 */

/*
 * WePlay web components.
 *
 * Every selector in this file is prefixed `.wp-`, and there is not a single element selector or
 * reset. That is not style preference: the first consumer is weplay-app-web, which is a Bootstrap
 * 5 application with seventy-odd hand-written stylesheets, and a library that restyles `table` or
 * `input` would silently redecorate pages nobody asked it to touch. The prefix is what lets this
 * be adopted one component at a time instead of in one risky sweep.
 *
 * Values come from tokens.css, which is generated from weplay-design-tokens/weplay.tokens.json.
 * A number that appears literally in this file is a number that belongs to exactly one component;
 * the moment a second component needs it, it goes in the token file instead.
 *
 * Load order: tokens.css first, this second.
 */

/* ==================================================================== focus ring ==
 * One ring for everything that takes focus, declared with the component rather than with the
 * focused state of it.
 *
 * An outline follows its element's own `border-radius`, so a ring matches the shape it is drawn
 * round for free -- but only while the shape is the element's. Rules used to hand their radius
 * to the focused state instead (`.wp-search-clear` is the one left, a pill that only became a
 * pill once you tabbed to it), which is the one way an outline can come out the wrong shape:
 * the geometry was a property of the state, so the two could drift. It lives with the component
 * now and the state cannot reach it.
 *
 * `outline-style` is the switch and not `outline-color`. Declaring the ring transparent at rest
 * reads better, but forced-colors mode overrides every outline colour it finds, so a transparent
 * ring is painted in High Contrast at all times: a box round every row, tile and button on the
 * screen, with nothing focused. `none` paints nothing in any mode, and the colour stays
 * declared in exactly one place.
 *
 * The offset is per component because what is around a component differs: a row inside a card
 * and a tile inside a carousel are clipped by their container, so their ring goes inside their
 * own edge or it is cut in half; a button standing on the page can hold its ring just outside.
 * That is the same split the rules being replaced had, kept as it was.
 */
:where(
  .wp-button,
  .wp-field input:not([type='range']),
  .wp-field textarea,
  .wp-flyout-action,
  .wp-list-row,
  .wp-match,
  .wp-media,
  .wp-member,
  .wp-navbar-button,
  .wp-pagination-step,
  .wp-search input,
  .wp-search-clear,
  .wp-segmented a,
  .wp-segmented label,
  .wp-switch
) {
    outline: 2px none var(--wp-color-brand-primary);
    outline-offset: var(--wp-focus-ring-offset, 2px);
}

/* Inside its own edge: each of these is clipped by whatever holds it -- a row by its card, a
   match card by its group, a tile by the carousel's `overflow-y: hidden` -- and a ring outside
   the edge would be the part that gets cut. */
:where(.wp-list-row, .wp-match, .wp-media) {
    --wp-focus-ring-offset: -2px;
}

/* A pixel of clearance, which is all there is: each of these sits tight against a neighbour, a
   field's own border, or the track it slides in. */
:where(.wp-pagination-step, .wp-search input, .wp-search-clear, .wp-segmented a, .wp-segmented label) {
    --wp-focus-ring-offset: 1px;
}

/* The state, and the whole of it. A select is deliberately excluded further down: what it opens
   is drawn and placed by the browser, so a ring on the closed control marks somewhere the reader
   has already left. */
:where(
  .wp-button,
  .wp-field input:not([type='range']),
  .wp-field textarea,
  .wp-flyout-action,
  .wp-list-row,
  .wp-match,
  .wp-media,
  .wp-member,
  .wp-navbar-button,
  .wp-pagination-step,
  .wp-search input,
  .wp-search-clear,
  .wp-segmented a,
  .wp-segmented label,
  .wp-switch
):focus-visible {
    outline-style: solid;
}

/* A checkbox or radio hands its ring to the label beside it, which is the part with a shape. */
.wp-segmented-input:focus-visible + label {
    outline-style: solid;
}

/* ========================================================================== card ==
 * The inset-grouped card. `overflow: hidden` is what clips a child's first and last
 * row separators to the rounded corner, so a table can sit flush inside one.
 */
/* The backdrop a card sits on. Half the inset-grouped look is the card and half is the dimmer
   surface behind it: a white card on a white page is just a table with rounded corners nobody can
   see.
   Surface only, no padding. On iOS this is the screen, not a band around one list, so it belongs
   on whatever element owns the page's background and the spacing around the card stays with
   whatever is already laying the page out. */
.wp-grouped {
    background: var(--wp-color-surface-grouped);
    color: var(--wp-color-text-primary);
}

.wp-card {
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    overflow: hidden;
}

/* A whole card that is one link, which is most of them on the home screen. Resets the two
   things an <a> brings that a card must not show: the underline and the link colour. Not folded
   into .wp-card, because a card that is not a link may well contain one and that link should
   still look like a link. */
.wp-card-link {
    color: var(--wp-color-text-primary);
    display: block;
    text-decoration: none;
}

/* The padding a card's content sits in. Separate from .wp-card because a tile with a full-bleed
   banner across the top has none, and a table inside a card brings its own. */
.wp-card-body {
    padding: var(--wp-space-lg);
}

/* ======================================================================== avatar ==
 * A player's picture, clipped to the same squircle the iOS app uses: the path is
 * AvatarShape.swift's bezier, converted curve for curve rather than approximated with
 * a border-radius, so the two platforms draw one shape.
 *
 * The clip path lives in the `component/defs` fragment, which a page includes once.
 * Without it the avatar still renders, square: a missing clip-path reference is
 * ignored rather than fatal, which is the right failure.
 *
 * Note the shape inscribes itself in the middle ~83% of the box, so a 42px avatar
 * draws a ~35px visible squircle. That is faithful to iOS, where AvatarView does the
 * same. Change it in AvatarShape.swift and here together, or not at all.
 */
.wp-avatar {
    background: var(--wp-color-surface-chip);
    clip-path: url(#wp-avatar-shape);
    flex: none;
    height: var(--wp-size-avatar);
    object-fit: cover;
    width: var(--wp-size-avatar);
}

/* AvatarView's `borderColor`/`borderWidth`/`borderGap`: a squircle ring around the avatar with a
 * gap between the two.
 *
 * A wrapper rather than a box-shadow on the avatar itself, because `clip-path` clips an element's
 * shadows along with its content — the shadow was being drawn and then thrown away. So the ring is
 * two pseudo-elements carrying the same clip, which is also the only way to get a ring that is
 * curvature-continuous: a stroked squircle, not a circular arc.
 *
 * `::before` is the ring colour and `::after` covers all but its outer edge with the gap's fill,
 * which leaves a band of the ring colour showing. iOS strokes the inflated shape instead, centred
 * on the path, and the insets here are what puts the two filled shapes' edges where that stroke's
 * two edges land. The clip shape occupies 20/24 of its box, so a 1px inset of the box moves the
 * visible edge by only 20/24 of a pixel, and going the other way a visible offset of `gap` plus or
 * minus half the stroke needs a box inset of `gap ± 0.6 × width`. Keep the two calcs in step with
 * each other or the ring stops being the width it says it is.
 *
 * Both pseudo-elements sit behind the avatar on a negative stack level, which needs `isolation` to
 * keep them inside this element's own stacking context rather than behind the card.
 */
.wp-avatar-ring {
    --wp-avatar-ring-color: currentcolor;
    --wp-avatar-ring-gap: 3px;
    --wp-avatar-ring-gap-fill: transparent;
    --wp-avatar-ring-width: 1.5px;
    display: inline-flex;
    flex: none;
    isolation: isolate;
    position: relative;
}

.wp-avatar-ring::before,
.wp-avatar-ring::after {
    clip-path: url(#wp-avatar-shape);
    content: '';
    position: absolute;
}

.wp-avatar-ring::before {
    background: var(--wp-avatar-ring-color);
    inset: calc(-1 * (var(--wp-avatar-ring-gap) + 0.6 * var(--wp-avatar-ring-width)));
    z-index: -2;
}

.wp-avatar-ring::after {
    background: var(--wp-avatar-ring-gap-fill);
    inset: calc(-1 * (var(--wp-avatar-ring-gap) - 0.6 * var(--wp-avatar-ring-width)));
    z-index: -1;
}

/* What a player with no picture gets. An <img> with no src still draws the browser's broken-image
   glyph, so the stand-in has to be a different element rather than a background on the same one.
   Mirrors AvatarView's own placeholder on iOS. */
.wp-avatar-placeholder {
    align-items: center;
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    justify-content: center;
}

.wp-avatar-placeholder svg {
    height: 45%;
    width: 45%;
}

/* On the hero the placeholder sits on the brand gradient rather than on a grouped surface, and
   secondary ink there is a grey smudge on purple. It takes the card's own ink instead -- the same
   reason the card states a colour at all rather than letting one through from the page. */
.wp-faceoff .wp-avatar-placeholder,
.wp-hero .wp-avatar-placeholder {
    color: var(--wp-color-text-on-brand);
}

.wp-avatar-sm {
    --wp-size-avatar: 42px;
}

.wp-avatar-lg {
    --wp-size-avatar: 64px;
}

/* ==================================================================== icon badge ==
 * The rounded tile behind a menu row's icon. Neutral by design: these used to take a
 * per-row colour, which read as meaning that was not there.
 */
.wp-icon-badge {
    align-items: center;
    background: var(--wp-color-surface-icon-badge);
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-primary);
    display: inline-flex;
    flex: none;
    height: var(--wp-size-icon-badge);
    justify-content: center;
    width: var(--wp-size-icon-badge);
}

.wp-icon-badge svg {
    height: 18px;
    stroke-width: 2;
    width: 18px;
}

/* The tile in the brand's own tint, for the rows that are the point of the menu rather than its
   housekeeping: Play is where somebody is going when they open this, and Account is where they
   go once a year. One variant class rather than a hue per row -- the mock paints six -- because
   six colours in a column of fourteen rows is a legend the reader has to learn, and none of the
   hues means anything: there is no amber concept in this product for a trophy to belong to.
   Which rows take it is one class in the template, so the decision is a line to move. */
.wp-icon-badge-accent {
    background: color-mix(in srgb, var(--wp-color-brand-primary) 12%, transparent);
    color: var(--wp-color-brand-primary);
}

/* The same tile wearing WhatsApp's own colour, for a row that opens one of a club's groups.
   A circle rather than the rounded square, and the mark's green on a wash of itself, which is
   what the app draws. The one place a row icon carries a colour: it stands for somebody else's
   service rather than for a section of ours. */
.wp-icon-badge-whatsapp {
    background: color-mix(in srgb, var(--wp-color-brand-external-whatsapp) 15%, transparent);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-brand-external-whatsapp);
}

/* =================================================================== player name ==
 * First name secondary over last name primary, falling back to one line when either
 * half is missing. Shared so that every list showing a player reads the same way.
 */
.wp-name {
    min-width: 0;
}

.wp-name-first,
.wp-name-last,
.wp-name-single {
    display: block;
    font: var(--wp-font-style-subheadline);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wp-name-first {
    color: var(--wp-color-text-secondary);
}

/* Explicit rather than inherited: these two also appear outside .wp-member, e.g. a
   participant with no profiles, and a component has to be right wherever it is put. */
.wp-name-last,
.wp-name-single {
    color: var(--wp-color-text-primary);
    font-weight: 500;
}

/* The avatar-plus-name pair, as one focusable unit. */
.wp-member {
    align-items: center;
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-primary);
    display: flex;
    gap: var(--wp-space-sm);
    min-width: 0;
    text-decoration: none;
}

/* ================================================================ move indicator ==
 * Direction plus how far. Fixed width so a row that did not move still reserves the
 * column and every name below it stays on the same left edge.
 *
 * Colour is never the only signal: the chevron carries the direction on its own, for
 * the ~8% of men with a red/green deficiency and for anyone reading a printout.
 */
.wp-move {
    /* Local, not a token: exactly one component uses it. Declared on the component root
       the way the slider declares its track height, so an instance can override it
       without the library having to promote a one-off measurement to a shared decision. */
    --wp-move-column: 16px;
    align-items: center;
    display: inline-flex;
    flex-direction: column;
    width: var(--wp-move-column);
}

.wp-move svg {
    display: block;
    height: 11px;
    stroke-width: 2;
    width: 11px;
}

.wp-move span {
    font: var(--wp-font-style-caption-2);
    font-variant-numeric: tabular-nums;
}

.wp-move[data-direction='up'] {
    color: var(--wp-color-ranking-up);
}

.wp-move[data-direction='down'] {
    color: var(--wp-color-ranking-down);
}

/* ======================================================================== slider ==
 * A styled native <input type="range">, not a div rig: keyboard support, touch
 * dragging, screen-reader semantics, RTL and form submission all come for free and
 * cannot silently regress.
 *
 * UISlider's metrics, 1pt mapped to 1px. The track height, thumb size and shadow are
 * local rather than tokens because exactly one component uses them.
 */
.wp-slider {
    /* Set by the slider controller. The thumb's centre travels from half-a-thumb to
       width-minus-half-a-thumb, so a naive 0-100% fill drifts from the knob by up to
       14px at the ends. calc() mixing px with % is what welds the fill to the thumb
       centre at any container width, with no measuring and no resize listener. */
    --wp-slider-progress: calc(var(--wp-slider-thumb-size) / 2);
    --wp-slider-thumb-fill: #ffffff;

    /* White in both appearances: iOS does not invert the knob. */
    --wp-slider-thumb-shadow: 0 0.5px 4px rgb(0 0 0 / 0.12), 0 6px 13px rgb(0 0 0 / 0.12);
    --wp-slider-thumb-shadow-active: 0 0.5px 4px rgb(0 0 0 / 0.16), 0 8px 18px rgb(0 0 0 / 0.18);
    --wp-slider-thumb-size: 28px;
    --wp-slider-track-height: 4px;

    /* Vertical scrolling still works from on top of the track. */
    appearance: none;
    background: transparent;
    cursor: grab;
    display: block;
    height: var(--wp-size-touch-target);
    margin: 0;
    padding: 0;

    /* The track pseudo-element paints, not the input. */
    touch-action: pan-y;
    width: 100%;
}

.wp-slider:active {
    cursor: grabbing;
}

/* There is no ::-webkit-slider-progress, so the two-tone track is one hard-stop
   gradient. Deliberately not transitioned: a transition here makes the fill lag a
   finger drag. */
.wp-slider::-webkit-slider-runnable-track {
    background: linear-gradient(
            to right,
            var(--wp-color-brand-primary) 0,
            var(--wp-color-brand-primary) var(--wp-slider-progress),
            var(--wp-color-control-track) var(--wp-slider-progress),
            var(--wp-color-control-track) 100%
    );
    border-radius: var(--wp-radius-full);
    height: var(--wp-slider-track-height);
}

.wp-slider::-webkit-slider-thumb {
    appearance: none;
    background: var(--wp-slider-thumb-fill);
    border: 0;
    border-radius: var(--wp-radius-full);
    box-shadow: var(--wp-slider-thumb-shadow);
    height: var(--wp-slider-thumb-size);

    /* Centres the knob on the track: (track - thumb) / 2. */
    margin-top: calc((var(--wp-slider-track-height) - var(--wp-slider-thumb-size)) / 2);
    transition: transform 120ms ease-out,
    box-shadow 120ms ease-out;
    width: var(--wp-slider-thumb-size);
}

/* Firefox has a real ::-moz-range-progress and its own thumb inset already matches,
   so the gradient is neither needed nor wanted: ::-moz-range-track overrides it. */
.wp-slider::-moz-range-track {
    background: var(--wp-color-control-track);
    border: 0;
    border-radius: var(--wp-radius-full);
    height: var(--wp-slider-track-height);
}

.wp-slider::-moz-range-progress {
    background: var(--wp-color-brand-primary);
    border-radius: var(--wp-radius-full);
    height: var(--wp-slider-track-height);
}

.wp-slider::-moz-range-thumb {
    background: var(--wp-slider-thumb-fill);
    border: 0;
    border-radius: var(--wp-radius-full);
    box-shadow: var(--wp-slider-thumb-shadow);
    height: var(--wp-slider-thumb-size);
    transition: transform 120ms ease-out,
    box-shadow 120ms ease-out;
    width: var(--wp-slider-thumb-size);
}

/* 1.06, not the 1.15 a web slider usually reaches for. UIKit's knob barely moves
   under a finger; anything larger reads as a toy. */
.wp-slider:active::-webkit-slider-thumb {
    box-shadow: var(--wp-slider-thumb-shadow-active);
    transform: scale(1.06);
}

.wp-slider:active::-moz-range-thumb {
    box-shadow: var(--wp-slider-thumb-shadow-active);
    transform: scale(1.06);
}

/* iOS has no focus ring because iOS has no keyboard focus. The web does, so
   :focus-visible only: ring on a Tab, no ring on a mouse drag. */
.wp-slider:focus {
    outline: none;
}

.wp-slider:focus-visible::-webkit-slider-thumb {
    box-shadow: var(--wp-slider-thumb-shadow),
    0 0 0 4px color-mix(in srgb, var(--wp-color-brand-primary) 35%, transparent);
}

.wp-slider:focus-visible::-moz-range-thumb {
    box-shadow: var(--wp-slider-thumb-shadow),
    0 0 0 4px color-mix(in srgb, var(--wp-color-brand-primary) 35%, transparent);
}

/* The fill goes flat rather than the whole control going translucent: opacity on the
   host washes the thumb shadow into a grey smudge. */
.wp-slider:disabled {
    --wp-color-brand-primary: var(--wp-color-control-track);
    --wp-slider-thumb-shadow: 0 0.5px 2px rgb(0 0 0 / 0.1);
    cursor: not-allowed;
}

.wp-slider:disabled::-moz-range-progress {
    background: var(--wp-color-control-track);
}

/* UISlider's minimumValueImage / maximumValueImage slot, as text. Inset by half a
   thumb so the labels sit under the ends of the thumb's travel, not the ends of the
   box. */
.wp-slider-captions {
    color: var(--wp-color-text-secondary);
    display: flex;
    font: var(--wp-font-style-footnote);
    font-variant-numeric: tabular-nums;
    justify-content: space-between;
    margin-top: -2px;
    padding: 0 calc(var(--wp-slider-thumb-size, 28px) / 2);
}

/* ======================================================================== search ==
 * The .searchable() field: grey wash, leading magnifier, trailing clear button that
 * appears once there is something to clear.
 */
.wp-search {
    position: relative;
}

/* A span around the glyph rather than a class on the <svg>, like the flyout's chevron: the icon
   fragments are shared and know nothing about the search field. */
.wp-search-icon {
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    left: 9px;
    pointer-events: none;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
}

.wp-search-icon svg {
    height: 15px;
    stroke-width: 2;
    width: 15px;
}

.wp-search input {
    background: var(--wp-color-surface-chip);
    border: 0;
    border-radius: var(--wp-radius-control);

    /* The insertion point takes the tint, as it does in every iOS text field. */
    caret-color: var(--wp-color-brand-primary);
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-body);
    height: 42px;
    padding: 0 30px;
    width: 100%;
}

.wp-search input::placeholder {
    color: var(--wp-color-text-secondary);
}

.wp-search input::-webkit-search-cancel-button,
.wp-search input::-webkit-search-decoration,
.wp-search input::-webkit-search-results-button,
.wp-search input::-webkit-search-results-decoration {
    appearance: none;
}

/* `xmark.circle.fill`, which is one glyph: a grey disc with the cross cut out of it. It was a
   disc painted in CSS with a &times; character printed over it, and the two differ in a way that
   is small and visible -- the character is lighter than the disc it sits on, it is centred on a
   font's idea of the glyph rather than on the disc, and it changes shape with the font. The
   colour is the same secondary grey the magnifying glass already uses, so the field's two
   accessories agree. */
.wp-search-clear {
    align-items: center;
    background: none;
    border: 0;
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-secondary);
    cursor: pointer;
    display: none;
    height: 18px;
    justify-content: center;
    padding: 0;
    position: absolute;
    right: 7px;
    top: 50%;
    transform: translateY(-50%);
    width: 18px;
}

.wp-search-clear svg {
    height: 18px;
    width: 18px;
}

/* The button carries no fill of its own now, so the ring is the only thing that can say it has
   the focus. Rounded to the glyph it draws rather than to the box around it. */
.wp-search[data-filled='true'] .wp-search-clear {
    display: flex;
}

/* ========================================================================= glass ==
 * Liquid Glass, as far as the web goes.
 *
 * `backdrop-filter` gives the blur and the saturation lift and the hairline gives the rim.
 * What it cannot give is refraction or a specular response to motion, so this reads as glass
 * without being the real material.
 *
 * There was an inset top highlight here as well, a one-pixel line meant to read as light
 * catching the edge. It does that on a bar floating over a photograph and nothing else, and on
 * a plain page it is a stray seam under the nav bar.
 *
 * The saturation matters more than the blur. Dropping it leaves grey frosting; at 180%
 * the colour of whatever scrolls underneath comes through, which is the thing that
 * makes the bar feel like it is in front of the page rather than on top of it.
 */
.wp-glass {
    /* Prefixed first: Safari shipped this years before it was unprefixed and still needs it. */
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
    background: var(--wp-color-material-glass);
    border: 1px solid var(--wp-color-material-glass-border);
    color: var(--wp-color-text-primary);
}

/* Without the filter the fill is a 72% wash and the content behind reads straight through
   the text. Firefox had this off by default until recently, so the fallback is not
   hypothetical. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .wp-glass {
        background: var(--wp-color-surface-base);
        color: var(--wp-color-text-primary);
    }
}

/* ===================================================================== app shell ==
 * The bar an iOS app has and a web page does not: a glass title bar pinned to the top,
 * with the page scrolling underneath it.
 *
 * There was a tab bar under this too, pinned to the bottom. It came out because a browser
 * is not a native app: mobile Safari already spends the bottom edge on its own address
 * bar, so the capsule landed on top of it and the two read as one double-decker, and the
 * page was paying for that edge twice. The menu in the title bar reaches everything it
 * did.
 */
.wp-app {
    background: var(--wp-color-surface-grouped);
    color: var(--wp-color-text-primary);
    min-height: 100dvh;

    /* The app is one column the width of the screen and it never scrolls sideways. A phone screen
       has no sideways; anything that needs a second axis, which so far is only a carousel, owns a
       scroller of its own and keeps the gesture inside it.
       `clip` rather than `hidden`: `hidden` makes this a scroll container, which hands the page a
       horizontal scrollport that script and the keyboard can still move, re-anchors any sticky
       descendant to it, and forces the vertical axis to `auto` as well. `clip` only clips. The
       bars are siblings of this element rather than children, so none of them is affected. */
    overflow-x: clip;

    /* The gutter a page ends on, so its last row does not sit flush against the bottom of the
       window. It used to be this plus the height of the floating tab bar, which the page had to
       end above or the last row was unreachable. Nothing floats there now. */
    padding-bottom: var(--wp-space-xl);
    padding-top: var(--wp-navbar-height);
}

:root {
    /* The bar's own height, which everything that has to clear it derives from: the page's top
       padding, the sticky date header, and the scroll-margin an anchored row keeps so it does not
       land underneath. 56 rather than 48 because the bar buttons are 42: at 48 the capsules had
       3px of air above and below and read as wedged into the bar rather than sitting in it. */
    --wp-navbar-height: 56px;

    /* The hairline between two rows of one card, wherever a card has rows: a table, the
       flyout's menu, a form's fields. A wash rather than the border token at full strength,
       because a separator inside a card should read as a crease and not as an edge. */
    --wp-separator: color-mix(in srgb, var(--wp-color-border-subtle) 45%, transparent);

    /* The glass capsule iOS 26 draws around a bar button. Still smaller than the 44px target the
       button claims through its own ::after, which is the point: the capsule is the drawing, not
       the hit area. The mark in the leading slot is sized from this too, so the brand and the
       glyph buttons cannot drift apart. */
    --wp-size-navbar-button: 42px;

    /* The widest the page's own column gets. One number that stops growing, rather than a
       percentage of a container that steps up at every breakpoint: a list of rows does not get
       more readable at 864px than at 720px, it just gets further from the phone the design is
       drawn for. */
    --wp-width-content: 720px;
}

/* The column the page's own content sits in, inside .wp-app.
 *
 * This replaces `.container` plus `col-12 offset-md-1 col-md-9 offset-lg-2 col-lg-8` on every
 * app-shell page. Those two were one job done twice, and done by percentages: the container
 * stepped 540/720/960/1140/1320 and the column then took 66.67% of whatever that came to, so the
 * text column grew with the window instead of settling. The md pair was also lopsided --
 * `offset-md-1 col-md-9` is 8.33% + 75%, which leaves 16.67% on the right and sat the page
 * visibly left of centre between 768px and 991px.
 *
 * `margin-inline: auto` centres it at every width, and the padding is the phone gutter, so the
 * narrow case is unchanged from what the container already did.
 */
.wp-content {
    margin-inline: auto;
    max-width: var(--wp-width-content);

    /* The gutter every screen keeps between its content and the edge of the phone, matching what
       iOS insets a grouped list by. It was a step narrower, which read as tight against the edge
       beside the app it is a port of, and left a card looking like it had been cropped rather than
       placed. `.wp-screen` already used this step; the two agree now.
       A carousel repeats this value twice to keep its first tile under the heading above it, so
       the two rules have to move together -- which the stylesheet's own test enforces rather than
       leaving to whoever edits one of them. */
    padding-inline: var(--wp-space-lg);
    width: 100%;
}

/* The space a screen keeps between the navigation bar and the first thing under it.
 *
 * A separate class because `.wp-content` is two jobs: the column a page's own content sits in,
 * and the column one block of a stack sits in -- the dashboard uses it eight times over, for the
 * greeting, each hero and each section that is not a full-bleed carousel. Only the first of those
 * is a screen, and only a screen has a navigation bar to clear, so the page column says which it
 * is rather than leaving the stylesheet to work it out from how deeply it is nested.
 *
 * It belongs here rather than on whatever opens the page, because it is a property of the screen:
 * a page that starts with a list got this spacing from `.wp-list-section`, a page that starts
 * with a card or a notice got nothing and sat flush under the bar. `.wp-list-section` keeps its
 * own margin and the two collapse through this element -- it has no padding or border on the
 * block axis -- so a page that already looked right is unchanged and the rest catch up.
 *
 * A page built as a stack does not carry this: `.wp-stack` sets its own block rhythm, and
 * `UiPageLayoutTest` in the app is what holds every other page to it. */
.wp-page {
    margin-top: var(--wp-space-xl);
}

/* --- navigation bar ------------------------------------------------------------- */
.wp-navbar {
    align-items: center;
    border-width: 0;
    display: grid;
    gap: var(--wp-space-sm);

    /* Three tracks, not flex: the title stays optically centred even when one side has a
       button and the other does not, which is what flex with `space-between` gets wrong. */
    grid-template-columns: 1fr auto 1fr;
    height: calc(var(--wp-navbar-height) + env(safe-area-inset-top));
    left: 0;
    padding-bottom: 0;

    /* The glass still spans the window, but what sits on it lines up with the page's own column.
       On a wide desktop the bar used to pin the menu to the far left and the action to the far
       right, which put the whole width of the monitor between two controls serving a 720px
       column: the reader's cursor was travelling the window rather than the page.

       Padding rather than a `max-width` on this element, because the element IS the material: cap
       its width and the glass stops spanning the window with it. Padding leaves the background
       full-bleed and only moves the grid's content box.

       The two terms are the two things `.wp-content` does, in the same order. `max(0px, ...)` is
       how far in the column itself starts, which is nothing until the window is wider than the
       column. `--wp-space-lg` is the gutter the column then insets its own contents by, and
       adding it here is what puts a bar button on the same line as the card below it rather than
       16px outside it. It applies at every width, so the two agree on a phone -- where the first
       term is zero and this is the whole of the padding -- exactly as they do on a desktop. */
    padding-inline: calc(max(0px, (100% - var(--wp-width-content)) / 2) + var(--wp-space-lg));
    padding-top: env(safe-area-inset-top);
    position: fixed;
    right: 0;
    top: 0;
    z-index: 100;
}

/* `ToolbarItem(placement: .principal)`: a VStack of the event's name over a caption, not a
   single line. Standings, Results, Forecast and Participants all use the same two-line
   principal item and only differ in what the second line says, so the second line is an
   optional slot rather than four variants of the bar. */
.wp-navbar-title {
    align-items: center;
    display: flex;
    flex-direction: column;

    /* The title is the page's <h1>, and a host stylesheet sizes an <h1> for a page heading: the
       first consumer's reboot makes it 40px, inside a bar 48px tall. Nothing shows it today
       because both lines are spans with type of their own, but the first bare word put in here
       would open the bar. The element states its own size for the same reason it states its own
       colour. */
    font: var(--wp-font-style-headline);
    gap: 1px;
    margin: 0;
    min-width: 0;
}

/* The mark stands in for the title on the screens that head themselves with the brand, and it is
   shorter here than at the head of the flyout. `.wp-wordmark`'s own 24px is set against a
   `title-2`, which is what titles the flyout; a bar titles itself in `headline`, five points
   smaller, and a mark cut for the larger type reads as the loudest thing in a 48px bar. 21px is
   that same relationship against `headline` instead. */
.wp-navbar-title .wp-wordmark {
    height: 21px;
}

/* Each line of the title clips rather than wraps. The bar is a fixed 48px and the leading slot
   now carries two buttons, so the centre track is ~80px narrower than it was: a club's longer
   event name used to fit and would now fold onto a second line and push out of the bar.
   `max-width` is what gives the ellipsis something to measure against -- the parent centres its
   children, so without it a line is sized by its own text and simply overflows the track it was
   given. */
.wp-navbar-title > span {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Both lines truncate rather than wrap, because a bar that grows is a bar that moves the page
   under it. Declared on the child rather than on each line, so the subtitle's own rule does not
   have to outrank a selector that already matches it. */
.wp-navbar-title > * {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wp-navbar-title > span:first-child {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
}

/* `.font(.caption).textCase(.uppercase)`. Uppercase here and not in the copy, so a translation
   does not have to carry the casing and a language without it is not shouted at. */
.wp-navbar-subtitle {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    text-transform: uppercase;
}

.wp-navbar-leading,
.wp-navbar-trailing {
    align-items: center;
    display: flex;
    gap: var(--wp-space-xs);
}

.wp-navbar-trailing {
    justify-content: flex-end;
}

/* A bar button. iOS 26 gives each one its own glass capsule rather than floating the glyph on
   the bar, which is why this carries the material itself and does not just inherit the bar's.
   36px is the capsule iOS draws; the 44px minimum target is the ::after, which costs nothing
   visually and keeps the two bars' buttons the same size as each other. */
.wp-navbar-button {
    /* Prefixed first: Safari shipped this years before it was unprefixed and still needs it. */
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    align-items: center;
    backdrop-filter: saturate(180%) blur(20px);
    background: var(--wp-color-material-glass);
    border: 1px solid var(--wp-color-material-glass-border);
    border-radius: var(--wp-radius-full);

    /* The mark's own ink, not the brand tint. Every button on this bar is navigation or a screen's
       one action, and they sit either side of a logo drawn in `--wp-color-text-primary`: a blue
       pencil beside a near-black menu beside a near-black badge read as three different kinds of
       thing. In light appearance the token is #051224, which is every dark pixel of the mark; in
       dark it is #FFFFFF, which is the whole of it. */
    color: var(--wp-color-text-primary);
    cursor: pointer;
    display: inline-flex;
    flex: none;
    font: var(--wp-font-style-body);
    height: var(--wp-size-navbar-button);
    justify-content: center;
    padding: 0;
    position: relative;
    text-decoration: none;
    width: var(--wp-size-navbar-button);
}

.wp-navbar-button::after {
    content: '';
    inset: calc((var(--wp-size-navbar-button) - var(--wp-size-touch-target)) / 2);
    position: absolute;
}

.wp-navbar-button svg {
    height: 20px;
    width: 20px;
}

/* The one bar button with no capsule: the mark brings its own circle and its own ring, and a
   ring drawn inside a ring reads as a mistake rather than as a badge in a holder. The 36px
   footprint, the 44px hit area and the focus ring all still come from `.wp-navbar-button`, so
   this takes away the material and nothing else -- the control stays exactly the size of the
   menu button beside it, which is what keeps the leading pair level. */
.wp-navbar-button-mark {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: none;

    /* Zero width, not a transparent colour: the capsule's 1px border is still laid out when it is
       only invisible, which leaves the mark 34px inside a 36px control and a hair smaller than the
       menu button beside it. The focus ring is an outline and is unaffected either way. */
    border-width: 0;
    padding: 0;
}

@media (hover: hover) and (pointer: fine) {
    /* The tint is the capsule lighting up, and the mark has no capsule to light: behind a logo it
       would read as a coloured halo rather than as a button responding. */
    .wp-navbar-button:not(.wp-navbar-button-mark):hover {
        background: color-mix(in srgb, var(--wp-color-brand-primary) 12%, var(--wp-color-material-glass));
    }
}

/* --- the glass tray a control floats on ------------------------------------------- */
/* The tray the standings/forecast picker rides on, so the list reads through it as it
   scrolls past: iOS's `liquidGlassTray()`, which is 4pt of padding and a glass capsule.
   The tray wraps the control rather than being it.
   The control's own trough is dropped inside a tray: a grey trough inside a glass
   capsule is two pieces of chrome doing one job, and the glass is the better one. */
.wp-glass-tray .wp-segmented {
    background: none;
}

.wp-glass-tray {
    --wp-glass-tray-inset: var(--wp-space-xs);
    --wp-glass-tray-radius: var(--wp-radius-full);
    border-radius: var(--wp-glass-tray-radius);

    /* No width of its own: the tray goes inside .wp-content and takes that column's width, so
       the picker lines up with the table it is picking for. It used to carry its own gutter and
       its own 560px cap, which put it on a different grid from everything under it. */
    margin-bottom: var(--wp-space-lg);
    padding: var(--wp-glass-tray-inset);
    position: sticky;
    top: calc(var(--wp-navbar-height) + env(safe-area-inset-top) + var(--wp-space-sm));
    z-index: 10;
}

/* ====================================================================== flyout ==
 * The leading-edge menu the nav bar's hamburger opens: iOS's My WePlay flyout.
 *
 * A panel plus a scrim, both driven by one `data-open` on the panel, so the only thing
 * script does is flip an attribute. Closed state is `translateX(-100%)` rather than
 * `display: none`, which is what lets it slide; `visibility` is what takes it out of
 * the tab order while it is off screen, because a transformed element is still
 * focusable.
 */
.wp-flyout {
    border-width: 0 1px 0 0;
    bottom: 0;
    display: flex;
    flex-direction: column;
    left: 0;
    overflow-y: auto;

    /* Reaching the end of the menu must not hand the scroll to the page behind the scrim. */
    overscroll-behavior-y: contain;
    padding: calc(env(safe-area-inset-top) + var(--wp-space-lg)) var(--wp-space-lg) calc(env(safe-area-inset-bottom) + var(--wp-space-lg));
    position: fixed;
    top: 0;
    transform: translateX(-100%);
    transition: transform 250ms ease,
    visibility 250ms;
    visibility: hidden;
    width: min(84vw, 320px);
    z-index: 200;
}

/* `overflow-x: hidden` only once it is open, which is the only state in which it can be seen
   to scroll. The panel sets `overflow-y: auto`, and a box with one axis scrollable and the other
   `visible` is not a thing CSS allows: the browser computes the visible axis to `auto` too, so
   the panel had a horizontal scroll it never asked for the moment anything inside it reached
   the edge. `hidden` rather than `clip` here, unlike `.wp-app`: this element is already a scroll
   container because of the vertical axis, so there is no new one to avoid creating. */
.wp-flyout[data-open='true'] {
    overflow-x: hidden;
    transform: translateX(0);
    visibility: visible;
}

/* The panel scrolls, its contents do not compress. A flex item only gets the implied
   `min-height: auto` that stops it shrinking past its own content while its overflow is
   visible, and .wp-flyout-group clips (it has to, to round its corners), so without this
   the groups shrink to share the viewport and the rows inside them are cut off instead of
   pushing the panel into a scroll. Short enough menus are unaffected. */
.wp-flyout > * {
    flex-shrink: 0;
}

.wp-flyout-scrim {
    background: rgb(0 0 0 / 0.35);
    inset: 0;
    opacity: 0;
    position: fixed;
    transition: opacity 250ms ease,
    visibility 250ms;
    visibility: hidden;
    z-index: 150;
}

.wp-flyout[data-open='true'] + .wp-flyout-scrim,
.wp-flyout-scrim[data-open='true'] {
    opacity: 1;
    visibility: visible;
}

/* A grouped list inside the flyout, so the menu reads like a Settings screen rather
   than a stack of links. */
.wp-flyout-group {
    /* Opaque, mixed over the card rather than laid on it as a translucent wash: the separator
       takes this exact colour on hover, and two different ways of arriving at "7% grey" do not
       land on the same pixel. Same reasoning, and same value, as .wp-table's row hover. */
    --wp-flyout-row-hover: color-mix(in srgb, var(--wp-color-text-secondary) 7%, var(--wp-color-surface-grouped-raised));
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    margin-bottom: var(--wp-space-lg);
    overflow: hidden;
}

/* A Section header, inset to the group's text rather than the panel edge. Uppercase, because
   MyWePlayView's sections are plain `Section("...")` in a default-styled List and SwiftUI
   uppercases those -- which is only visible in the source through the `.textCase(nil)` that
   PlayerDetailView has to put on a header's trailing link to escape it. Same style as
   `.wp-list-title`, and for the same reason; the two are not shared only because this panel's
   header is a sibling of its group rather than a child of a header row. */
.wp-flyout-section {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    margin: 0 0 var(--wp-space-sm);
    padding: 0 var(--wp-space-lg);
    text-transform: uppercase;
}

/* `text-primary`, where this was `text-secondary`. A row's label is the content of the menu, not
   a caption on it, and grey label beside grey chevron beside grey value made every row read as a
   control that had been turned off. The value and the chevron say secondary on themselves, so
   the row's own colour is free to be the one the label needs. */
.wp-flyout-item {
    align-items: center;
    color: var(--wp-color-text-primary);
    display: flex;

    /* 15px at 500, which is not a step the scale has: `subheadline` is 15px at 400, and a column
       of fourteen labels at 400 reads as body copy rather than as a list of destinations. The
       shorthand sets the family and the leading; the two that follow are the menu's own, and the
       size is restated beside the weight so the row keeps it if the step ever moves. */
    font: var(--wp-font-style-subheadline);
    font-size: 15px;
    font-weight: 500;
    gap: var(--wp-space-md);
    min-height: var(--wp-size-touch-target);
    padding: var(--wp-space-sm) var(--wp-space-lg);
    position: relative;
    text-decoration: none;
}

/* A row is an <a> for the eighteen that navigate and a <button> for the two that open a sheet.
   A button brings its own background, border, font and centring, and none of them are the row's;
   `width: 100%` is the one an <a> gets for free from being a flex child of the group and a
   button does not, because a button is not a block. */
button.wp-flyout-item {
    background: none;
    border: 0;
    cursor: pointer;
    text-align: left;
    width: 100%;
}

/* The separator starts at the label rather than the card edge, clearing the icon badge the way
   a UITableView's separatorInset clears a row's image. 56px is the padding, the badge and the
   gap: a pseudo-element rather than a box-shadow or a gradient, because both of those collide
   with the row's own background the moment it is hovered or destructive. */
.wp-flyout-item + .wp-flyout-item::before {
    background: var(--wp-separator);
    content: '';
    height: 1px;
    left: calc(var(--wp-space-lg) + var(--wp-size-icon-badge) + var(--wp-space-md));
    position: absolute;
    right: 0;
    top: 0;
}

/* Takes the slack between the icon and the chevron, so every chevron in a group lines up on
   the right however short the label. */
.wp-flyout-label {
    flex: 1;
    min-width: 0;
}

/* The trailing value on a row that reports something as well as leading somewhere: the language
   in force, the appearance, the service a row is about to open.
   A footnote and not body copy. It was `body`, which is 16px, so the answer was drawn LARGER
   than the 15px label asking the question -- and on the one row that carries both a long label
   and a value, those two pixels were the difference between one line and two at 320px. */
.wp-flyout-value {
    color: var(--wp-color-text-secondary);
    flex: none;
    font: var(--wp-font-style-footnote);
}

/* A native picker covering the whole row, for the two rows that answer rather than navigate.
 *
 * Invisible and on top, with the row's own label, value and chevron drawn underneath it: the
 * select is the hit area, so a press anywhere on the row opens the platform's list, its
 * type-to-find and, on a phone, its wheel. `.wp-flyout-item` is already `position: relative`,
 * which is what this is positioned against.
 *
 * `opacity: 0` rather than `visibility: hidden` or a zero size, because those three are not the
 * same thing: the other two take the element out of hit-testing, which is the one property this
 * needs. A <label> wrapping the control was the obvious shape and does not work either -- a
 * label FOCUSES an associated select everywhere and only Chrome also opens it.
 *
 * The native control is still what a keyboard and a screen reader get, because it is still the
 * only focusable thing here: it is drawn transparent rather than hidden from the accessibility
 * tree, and the decoration under it is `aria-hidden` in the markup. */
.wp-flyout-item-control {
    appearance: none;
    border: 0;
    cursor: pointer;
    inset: 0;
    opacity: 0;
    position: absolute;
}

/* No focus ring on these two rows, by request. Nothing else is needed to suppress it: the
   control is `opacity: 0`, and opacity applies to an element's outline as much as to its box, so
   the browser's own ring is already invisible and there is nothing here to turn off.
   Worth knowing rather than discovering: that leaves a keyboard reader with no indication of
   where they are when they tab onto Language or Appearance. The row still takes focus and the
   picker still opens on Space or Enter; it just does not say so. */

/* A span around the glyph rather than a class on the <svg>: the icon fragments are shared and
   know nothing about the flyout, the same way .wp-icon-badge wraps rather than restyles one. */
.wp-flyout-chevron {
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    flex: none;
}

.wp-flyout-chevron svg {
    height: 13px;
    stroke-width: 2;
    width: 13px;
}

/* `(hover: hover) and (pointer: fine)` because a touch device fires hover on tap and then keeps
   it: without the guard a phone leaves the last row the reader touched permanently highlighted.
   Same guard, same reason, as the standings table. */
@media (hover: hover) and (pointer: fine) {
    .wp-flyout-item:hover {
        background: var(--wp-flyout-row-hover);
    }

    /* The highlight owns the full width of the row, creases included: the separator above the
       hovered row and the one below it take the hover colour and lose their inset, so the
       highlight reads as one unbroken block instead of a band with a line cut through it. The
       row below is reached through its own ::before, which is the separator between the two. */
    .wp-flyout-item:hover::before,
    .wp-flyout-item:hover + .wp-flyout-item::before {
        background: var(--wp-flyout-row-hover);
        left: 0;
    }
}

/* The panel's header: the title on the leading edge and one action trailing it.
   `space-between` and not the nav bar's three tracks, because there is nothing to centre
   between the two -- the mark IS the leading item, and a middle track would only pull it off
   the edge the rows under it line up on.

   The group's bottom margin lives here rather than on the title, so the gap is the same whether
   or not the header has an action in it. */
.wp-flyout-header {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: space-between;
    margin-bottom: var(--wp-space-lg);
}

/* The panel's title, which is the wordmark rather than the product's name set in type: the menu
   is the one place a page whose header is a search field has room to draw the brand instead of
   writing it.

   `line-height: 0` rather than a height of its own, so the slot is exactly the mark's. The reset
   makes an image a block, which is what already keeps it there, but this file has to hold on its
   own for a consumer that declined the reset: an inline image sits on a baseline and carries the
   line box's descender under it, which would open a gap below the mark that nothing chose. */
.wp-flyout-title {
    line-height: 0;
    margin: 0;
}

/* The header's action: a bar button's capsule in the brand's tint rather than in glass. The
   panel is itself the material a `.wp-navbar-button` would be floating on, and glass over glass
   reads as a smear rather than as a control, so this one takes the tint `.wp-button-tinted`
   uses and leaves the blur to the surface that earns it.

   36px drawn and 44px targeted, the same pair as a bar button, so the control the reader meets
   in the menu is the size of the one they met in the bar that opened it. */
.wp-flyout-action {
    align-items: center;
    background: color-mix(in srgb, var(--wp-color-surface-icon-badge) 15%, transparent);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    flex: none;
    height: var(--wp-size-navbar-button);
    justify-content: center;
    position: relative;
    text-decoration: none;
    width: var(--wp-size-navbar-button);
}

.wp-flyout-action::after {
    content: '';
    inset: calc((var(--wp-size-navbar-button) - var(--wp-size-touch-target)) / 2);
    position: absolute;
}

.wp-flyout-action svg {
    height: 20px;
    width: 20px;
}

@media (hover: hover) and (pointer: fine) {
    .wp-flyout-action:hover {
        background: color-mix(in srgb, var(--wp-color-surface-icon-badge) 100%, transparent);
    }
}

/* A destination that leaves the site, in place of the chevron. A chevron promises a page you can
   come back from with the back button, which is not what a WhatsApp group or a mail client is;
   the glyph says so before the tap rather than after it. The service's name goes in the row's
   own `.wp-flyout-value`, so the reader knows what is about to open as well as that something
   will. */
.wp-flyout-external {
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    flex: none;
}

.wp-flyout-external svg {
    height: 15px;
    stroke-width: 2;
    width: 15px;
}

/* The two partners, as tiles rather than as rows.
 *
 * They are the one group here that is not navigation within the product: somebody else's shop and
 * somebody else's app. A row in the same stack as "My profile" says they are more of the same, and
 * a pair of tiles says they are an aside -- which is what they are, and why they sit between the
 * settings and the help rather than among the account rows.
 *
 * Two columns at 288px, which is the panel's card width: a tile is 139px and holds "SwingVision"
 * on one line at 15px. `1fr 1fr` and not `auto-fit`, because a third partner should wrap to a new
 * row rather than squeeze these two under their own text.
 */
.wp-flyout-tiles {
    display: grid;
    gap: var(--wp-space-md);
    grid-template-columns: 1fr 1fr;
    margin-bottom: var(--wp-space-lg);
}

.wp-flyout-tile {
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-xs);
    min-width: 0;
    padding: var(--wp-space-md);
    text-decoration: none;
}

.wp-flyout-tile .wp-icon-badge {
    margin-bottom: var(--wp-space-xs);
}

/* `overflow-wrap` rather than a truncation: a partner's name is the whole content of the tile and
   half of it is worse than two lines of it. */
.wp-flyout-tile-title {
    font: var(--wp-font-style-subheadline);
    font-weight: 600;
    margin: 0;
    overflow-wrap: anywhere;
}

.wp-flyout-tile-note {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    margin: 0;
}

@media (hover: hover) and (pointer: fine) {
    .wp-flyout-tile:hover {
        background: var(--wp-flyout-row-hover);
    }
}

/* Signing out, on its own rather than as the only row of a group of one.
 *
 * It is the one thing in this panel that is neither navigation nor a setting, and a card around
 * it made it look like a section whose other rows had failed to load. Centred, because there is
 * no icon tile to align a label against and nothing trailing it: it goes nowhere, so there is no
 * chevron, and a lone left-aligned word in a full-width control reads as a row missing its row.
 */
.wp-flyout-signout {
    align-items: center;
    background: color-mix(in srgb, var(--wp-color-feedback-danger) 10%, transparent);
    border: 0;
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-feedback-danger);
    cursor: pointer;
    display: flex;
    font: var(--wp-font-style-subheadline);
    font-weight: 600;
    gap: var(--wp-space-sm);
    justify-content: center;
    margin-bottom: var(--wp-space-lg);
    min-height: var(--wp-size-touch-target);
    padding: var(--wp-space-md) var(--wp-space-lg);
    text-decoration: none;
    width: 100%;
}

.wp-flyout-signout svg {
    height: 18px;
    stroke-width: 2;
    width: 18px;
}

@media (hover: hover) and (pointer: fine) {
    .wp-flyout-signout:hover {
        background: color-mix(in srgb, var(--wp-color-feedback-danger) 16%, transparent);
    }
}

/* The three documents every page has to be able to reach, as a line of links rather than as three
   more rows: they are the small print, and giving them the same weight as "My profile" is what
   made the About section the longest one in the menu. */
.wp-flyout-legal {
    color: var(--wp-color-text-secondary);
    display: flex;
    flex-wrap: wrap;
    font: var(--wp-font-style-caption);
    gap: var(--wp-space-xs) var(--wp-space-sm);
    justify-content: center;
    margin: 0;
    padding-top: var(--wp-space-lg);
}

.wp-flyout-legal a {
    color: var(--wp-color-text-secondary);
    text-decoration: none;
}

@media (hover: hover) and (pointer: fine) {
    .wp-flyout-legal a:hover {
        color: var(--wp-color-text-primary);
    }
}

/* The separator between them, hidden from a screen reader: it is punctuation between links, and
   read aloud it is a middot between every pair. */
.wp-flyout-legal-separator {
    opacity: 0.5;
}

/* The build, under the last group, the way the app prints its version under the sign-out row.
   `margin-top: auto` pins it to the foot of a short menu instead of leaving it mid-panel. */
.wp-flyout-footer {
    margin-top: auto;
}

.wp-list-header {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: space-between;
    margin: 0;
    padding: 0 var(--wp-space-md) var(--wp-space-sm) var(--wp-space-md);
}

.wp-list-footer {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: space-between;
    margin: 0;
    padding: var(--wp-space-sm) var(--wp-space-md) 0 var(--wp-space-md);
}

.wp-list-title {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    margin: 0;
    text-transform: uppercase;
}

/* `.foregroundStyle(.secondary)` and `.textCase(nil)`: the header's size and colour, and none of
   its casing. */
.wp-list-link {
    color: var(--wp-color-text-secondary);
    flex: none;
    font: var(--wp-font-style-footnote);
    text-decoration: none;
}

/* One `Section`: its header, its card and its footer as a block. The gap between sections lives
   here rather than on a parent, so the header can keep hugging its own card. */
.wp-list-section {
    margin-top: var(--wp-space-lg);
}

.wp-list {
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.wp-list-row {
    align-items: center;
    color: var(--wp-color-text-primary);
    display: flex;
    font: var(--wp-font-style-subheadline);
    gap: var(--wp-space-md);
    justify-content: space-between;
    min-height: var(--wp-size-touch-target);
    padding: var(--wp-space-sm) var(--wp-space-lg);
    position: relative;
    text-decoration: none;
}

/* A row that is a control rather than a link: it opens a sheet, and there is nowhere to go. The
   element brings a border, a background and a shrink-to-fit width a row must not have; the rule
   above already states the font and the colour. */
/* Full bleed, unlike the flyout's: there is no icon to clear, so the hairline runs the width of
   the card the way a UITableView's does when a row has no image. */
.wp-list-row + .wp-list-row::before {
    background: var(--wp-separator);
    content: '';
    height: 1px;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
}

/* ClubWhatsAppGroupsCard: a list whose rows lead with an icon badge.
   The app insets the divider past the badge rather than running it the width of the card, and gives
   the row the height a 40px badge needs rather than the 44 a bare row gets. The inset is the
   badge's own arithmetic and not a literal 68px, so the two cannot drift: change the badge and the
   divider follows it. */
.wp-list-badged {
    --wp-size-icon-badge: 40px;
}

.wp-list-badged > .wp-list-row {
    padding-block: var(--wp-space-lg);
}

.wp-list-badged > .wp-list-row + .wp-list-row::before {
    left: calc(var(--wp-space-lg) + var(--wp-size-icon-badge) + var(--wp-space-md));
}

/* 18 against the bare row's 14: a bigger tile wants a bigger glyph, and this is the size the app
   draws the mark at. */
.wp-list-badged .wp-icon-badge svg {
    height: 18px;
    stroke-width: 1.5;
    width: 18px;
}

/* The value is what gets squeezed when the pair does not fit, not the label: a label is a fixed
   word and a value is a name, an address or a URL. The label can still give way as a last
   resort, because a row whose label is a runtime string -- an event's name, a club's -- would
   otherwise push the value out of the card. */
.wp-list-label {
    color: var(--wp-color-text-secondary);
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* `margin-left: auto` and not `justify-content`: a row that also has a chevron has three
   children, and space-between would park the value in the middle of the row instead of against
   the chevron. This pins it to the trailing edge whatever else is in the row. */
/* Takes the slack between the icon and the chevron, so the label sits against its badge rather
   than being centred by the row's own `space-between`. Same reason as .wp-flyout-label. */
.wp-icon-badge + .wp-list-label {
    flex: 1 1 auto;
}

/* A grouped row's badge is bigger than a menu's. It leads the row where a date tile leads the
   one above it, and a date tile is 46px: a list with a fixture on one row and a venue on the
   next cannot have the two lead at different sizes. The glyph grows with the square rather than
   sitting small in a bigger one.
   Scoped to the row and not raised on the token, because the flyout's menu rows use the same
   badge and should keep the smaller one: a menu is not a grouped list. */
.wp-list-row .wp-icon-badge {
    --wp-size-icon-badge: 46px;
}

.wp-list-row .wp-icon-badge svg {
    height: 24px;
    stroke-width: 1.5;
    width: 24px;
}

.wp-list-value {
    margin-left: auto;
    min-width: 0;
    overflow: hidden;
    text-align: right;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* `stackedLabeledContent`: the value goes under the label instead of beside it. Phone numbers,
   email addresses and hosts are long enough that pinning them to the trailing edge reads
   cramped, and these are the rows that carry a trailing action as well. */
.wp-list-row-stacked {
    align-items: center;
}

.wp-list-row-stacked .wp-list-text {
    align-items: flex-start;
    display: flex;

    /* Takes the slack, so what follows it -- a flag, a chevron -- sits at the row's end and the
       text is what truncates rather than the gap being what absorbs the difference. */
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.wp-list-row-stacked .wp-list-value {
    margin-left: 0;
    text-align: left;
}

/* The number a row is really about, at its trailing edge: bold and in the primary colour, where
   `.wp-list-value` is the quieter secondary one. Tabular digits so a column of #34, #71 and #147
   lines up on the same edge rather than wobbling with the width of each glyph. */
.wp-list-position {
    color: var(--wp-color-text-primary);
    flex: none;
    font: var(--wp-font-style-headline);
    font-variant-numeric: tabular-nums;
}

/* A stacked row whose first line is the name of the thing the row is about rather than a label
   for the value under it. The two swap weight: the name takes the primary colour and the detail
   under it the quieter secondary one, where a label-and-value row has the label quiet and the
   value loud. */
.wp-list-row-named .wp-list-label {
    color: var(--wp-color-text-primary);
}

.wp-list-row-named .wp-list-value {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
}

/* A third line, under a row that already has a title and a subtitle: a club's street under its
   name and its town. `caption`, as `ClubSearchResultRowView` has it -- small enough that three
   lines still read as one row rather than as three. */
.wp-list-caption {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    min-width: 0;

    /* Wraps rather than truncates: the app puts no line limit on a row's subtitle, and the
       longest of them is a biography, which says nothing at all cut off after one line.
       `anywhere` because a website is a subtitle too, and a URL has no spaces to break at. */
    overflow-wrap: anywhere;
}

/* A row that is a sentence rather than a fact.
 *
 * `MatchDetailView`'s summary is a paragraph about the match, and `.wp-list-value` is the wrong
 * thing to put it in: that one truncates on purpose, because it is built for a phone number at a
 * row's trailing edge, and a sentence in it loses everything past the first line behind an
 * ellipsis. This wraps, reads at body size and sits at the leading edge like the text it is.
 */
.wp-list-paragraph {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-body);
    margin: 0;
}

/* A row that copies its value. The pasteboard write is silent, so the value dims to secondary
   and fades back; that fade is the whole confirmation, and it is what the app does. The
   attribute is set and cleared by clipboard-controller, which owns the timing. */
.wp-list-row[data-copied] .wp-list-value {
    color: var(--wp-color-text-secondary);
}

.wp-list-row .wp-list-value {
    transition: color 200ms ease-in-out;
}

/* A row is an <a> for the ones that navigate and a <button> for the ones that open something.
   A button brings its own font, background, border and centring, and none of them are the row's. */
button.wp-list-row {
    background: none;
    border: 0;
    cursor: pointer;
    font: var(--wp-font-style-subheadline);
    text-align: left;
    width: 100%;
}

/* A row with two actions -- the phone number copies, the WhatsApp glyph beside it opens a chat
   -- cannot be one button, so the label-and-value stack is the button instead. */
.wp-list-copy {
    background: none;
    border: 0;
    color: var(--wp-color-text-primary);
    cursor: pointer;
    flex: 1 1 auto;
    font: var(--wp-font-style-body);
    padding: 0;
    text-align: left;
}

.wp-list-chevron {
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    flex: none;
}

.wp-list-chevron svg {
    height: 14px;
    stroke-width: 2;
    width: 14px;
}

/* A row that goes somewhere lights up under the pointer, and the hairlines above and below it
   take the same colour so the wash reads as one block rather than a band with seams. */
@media (hover: hover) and (pointer: fine) {
    a.wp-list-row:hover,
    button.wp-list-row:hover {
        background: color-mix(in srgb, var(--wp-color-text-secondary) 7%, var(--wp-color-surface-grouped-raised));
    }

    a.wp-list-row:hover::before,
    a.wp-list-row:hover + .wp-list-row::before,
    button.wp-list-row:hover::before,
    button.wp-list-row:hover + .wp-list-row::before {
        background: color-mix(in srgb, var(--wp-color-text-secondary) 7%, var(--wp-color-surface-grouped-raised));
    }
}

/* The same arithmetic as the field rows: an inset ring curves the way the element's own
   `border-radius` says, so a row at either end of the list has to carry the list's radius or its
   ring squares off a corner the list rounds and `overflow: hidden` then clips the square away. */
.wp-list > .wp-list-row:first-child {
    border-start-end-radius: var(--wp-radius-card);
    border-start-start-radius: var(--wp-radius-card);
}

.wp-list > .wp-list-row:last-child {
    border-end-end-radius: var(--wp-radius-card);
    border-end-start-radius: var(--wp-radius-card);
}

/* EventDetailView's `statusBadge`: a participant's standing in an event, outlined rather than
   filled. Green only when they are actually in it -- every other state is a fact rather than a
   warning, so it takes the page's own secondary ink and the same outline. `radius.outline` and
   not `radius.tile`: a stroked box round a word is tighter than a filled tile, which is the
   distinction that step exists to carry. */
.wp-status {
    border: 1px solid var(--wp-color-text-secondary);
    border-radius: var(--wp-radius-outline);
    color: var(--wp-color-text-secondary);
    display: inline-block;
    font: var(--wp-font-style-footnote);
    padding: 2px 6px;
}

.wp-status-active {
    border-color: var(--wp-color-feedback-success);
    color: var(--wp-color-feedback-success);
}

/* MembershipsView's `verified` row: a tick or a cross where the value goes, with no box round it.
   Not `.wp-status`, which is an outlined badge built to hold a word - a bordered capsule round a
   single glyph reads as a button.
   Green when the club has confirmed the membership, and the page's own secondary ink when it has
   not: an unverified membership is a state rather than a warning, which is the same call
   `.wp-status` makes for every state of its own that is not active. */
.wp-verified {
    align-items: center;
    color: var(--wp-color-text-secondary);
    display: inline-flex;
}

.wp-verified-yes {
    color: var(--wp-color-feedback-success);
}

.wp-verified svg {
    height: 20px;
    width: 20px;
}

/* A `Section`'s footer: the sentence under a group explaining what it is.
   No inline padding of its own. It used to carry a `--wp-space-lg` inset so that a note sitting in
   the gutter lined up with the text inside the card above it rather than with the card's edge; it
   now takes whatever inset it is placed in, which is the page gutter in the gutter and the card's
   own padding inside a card.

   `margin-block`, not the `margin` shorthand: a note that is also the page column
   (`.wp-list-note.wp-content`, which the club page's WhatsApp groups note is) needs
   `.wp-content`'s `margin-inline: auto` to survive, and the shorthand's two trailing zeros came
   later in this stylesheet and silently reset it -- leaving the note flush against the window
   while the heading and the card above and below it stayed centred. The reset already zeroes a
   paragraph's margins, so naming only the block axis loses nothing. */
.wp-list-note {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
}

/* ======================================================================= profile ==
 * PlayerDetailView's header section: the picture, the name, the handle. A section of the list
 * with a clear row background and no separator, which on the web is simply not a card.
 */
.wp-profile-header {
    align-items: center;
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-sm);
    margin-top: var(--wp-space-xl);
    text-align: center;
}

.wp-profile-header .wp-avatar {
    --wp-size-avatar: 180px;
}

/* LocationDetailView's header is 160 where PlayerDetailView's is 180: a club's photo is a place
   and a player's is a face, and the app sizes them apart. */
.wp-avatar-club {
    --wp-size-avatar: 160px;
}

/* An organisation's own mark, which is not an avatar: it is already the shape its designer gave
   it, so there is no squircle to clip it to and no chip to sit it on. Contained rather than
   covered, because a logo cropped to a square is a different logo. */
.wp-logo {
    height: 100px;
    object-fit: contain;
    width: 100px;
}

/* A mark drawn for a light background and one drawn for a dark one. Which shows is CSS and not
 * script, so it is right in the first frame rather than after one.
 *
 * The selectors are tokens.css's, exactly: `prefers-color-scheme` is the default, `data-theme`
 * overrides it in either direction, and `data-theme` is honoured on ANY ancestor rather than
 * only on :root -- the gallery puts it on one section so both appearances can sit on one page,
 * and a host embedding the library in part of a page can do the same.
 */
.wp-logo-dark {
    display: none;
}

/* `:where()` so this weighs nothing. tokens.css can write the same condition plainly because
   its rules all target one element and order decides between them; here the overrides below are
   less specific than `:root:not([data-theme='light'])` would be, and would lose to it whatever
   the order. */
@media (prefers-color-scheme: dark) {
    :where(:root:not([data-theme='light'])) .wp-logo-light {
        display: none;
    }

    :where(:root:not([data-theme='light'])) .wp-logo-dark {
        display: block;
    }
}

[data-theme='light'] .wp-logo-light {
    display: block;
}

[data-theme='light'] .wp-logo-dark {
    display: none;
}

[data-theme='dark'] .wp-logo-light {
    display: none;
}

[data-theme='dark'] .wp-logo-dark {
    display: block;
}

/* The product's name as it is drawn rather than as it is set: a dark-ink mark and a light-ink
   one, swapped by the same pair of classes a club's own logo uses above.

   Sized by its height alone, with the width left to follow, so the two files are interchangeable
   and neither can be stretched by a container. 24px puts the wordmark's cap height within a pixel
   of a `title-2`'s, which is the type it replaced at the head of the flyout. */
.wp-wordmark {
    height: 24px;
    width: auto;
}

/* The other half of the brand: the round badge rather than the drawn name, swapped between inks
   by the same pair of classes above.

   Square by its file and round by its artwork, so the radius here only clips the raster's corner
   pixels to the circle the eye already sees. Sized from the bar button's own token because the
   title bar is the only place it goes today: the mark stands in for a button there, so the two
   have to stay the same size as each other without anyone having to remember to change both. */
.wp-mark {
    border-radius: var(--wp-radius-full);
    height: var(--wp-size-navbar-button);
    width: var(--wp-size-navbar-button);
}

.wp-profile-name {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-2);
    margin: 0;
}

.wp-profile-handle {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
    margin: 0;
    text-transform: uppercase;
}

/* What ProfileEditMenuView puts under the picture: a footnote, not a button. The app offers the
   camera or the library through a confirmation dialog; a browser has one picker, so this is a
   <label> in front of a hidden file input and the whole thing is the target. */
.wp-profile-picture-action {
    color: var(--wp-color-brand-primary);
    cursor: pointer;
    font: var(--wp-font-style-footnote);
}

.wp-profile-picture-action:focus-within {
    border-radius: var(--wp-radius-tile);
    outline: 2px solid var(--wp-color-brand-primary);
    outline-offset: 2px;
}

/* `formSquare`: one match's result. A square and not the home card's arrow, because this row is
   a run of them and the shape is what makes the run readable at a glance. A draw is the odd one
   out -- no fill, a hairline, and the page's own ink -- so it reads as neither. */
.wp-outcome-list {
    align-items: center;
    display: flex;
    gap: 6px;
}

.wp-outcome {
    align-items: center;
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-on-brand);
    display: inline-flex;
    flex: none;
    font: var(--wp-font-style-subheadline);
    font-weight: 700;
    height: 24px;
    justify-content: center;
    text-decoration: none;
    width: 24px;
}

.wp-outcome-win {
    background: var(--wp-color-feedback-success);
}

.wp-outcome-loss {
    background: var(--wp-color-feedback-danger);
}

.wp-outcome-draw {
    border: 1px solid var(--wp-color-border-subtle);
    color: var(--wp-color-text-primary);
}

/* ===================================================================== segmented ==
 * iOS's segmented picker, as navigation.
 *
 * Links in a <nav>, not radio inputs and not `role="tablist"`. On iOS this switches a
 * value in place, so a Picker is right; here each segment is a different page, and a
 * thing that loads a new document is a link. That also means the current segment is
 * marked with `aria-current="page"` rather than `aria-selected`, and the whole control
 * keeps working with JavaScript off.
 *
 * Metrics are UISegmentedControl's: a 2px trough around 7px-radius segments, which is
 * where the 9px outer radius comes from.
 */
.wp-segmented {
    /* The trough around the segments. Named rather than inlined because anything nesting this
       needs it to keep its own corners concentric with the segments inside. */
    --wp-segmented-inset: 2px;
    background: var(--wp-color-control-track);
    border-radius: calc(var(--wp-radius-tile) + var(--wp-segmented-inset));

    /* Grid, not flex: UISegmentedControl makes every segment as wide as the widest title, and
       `1fr` columns do that wherever the control has a width to share out. In a container that
       shrink-wraps to its content the columns fall back to their own widths, which is the one case
       the segments are not equal; they still never clip, because the labels set the floor. */
    display: grid;
    gap: var(--wp-segmented-inset);
    grid-auto-columns: 1fr;
    grid-auto-flow: column;
    padding: var(--wp-segmented-inset);
}

.wp-segmented a {
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-footnote);
    padding: 6px var(--wp-space-md);
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
}

/* The selected segment is a raised tile, the way iOS draws it: lifted off the trough
   rather than tinted, so the control reads the same whatever the brand colour is. */
.wp-segmented a[aria-current] {
    background: var(--wp-color-surface-grouped-raised);
    box-shadow: 0 0.5px 3px rgb(0 0 0 / 0.12);
    color: var(--wp-color-text-primary);
    font-weight: 600;
}

@media (hover: hover) and (pointer: fine) {
    .wp-segmented a:not([aria-current]):hover {
        background: color-mix(in srgb, var(--wp-color-text-secondary) 10%, transparent);
    }
}

/* The same control as a set of radios, for a segmented control that answers a question rather
 * than navigating. The radio stays in the layout and keeps its focus and keyboard behaviour --
 * arrow keys move between radios in a group, which is what a segmented control does on iOS --
 * and the label beside it is what gets painted. `opacity` rather than `display: none`, because
 * a `display: none` radio is not focusable and the group loses its keyboard entirely.
 */
.wp-segmented-input {
    height: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
    position: absolute;
    width: 1px;
}

.wp-segmented label {
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-primary);
    cursor: pointer;
    font: var(--wp-font-style-footnote);
    padding: 6px var(--wp-space-md);
    text-align: center;
}

.wp-segmented-input:checked + label {
    background: var(--wp-color-surface-grouped-raised);
    box-shadow: 0 0.5px 3px rgb(0 0 0 / 0.12);
    color: var(--wp-color-text-primary);
    font-weight: 600;
}

@media (hover: hover) and (pointer: fine) {
    .wp-segmented-input:not(:checked) + label:hover {
        background: color-mix(in srgb, var(--wp-color-text-secondary) 10%, transparent);
    }
}

.wp-segmented-input:disabled + label {
    cursor: default;
    opacity: 0.5;
}

/* Concentric corners for anything riding a glass tray. An inner radius has to be the outer one
   less the gap between them, or the inner curve runs tighter than the one it sits in and the two
   read as unrelated shapes rather than one nested in another. Both radii come from properties on
   the tray, so a tray with a different radius stays concentric without anyone recomputing its
   children.
   Placed after `.wp-segmented` rather than beside the tray: `.wp-glass-tray > *` and
   `.wp-segmented` have the same specificity, so this only wins by coming later. */
.wp-glass-tray > * {
    border-radius: calc(var(--wp-glass-tray-radius) - var(--wp-glass-tray-inset));
}

/* One level further in: the segments sit inside the control's own trough as well. */
.wp-glass-tray .wp-segmented a {
    border-radius: calc(var(--wp-glass-tray-radius) - var(--wp-glass-tray-inset) - var(--wp-segmented-inset));
}

/* ========================================================================= table ==
 * A ranking table as a grouped list: standings, a ranking forecast, anything that is
 * a numbered list of players.
 *
 * A real <table>, deliberately, where iOS draws a List. This data is tabular: a
 * screen reader announcing "Points, 305" when the user arrows onto a cell beats a
 * flat row of unlabelled numbers, and column headers are a web affordance a phone
 * has no room for. The inset-grouped look is entirely CSS; none of it needs divs.
 */
.wp-table {
    /* One value for the row highlight and for the separators that bound it, so "the same colour as
       the hovered row" stays true by construction rather than by two numbers agreeing today.
       Opaque, mixed into the card surface rather than into `transparent`: a translucent separator
       painted over a translucent highlight composites twice and reappears as a darker line. */
    --wp-table-row-hover: color-mix(in srgb, var(--wp-color-text-secondary) 7%, var(--wp-color-surface-grouped-raised));
    --wp-table-separator: var(--wp-separator);
    border-collapse: collapse;
    color: var(--wp-color-text-primary);
    width: 100%;
}

.wp-table thead th {
    background: var(--wp-color-surface-grouped-raised);
    border-bottom: 1px solid var(--wp-table-separator);
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption-2);
    font-weight: 600;
    letter-spacing: 0.05em;
    padding: var(--wp-space-md) var(--wp-space-xs) var(--wp-space-sm);
    text-align: left;
    text-transform: uppercase;
}

/* `.wp-points` already asks for this, and loses: one class scores under the class-and-two-elements
   above, so a points heading sat left over figures that sit right. Said again, at a weight that
   wins, rather than by loosening the rule every other heading depends on. */
/* The two columns of numbers, whose headings sit over the figures rather than beside them.
   `.wp-stat` and `.wp-points` both say `text-align: right` on themselves, which is enough in the
   body and not in the head: `.wp-table thead th` above is two classes and an element, so it
   outweighs a single class and pulls the heading back to the left. Stated here at the same
   weight, which is the only thing that reaches it. */
.wp-table thead th.wp-points,
.wp-table thead th.wp-stat {
    text-align: right;
}

/* Same collision, the other direction: a column of centred ticks had its heading pulled to the
   left edge of the column by the rule above. */
.wp-table thead th.wp-col-check {
    text-align: center;
}

.wp-table tbody td,
.wp-table tbody th {
    padding: var(--wp-space-sm) var(--wp-space-xs);
    vertical-align: middle;
}

/* The UA centres a `th` and the reset no longer says otherwise, so a row header has to. Left,
   like the column heading above it and like every other cell in the row. */
.wp-table tbody th {
    text-align: left;
}

.wp-table tbody td:first-child,
.wp-table tbody th:first-child,
.wp-table thead th:first-child {
    padding-left: var(--wp-space-lg);
}

.wp-table tbody td:last-child,
.wp-table tbody th:last-child,
.wp-table thead th:last-child {
    padding-right: var(--wp-space-lg);
}

/* A box-shadow on each cell rather than a border on the row, because `border-collapse: collapse`
   eats row borders. Full width, across every column: this was once inset from the leading two
   columns the way a UITableView separatorInset is, but a table whose first column is the month
   rather than a position reads that as two columns missing their rule.
   `> *` rather than `> td`: a row's leading cell is a <th scope="row">, so a rule written for td
   silently skips it and the first column ends up the one column with no separator under it and no
   highlight over it. */
.wp-table tbody tr:not(:last-child) > * {
    box-shadow: inset 0 -1px 0 var(--wp-table-separator);
}

/* `(hover: hover) and (pointer: fine)` because a touch device fires hover on tap and then keeps
   it: without the guard a phone leaves the last row the reader touched permanently highlighted. */
@media (hover: hover) and (pointer: fine) {
    .wp-table tbody tr:not(.wp-table-aside):hover > * {
        background: var(--wp-table-row-hover);
    }

    .wp-table tbody tr:has(.wp-member:only-child):hover {
        cursor: pointer;
    }

    /* The separator takes the highlight's own colour under a hovered row, so the band reads as one
       block rather than as a row with a line drawn through its foot.
       Two rows are involved: the hovered one owns the separator below it, and the row before it
       owns the one above. `:not(:last-child)` is a filter and not decoration - the last row has no
       separator, and without the guard this would give it one. */
    .wp-table tbody tr:not(:last-child):not(.wp-table-aside):hover > *,
    .wp-table tbody tr:not(:last-child):has(+ tr:not(.wp-table-aside):hover) > * {
        box-shadow: inset 0 -1px 0 var(--wp-table-row-hover);
    }
}

/* A row in the table that is not a standing: a promotion, a divider, anything interleaved
   between the real rows. It keeps the separator so it still reads as a break in the list, but it
   takes none of the row affordances, because none of them are true of it. Highlighting a banner
   on hover promises a click that does not exist.
   A class rather than something inferred from the markup, e.g. `:has(> .wp-position)`, because
   the separator above a hovered row needs `:has(+ tr:not(...))` and CSS does not allow one
   `:has()` inside another. */
.wp-table tbody tr.wp-table-aside {
    cursor: default;
}

/* Three columns of one width, which is what the app's own month table gives them: each of its
   three is `maxWidth: .infinity`, so they divide the card evenly however long a month's name or
   a points total happens to be. Left to size themselves they drift, and a column of positions no
   longer lines up under its own heading. */
.wp-table-even {
    table-layout: fixed;
}

.wp-table-even thead th,
.wp-table-even tbody td,
.wp-table-even tbody th {
    width: 33.33%;
}

/* iOS pads positions with transparent zero glyphs so "1" and "223" share a right
   edge. On the web those fake glyphs would be read out loud, so tabular figures plus
   a ch-based width do the same job without them. */
.wp-position {
    font: var(--wp-font-style-subheadline);
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    min-width: 3ch;
    text-align: right;
    white-space: nowrap;
}

/* Two or more members stack; every other column stays on one row. */
.wp-participant {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-sm);
}

/* The whole row is the link, not just the name.
   Done by stretching the existing anchor over the row rather than by putting a click handler on
   the <tr>: this stays a real link, so middle-click, cmd-click, "copy link address" and the
   status bar preview all keep working, and the keyboard still tabs to one focusable thing whose
   focus ring sits on the name where it can be seen.
   `:only-child` is the whole condition: a doubles pair has two members and therefore two
   destinations, so stretching either one across the row would send half the clicks to the wrong
   player. Those rows keep a link per name. */
.wp-table tbody tr {
    position: relative;
}

.wp-participant > .wp-member:only-child::after {
    content: '';
    inset: 0;
    position: absolute;
}

/* A numeric cell: a value with a quieter one beneath it. The standings use it for a
   win-loss record over its percentage, the forecast for this month's points over last
   month's. Fixed width so this column and the one beside it share a trailing edge on
   every row, instead of drifting with how long the name happens to be. */
.wp-stat {
    /* Local, for the same reason as --wp-move-column. Wide enough for "100%" at caption-2
       and no wider: every extra pixel here comes straight out of the name beside it. */
    --wp-stat-column: 58px;
    text-align: right;
    white-space: nowrap;
    width: var(--wp-stat-column);
}

.wp-stat-value {
    display: block;
    font: var(--wp-font-style-caption);
    font-variant-numeric: tabular-nums;
}

.wp-stat-note {
    color: var(--wp-color-text-secondary);
    display: block;
    font: var(--wp-font-style-caption-2);
    font-variant-numeric: tabular-nums;
}

.wp-points {
    font: var(--wp-font-style-subheadline);
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* A country flag beside a player, at FlagView.swift's 21x15 and 2pt corner. The artwork is the
   same set iOS rasterises into its asset catalog, drawn at exactly this aspect, so `contain`
   never has anything to letterbox and `cover` would have nothing to crop; `contain` is the
   honest one of the two and matches `scaledToFit`. `alt` carries the country name, so this is
   never the only place the country is stated. */
.wp-flag {
    border-radius: var(--wp-radius-hairline);
    /* Inline-block so the box survives a table cell: outside a flex container an inline span
       would ignore the size, and a row whose country is unknown would lose the column rather
       than hold it open. */
    display: inline-block;
    flex: none;
    height: 15px;
    object-fit: contain;
    width: 21px;
}

/* A column of ticks rather than figures: the plan comparison. Narrow and centred, because
   what the reader is scanning is which column has a mark in it, not what the mark says. */
.wp-col-check {
    text-align: center;
    width: 20%;
}

.wp-col-check svg {
    color: var(--wp-color-brand-primary);
    height: 18px;
    width: 18px;
}

/* iOS drops the flag and the games record because a phone has no room. A browser at
   720px and up does have room, and the data is already on the page's model, so hiding
   it would be mimicry at the reader's expense. */
.wp-col-wide {
    display: none;
}

/* A column heading in two lengths: the words where they fit, an abbreviation where they do not.
 *
 * `.wp-col-wide` hides whole columns, which is the right answer for a column a phone has no room
 * for at all. This is the other case: the column is wanted on a phone and only its heading is too
 * long -- "Victorias-Derrotas" is eighteen characters over a cell holding "8-2", and the table
 * gave it the width by taking it from the points column, which then had nowhere to go.
 *
 * Two elements rather than one with its text swapped in CSS: the words are translated, and copy
 * that lives in a stylesheet is copy no translator will ever see. */
.wp-label-long {
    display: none;
}

@media (width >= 720px) {
    .wp-col-wide {
        display: table-cell;
    }

    .wp-label-long {
        display: inline;
    }

    .wp-label-short {
        display: none;
    }

    .wp-table tbody td,
    .wp-table tbody th {
        padding-bottom: var(--wp-space-md);
        padding-top: var(--wp-space-md);
    }
}

/* ========================================================================= match ==
 * One played match: side A, a divider, side B. A port of MatchRowView, which is why
 * the scores sit inline on each side rather than in a column of their own: the web
 * table used to centre them between the sides, and reading "who won which set" then
 * meant tracking across the row and back.
 *
 * The winner is marked per side with `data-winner`, not by colour: bold against
 * regular, primary against secondary, plus a check. Colour alone would not survive a
 * printout or a red/green deficiency, and there is no "winner colour" in the palette
 * because a match has no good side and bad side.
 */
/* One day's matches, header included.
   The wrapper is what makes the pinning work: a sticky element sticks only as far as its
   containing block, so a day's header gives way as that day's last card scrolls off and
   the next day's header takes the slot. Without it every header in the list would share
   one containing block and they would stack on top of each other at the top of the
   screen.
   `scroll-margin-top` is for a link landing on a group: the browser would otherwise
   scroll the group's top edge under the fixed nav bar. */
.wp-match-group {
    scroll-margin-top: calc(var(--wp-navbar-height) + env(safe-area-inset-top));
}

/* The gap between one day and the next, and it has to clear the header's sticky `top`, which
   sits a step below the nav bar: a group that begins any higher than that hands its header a
   displaced resting position, where it eats its own bottom margin and touches the first card.
   One value rather than a margin here and a padding inside the group that added to it -- the
   padding was there to space the first group off the nav bar, which `.wp-page` now does for
   every screen alike. */
.wp-match-group + .wp-match-group {
    margin-top: var(--wp-space-xl);
}

/* The day a group of matches was played, floating over its group as that group scrolls
   under it: a `Section` header in a `.listStyle(.plain)` list, which is the one list style
   whose headers pin.
   This is the only place the date appears. The card used to carry it in a meta line of its
   own, repeated identically down a day's worth of cards; the header says it once and keeps
   saying it for as long as that day is on screen, which is what the pinning is for.
   A capsule rather than a bar, and glass rather than an opaque fill. It was opaque because
   rows pass underneath it and had to be hidden; glass is the other answer to the same
   problem -- the rows stay visible, blurred, which is what says the label is floating in
   front of the list rather than cutting a slot out of it. `.wp-glass` carries the material,
   so the capsule is `.wp-date-header .wp-glass` in the markup the way the title bar is
   `.wp-navbar .wp-glass`.
   `top` clears the nav bar by a step rather than butting against it, as the segmented
   tray does: a thing that floats should be seen to float. */
.wp-date-header {
    border-radius: var(--wp-radius-full);
    display: inline-flex;
    font: var(--wp-font-style-subheadline);

    /* `.subheadline.bold()`. The shorthand above resets the weight, so this has to follow it. */
    font-weight: 700;
    margin: 0 0 var(--wp-space-sm);
    padding: var(--wp-space-xs) var(--wp-space-md);
    position: sticky;
    text-transform: uppercase;
    top: calc(var(--wp-navbar-height) + env(safe-area-inset-top) + var(--wp-space-sm));

    /* Under the glass tray at 10, which is a control and has to stay reachable, and well
       under the nav bar at 100. */
    z-index: 5;
}

/* A column of matches. The gap is what separates one card from the next; a list of
   records rather than rows of a dataset, which is what the markup always was. */
.wp-match-list {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-md);
}

/* Bigger than a list row's, because the card is the thing on the screen rather than one line
   of a list: `MatchRowView` and `MatchUpRowView` both take `AvatarView`'s own default, where
   the web was drawing the 28px it uses in a table. Set on the card the way the profile header
   sets its 180, rather than through a size class: one place uses this number, and putting it
   on the public scale would say otherwise. */
.wp-match .wp-avatar {
    --wp-size-avatar: 48px;
}

.wp-match {
    background: var(--wp-color-surface-grouped-raised);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;

    /* `VStack(spacing: 6)`: tighter than the spacing scale's nearest step, and deliberately so --
       the two sides and the rule between them are one block, not three stacked things. */
    gap: 6px;
    padding: var(--wp-space-lg);

    /* The whole card is a link to the match, the way the row is a `Button` on iOS. Nothing
       inside it is underlined for it. */
    text-decoration: none;
}

/* The card goes to the match, so it answers the pointer and the keyboard the way a list
   row does. The outline is inset, because the card is the focus ring's own box. */
@media (hover: hover) and (pointer: fine) {
    a.wp-match:hover {
        background: color-mix(in srgb, var(--wp-color-text-secondary) 7%, var(--wp-color-surface-grouped-raised));
        color: var(--wp-color-text-primary);
    }
}

/* Where the match was played and in what. Above the sides, quiet, because it is the
   same for every match in a day's list and the players are what is being scanned. */
.wp-match-meta {
    color: var(--wp-color-text-secondary);
    display: flex;
    flex-wrap: wrap;
    font: var(--wp-font-style-caption-2);
    gap: var(--wp-space-xs) var(--wp-space-sm);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* The tournament link sits in the meta line and should not shout; same token as the
   text around it rather than `inherit`, which would re-inherit whatever a host page
   had set. */
.wp-match-meta a {
    color: var(--wp-color-text-secondary);
}

.wp-match-side {
    align-items: center;
    display: flex;
    gap: var(--wp-space-sm);
    justify-content: space-between;
}

.wp-match-players {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-xs);
    min-width: 0;
}

/* A linked player's name reads the same whether they won or lost. `nameStack` takes no notice
   of the result at all -- the first name is secondary and the last name primary, both regular --
   and what carries the outcome is the tick and the score beside them. The web used to bold and
   dim the last name as well, which said the same thing three times and made a doubles pair's
   four names flicker between weights down a list. */
.wp-match-side .wp-name-last {
    font-weight: 400;
}

/* A side with no linked profiles is the one case that does change: it has only the one line, so
   that line has to carry the outcome by itself. */
.wp-match-side[data-winner='false'] .wp-name-single {
    color: var(--wp-color-text-secondary);
    font-weight: 400;
}

.wp-match-side[data-winner='true'] .wp-name-single {
    font-weight: 700;
}

/* The rule either side of "vs". A flex row rather than a border so the word sits in
   the gap instead of on top of the line. */
/* MatchUpRowView: a match that has not been played yet. The same card and the same two sides as
 * a played one, so the two read as the same kind of thing, minus everything a result brings --
 * no tick, no score, no winner emphasis, because none of that exists yet.
 *
 * What it adds is the colour bar at the head of each side. It is the only thing telling the two
 * sides apart when neither has won, and it is what the "who contacts whom" note at the foot of
 * the list refers to. Square against the card's edge and rounded away from it, which is what
 * `UnevenRoundedRectangle(bottomTrailingRadius: 3, topTrailingRadius: 3)` draws -- at
 * `radius.hairline` rather than that 3, because a point either way on a 6px-wide bar is not a
 * shape anybody chose and the scale has no step between 2 and 4. The app's own 3 is the one
 * radius its CornerRadius left behind; it should come here too.
 */
.wp-match-up-tint {
    border-radius: 0 var(--wp-radius-hairline) var(--wp-radius-hairline) 0;
    flex: none;
    height: 22px;
    width: 6px;
}

.wp-match-up-tint-a {
    background: var(--wp-color-match-side-a);
}

.wp-match-up-tint-b {
    background: var(--wp-color-match-side-b);
}

/* The bar sits against the card's leading edge, so this side cancels the card's own padding
   there and the rest of the row keeps it.

   `flex-start` and not the played row's `space-between`: the app puts the Spacer AFTER the
   players, and with no result to push against, space-between threw them to the far edge with
   the colour bar stranded on its own. */
.wp-match-up .wp-match-side {
    justify-content: flex-start;
    margin-left: calc(-1 * var(--wp-space-lg));
}

.wp-match-divider {
    align-items: center;

    /* The word takes the secondary text colour and only the rules either side take the border
       one. They were both the border colour, which is a hairline weight: 3.4:1 on white, and
       1.45:1 once the dark appearance turned it into #38383A. */
    color: var(--wp-color-text-secondary);
    display: flex;
    font: var(--wp-font-style-caption-2);
    gap: var(--wp-space-sm);
    text-transform: uppercase;
}

.wp-match-divider::before,
.wp-match-divider::after {
    background: var(--wp-color-border-subtle);
    content: '';
    flex: 1;
    height: 1px;
}

.wp-match-result {
    align-items: center;
    display: flex;
    flex: none;
    gap: var(--wp-space-sm);
}

/* A box the glyph fills, rather than sizing the glyph: the icon comes from the
   library's icon set and should not have to know where it is being used. */
.wp-match-check {
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    flex: none;
    height: 17px;

    /* The app sets no font on this symbol, so it comes out at body size rather than at a caption's. */
    width: 17px;
}

.wp-match-check svg {
    height: 100%;
    width: 100%;
}

/* A set score, with the tie-break games raised beside it the way a scoreboard prints
   them. `align-items: start` is what puts the small number at the top rather than on
   the baseline.
   The family is stated rather than inherited. Every other rule in here takes its type
   from a `font:` shorthand, which carries `--wp-font-family-sans` with it; this one sets
   no shorthand because it has no size of its own to set, so it was picking up whatever
   the host page uses -- Encordats in the first consumer, whose figures are not the ones
   the rest of the card is drawn in. `font-variant-numeric` only lines digits up within
   one family; it cannot make two families agree. */
.wp-match-score {
    align-items: start;
    display: flex;
    font-family: var(--wp-font-family-sans);
    font-variant-numeric: tabular-nums;
    gap: 1px;
}

.wp-match-side[data-winner='true'] .wp-match-score {
    font-weight: 700;
}

.wp-match-side[data-winner='false'] .wp-match-score {
    color: var(--wp-color-text-secondary);
}

.wp-match-score-tiebreak {
    font-size: 10px;
    line-height: 1;
}

/* Walkover and retirement: outcomes with no score to show, so they are stated. */
.wp-match-badge {
    align-items: center;
    border: 1px solid var(--wp-color-border-subtle);

    /* `radius.outline`: the app draws this one tighter than a tile, and at this size a tile radius
       reads as a pill. */
    border-radius: var(--wp-radius-outline);
    color: var(--wp-color-text-secondary);
    display: inline-flex;
    font: var(--wp-font-style-caption);
    gap: 2px;
    padding: var(--wp-space-xs);
    white-space: nowrap;
}

.wp-match-badge svg {
    height: 12px;
    stroke-width: 2;
    width: 12px;
}

/* The web has room for actions the app puts behind a tap on the row. */
.wp-match-actions {
    display: flex;
    gap: var(--wp-space-sm);
    justify-content: flex-end;
}

/* ========================================================================== home ==
 * HomeView.swift: a greeting, then a stack of sections, some of which scroll sideways.
 *
 * No container class of its own any more. It used to carry both a background `.wp-app` already
 * paints and a gutter two steps wider than every other page's, which meant the one screen a
 * visitor lands on was the one screen that did not line up with the rest. It is now
 * `.wp-content .wp-stack` like any other ported screen, and the carousels are the only thing
 * that leaves the gutter -- see `.wp-carousel`.
 */

/* --- greeting -------------------------------------------------------------------- */
/* A row, so the hand can never be left on a line of its own.
 *
 * Inline, the hand is just another thing in the line and wraps like a word: a long greeting -- a
 * long first name, or "Welcome to WePlay!" on a narrow phone -- pushed it onto a second line by
 * itself, waving at nothing. A flex line does not wrap (`flex-wrap` defaults to `nowrap`), so the
 * words and the hand stay together whatever the greeting says.
 *
 * `baseline` rather than `center`: the hand sits on the line the text sits on, which is where it
 * sat as a glyph. The words keep their own wrapping -- they are one flex item and break inside
 * themselves as before -- and the hand stays beside the first line of them. */
.wp-greeting-title {
    align-items: baseline;
    color: var(--wp-color-text-primary);
    display: flex;
    font: var(--wp-font-style-title);
    gap: var(--wp-space-xs);
    margin: 0 0 var(--wp-space-xs);
}

/* Sized from the greeting's own type rather than in pixels, so it stays the height of the words
   it sits beside whatever the title is set at. `1em` against a 28px title is about what the emoji
   it replaces occupied.
   The baseline nudge is the difference between a glyph, which sits on the baseline with its own
   built-in padding, and a box, which does not: without it the hand hangs a couple of pixels low
   against the cap height of the text. */
.wp-waving-hand {
    /* `reset.css` makes every img, svg and video a block, which is right for the pictures that get
       a line of their own and wrong for this one: as a block it took a second line under the
       greeting rather than sitting at the end of it. */
    display: inline-block;

    /* Never the thing that gives way. As a flex item in the greeting it would otherwise shrink to
       make room for a long name, which is the one way a fixed-size mark can go wrong silently. */
    flex: none;
    height: 1em;

    /* For the inline case. It is a no-op inside the greeting's flex line, where `align-items` does
       this job, and still right anywhere the hand is dropped into a run of text. */
    vertical-align: -0.125em;
    width: 1em;
}

.wp-greeting-text {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
    margin: 0;
}

/* --- section header --------------------------------------------------------------- */
/* CarouselSectionHeaderView: a title with an optional "See all" at the far end. */
.wp-section {
    align-items: baseline;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: space-between;
    margin-bottom: var(--wp-space-md);
}

.wp-section-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-3);
    margin: 0;
}

.wp-section-subtitle {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    margin-block: var(--wp-space-sm) var(--wp-space-md);
}

/* A heading followed by its own subtitle gives up the gap it keeps from whatever comes after it.
   `.wp-section` is the row the title sits in and its margin is the space before the card below;
   with a subtitle underneath, that margin landed between the two halves of one heading and the
   8px above the subtitle was added to it, so the line explaining the section read as belonging
   to nothing in particular. The subtitle carries the whole of the gap now: 8px above it, holding
   it to the title it belongs to, and the section's own 12px below it, before the card. */
.wp-section:has(+ .wp-section-subtitle) {
    margin-bottom: 0;
}

.wp-section-link {
    color: var(--wp-color-text-secondary);
    flex: none;
    font: var(--wp-font-style-subheadline);
    text-decoration: none;
}

/* --- carousel ---------------------------------------------------------------------- */
/* The frame a carousel sits in when it has arrows: the scroller, full-bleed as ever, and the two
   controls laid over its ends.

   A wrapper rather than arrows positioned against whatever the carousel's parent happens to be,
   because that parent differs per screen -- a section here, a stack there -- and the arrows have
   to be measured against the scroller itself. The frame is a plain block, so the scroller inside
   it still spans exactly what it spanned before and the full-bleed padding sum is unchanged. */
.wp-carousel-frame {
    position: relative;
}

/* Drawn for a pointer and nobody else.
 *
 * A thumb has the gesture already, and the gutter leaves the next tile peeking to say so; drawing
 * arrows on a phone is chrome over the screen with the least room for it, and iOS draws none. A
 * mouse has no gesture at all -- the wheel is vertical and the scrollbar is hidden -- which is the
 * whole reason these exist.
 *
 * The same query the tab used to use for its hover and the bar buttons still do, so "is this a
 * pointer" is asked the one way throughout. */
/* Not drawn at all by default, so a touch device never gets them and the query below is what
   turns them on rather than something else having to turn them off. */
.wp-carousel-control {
    display: none;
}

@media (hover: hover) and (pointer: fine) {
    .wp-carousel-control {
        -webkit-backdrop-filter: saturate(180%) blur(20px);
        align-items: center;
        backdrop-filter: saturate(180%) blur(20px);
        background: var(--wp-color-material-glass);
        border: 1px solid var(--wp-color-material-glass-border);
        border-radius: var(--wp-radius-full);
        color: var(--wp-color-text-primary);
        cursor: pointer;
        display: inline-flex;
        height: var(--wp-size-navbar-button);
        justify-content: center;

        /* Over the row rather than beside it: a carousel is full-bleed and there is no margin to put
           them in. Centred on the scroller's own height, so a row of short tiles and a row of tall
           ones both get their arrows in the middle of the tiles. */
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        width: var(--wp-size-navbar-button);
        z-index: 1;
    }

    .wp-carousel-control svg {
        height: 20px;
        width: 20px;
    }

    /* On the gutter, which is where the column's content starts, so an arrow sits over the edge of
       the first or last tile rather than out in the full-bleed margin. */
    .wp-carousel-control-previous {
        left: var(--wp-space-sm);
    }

    .wp-carousel-control-next {
        right: var(--wp-space-sm);
    }

    /* What the script sets when there is nothing that way. `[hidden]` and the class have the same
       weight, so without this the author rule above wins and an arrow that leads nowhere is drawn. */
    .wp-carousel-control[hidden] {
        display: none;
    }

    .wp-carousel-control:focus-visible {
        outline: 2px solid var(--wp-color-brand-primary);
        outline-offset: 2px;
    }

    .wp-carousel-control:hover {
        background: color-mix(in srgb, var(--wp-color-brand-primary) 12%, var(--wp-color-material-glass));
    }
}

/* `.contentMargins(.horizontal, 20)` plus `.scrollTargetBehavior(.viewAligned)`: the gutter is
   scroll padding rather than padding, so the first tile starts at the gutter and the last one
   can still be scrolled clear of the edge, and each tile snaps.
   `scroll-padding-inline` is what aligns a snapped tile with the gutter instead of the very
   edge; without it every tile after the first lands 20px too far left. */
.wp-carousel {
    display: flex;
    gap: var(--wp-space-md);

    /* `min-width: 0` so the row can be narrower than the tiles it holds. Without it a flex or grid
       parent sizes this to its content and the overflow moves up the tree to whoever cannot clip
       it, which is how one carousel ends up scrolling the whole screen. */
    min-width: 0;
    overflow-x: auto;

    /* A carousel scrolls one way, and saying so takes both axes. `overflow-x: auto` alone makes it
       a scroll container on BOTH: an unspecified `overflow-y: visible` computes to `auto` as soon
       as the other axis is not visible, so a tile a fraction taller than the row is a few pixels
       of vertical scroll that a diagonal flick finds, and the row is left sitting crooked under its
       heading with no way to tell it to come back. */
    overflow-y: hidden;

    /* Flicking past the end of a carousel must not hand the gesture to the page behind it. That
       chaining is what makes a sideways swipe feel like the whole screen moving, and it is the one
       way a page with nothing overflowing still appears to scroll sideways. */
    overscroll-behavior-x: contain;
    /* A carousel is the one thing on a screen that is NOT in the centred column: it spans its
       parent, so a tile can scroll past where the column ends and the next one peeks. What it does
       keep is the column's leading edge, so the first tile at rest sits exactly under the heading
       above it.
       That inset is where the column's content starts: half the slack either side of the cap, plus
       the gutter, and never less than the gutter once the parent is narrower than the cap -- which
       is the phone, where there is no slack. All percentages, deliberately: an earlier version did
       this with `50vw`, which counts the space a classic scrollbar reserves and so pushed the row a
       few pixels left of the heading and the same few over the right edge. A percentage resolves
       against the parent, which has already had the scrollbar taken out of it. */
    padding-block: 0;
    padding-inline: max(var(--wp-space-lg), (100% - var(--wp-width-content)) / 2 + var(--wp-space-lg));
    scroll-padding-inline: max(var(--wp-space-lg), (100% - var(--wp-width-content)) / 2 + var(--wp-space-lg));
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
}

.wp-carousel::-webkit-scrollbar {
    display: none;
}

.wp-carousel > * {
    flex: none;
    scroll-snap-align: start;
}

/* --- tiles --------------------------------------------------------------------------- */
/* A card sized for a carousel. The widths are iOS's: 320 for a hero, 280 for a season card,
   220 for a discovery card, all narrow enough that the next one peeks. */
.wp-tile {
    width: 280px;
}

.wp-tile-sm {
    width: 220px;
}

.wp-tile-lg {
    width: 320px;
}

/* --- hero ----------------------------------------------------------------------------- */
/* The dominant card: the next match, or the nudge that stands in for it.
 *
 * CourtPatternBackground is a diagonal brand gradient with the logo set into the bottom-right
 * corner at 16%, rotated. The watermark is a background-image rather than an element so the
 * card's content does not have to step around it, and it is the app's own logo rather than a
 * new asset.
 */
/* The measurements are NextMatchHeroCard's own. The 28 is `radius.hero`, a step of its own
   precisely because it is not `radius.card`; 20 is still not `space.xl` and 210 is still not
   anything, so a change to the spacing scale must not move them. */
.wp-hero {
    background: linear-gradient(135deg, var(--wp-color-brand-primary), color-mix(in srgb, var(--wp-color-brand-primary) 78%, transparent));
    border-radius: var(--wp-radius-hero);
    color: var(--wp-color-text-on-brand);
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-lg);
    min-height: 210px;
    overflow: hidden;
    padding: 20px;
    position: relative;
    text-decoration: none;
}

/* `.offset(x: 54, y: 54)` past the bottom-trailing corner, which is what puts the mark half off
   the card rather than sitting in it. */
.wp-hero-watermark {
    /* A parameter rather than a constant, because `BrandPrimaryBackground` takes one: the face-off
       card dials it down, since there the content sits directly on top of the mark. */
    --wp-hero-watermark-opacity: 0.16;
    bottom: -54px;
    color: var(--wp-color-text-on-brand);
    height: 180px;
    opacity: var(--wp-hero-watermark-opacity);
    pointer-events: none;
    position: absolute;
    right: -54px;
    transform: rotate(-35deg);
    width: 180px;
}

/* The mark is the consuming app's own logo, which arrives at whatever intrinsic size it was
   drawn at. Without this it keeps that size and the card clips a corner of it, which reads as a
   stray arc rather than as a watermark. */
.wp-hero-watermark svg {
    height: 100%;
    width: 100%;
}

/* The capsule that says what kind of card this is. */
.wp-hero-label {
    align-items: center;
    align-self: flex-start;
    background: color-mix(in srgb, var(--wp-color-text-on-brand) 22%, transparent);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-on-brand);
    display: inline-flex;
    font: var(--wp-font-style-caption);
    font-weight: 600;
    gap: 6px;
    padding: 6px var(--wp-space-md);
}

.wp-hero-label svg {
    height: 13px;
    stroke-width: 2;
    width: 13px;
}

/* The schedule, opposite the label. */
.wp-hero-meta {
    align-items: flex-start;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: space-between;
}

/* Two lines, day over time, 2px apart and uppercase. The label beside it is NOT uppercase: on
   iOS only the schedule carries `.textCase(.uppercase)`. */
.wp-hero-when {
    align-items: flex-end;
    color: var(--wp-color-text-on-brand);
    display: flex;
    flex: none;
    flex-direction: column;
    font: var(--wp-font-style-caption);
    font-weight: 600;
    gap: 2px;
    text-align: right;
    text-transform: uppercase;
    white-space: nowrap;
}

/* These two are <p>, and a <p> arrives with whatever bottom margin the host gives it: 1rem in
   weplay-app-web, which pushed the three lines of this card 16px apart where iOS has 10 and 2.
   The library is not supposed to inherit layout from the page it is dropped into, and the
   gallery hid it, because that page has no element defaults for them to inherit. */
.wp-hero-eyebrow {
    color: color-mix(in srgb, var(--wp-color-text-on-brand) 90%, transparent);
    font: var(--wp-font-style-caption);
    font-weight: 600;
    margin: 0 0 10px;
    text-transform: uppercase;
}

.wp-hero-title {
    color: var(--wp-color-text-on-brand);
    font: var(--wp-font-style-title-2);
    margin: 0;
}

.wp-hero-text {
    color: color-mix(in srgb, var(--wp-color-text-on-brand) 85%, transparent);
    font: var(--wp-font-style-subheadline);
    margin: 0;
}

/* `margin-top: auto` rather than a Spacer: the call to action sits at the foot of the card
   whether the content above it is two lines or five. */
.wp-hero-cta {
    align-items: center;
    align-self: flex-start;

    /* White in both appearances, which is what `.background(Color.white, in: Capsule())` draws.
       It was `surface.raised`, so it inverted to near-black in the dark appearance: correct for a
       card on a page, wrong for a capsule sitting on the brand fill, which is brand-coloured in
       both. `text.on-brand` is the token that means "drawn on top of the brand" and is white
       either way. */
    background: var(--wp-color-text-on-brand);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-brand-primary);
    display: inline-flex;
    font: var(--wp-font-style-subheadline);
    font-weight: 600;
    gap: 6px;
    margin-top: auto;
    padding: 10px var(--wp-space-lg);

    /* The dashboard's hero is itself one big <a>, so this capsule was never an anchor there and
       never needed to say so. On the pricing page it is the anchor, and the host's Reboot
       underlines a bare one. */
    text-decoration: none;
}

.wp-hero-cta svg {
    height: 18px;
    width: 18px;
}

.wp-hero-cta-block {
    align-self: stretch;
    font: var(--wp-font-style-headline);
    justify-content: center;
    padding: var(--wp-space-lg) var(--wp-space-xl);
}

/* The plan you are already on, in the capsule the action would have used: it holds the place
   the action held, because a gap there reads as something missing. A translucent fill of the
   card's own white rather than a dimmed white pill -- the same 22% `.wp-hero-label` wears --
   so the words stay white on the gradient and keep their contrast instead of fading into it. */
.wp-hero-cta[aria-disabled='true'] {
    background: color-mix(in srgb, var(--wp-color-text-on-brand) 22%, transparent);
    color: var(--wp-color-text-on-brand);
    cursor: default;
}

/* The arrow leans into the direction it points when the pointer is over it. A transform and
   not a margin, so nothing around it moves; `(hover: hover) and (pointer: fine)` because a
   touch device fires hover on tap and then keeps it, leaving the arrow permanently nudged. */
@media (hover: hover) and (pointer: fine) {
    .wp-hero-cta:hover svg {
        transform: translateX(3px);
    }
}

@media (prefers-reduced-motion: no-preference) {
    .wp-hero-cta svg {
        transition: transform 0.15s ease;
    }
}

/* The profile nudge: the same card, but a wash on the page's own surface rather than the brand
   fill. It asks for something rather than announcing something, and two full-brand cards in a
   row would make neither of them the hero. */
.wp-hero-tint {
    background: linear-gradient(
            90deg,
            var(--wp-color-surface-base),
            color-mix(in srgb, var(--wp-color-brand-primary) 8%, var(--wp-color-surface-base)),
            color-mix(in srgb, var(--wp-color-brand-primary) 18%, var(--wp-color-surface-base))
    );
    color: var(--wp-color-text-primary);
}

.wp-hero-tint .wp-hero-label {
    background: color-mix(in srgb, var(--wp-color-brand-primary) 15%, transparent);
    color: var(--wp-color-brand-primary);
}

.wp-hero-tint .wp-hero-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-3);
}

.wp-hero-tint .wp-hero-text {
    color: var(--wp-color-text-secondary);
}

.wp-hero-tint .wp-hero-cta {
    background: var(--wp-color-brand-primary);
    color: var(--wp-color-text-on-brand);
}

/* The opponent, on the next-match card. */
.wp-hero-opponent {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
}

/* A pair overlaps rather than sitting side by side: the second avatar is 32px along and 1.03
   times the size, which is what makes it read as the one in front. */
.wp-hero-avatars {
    /* On the container and not on the avatars, so the ring wrapper is the same width as the avatar
       it wraps — that width is what the overlap below is measured against — and so the second
       avatar's own larger size can override it by inheritance. */
    --wp-size-avatar: 52px;
    align-items: center;
    display: flex;
    flex: none;
    position: relative;
}

/* `.white.opacity(0.5)` with `borderGap: 3`, and the gap filled with the card's own colour. iOS
   fills it only on the second avatar, where it is what stops the one behind showing through the
   gap; the first has nothing behind it but the card, so filling both is the same picture and one
   rule fewer. */
.wp-hero-avatars .wp-avatar-ring {
    --wp-avatar-ring-color: color-mix(in srgb, var(--wp-color-text-on-brand) 50%, transparent);
    --wp-avatar-ring-gap-fill: var(--wp-color-brand-primary);
}

/* `placeholderFill` is the card's text colour at 0.2, so a player with no picture reads as part of
   the card rather than a hole in it. */
.wp-hero-avatars .wp-avatar {
    background: color-mix(in srgb, var(--wp-color-text-on-brand) 20%, transparent);
}

/* `secondAvatarSize = avatarSize * 1.03` and `avatarOverlap = 32`. The overlap is between the two
   avatars' leading edges, so the margin subtracts the FIRST one's width and the second being the
   larger of the two does not come into it. Sized rather than scaled, because `scale` would take
   the ring's 1.5px with it. */
.wp-hero-avatars .wp-avatar-ring + .wp-avatar-ring {
    --wp-size-avatar: 53.56px;
    margin-left: -20px;
}

.wp-hero-versus {
    color: color-mix(in srgb, var(--wp-color-text-on-brand) 85%, transparent);
    font: var(--wp-font-style-subheadline);
    margin: 0;
}

/* `spacing: 2` between the "You vs." line and the opponent's name. */
.wp-hero-versus + .wp-hero-title {
    margin-top: 2px;
}

/* --- recent form ------------------------------------------------------------------- */
/* MatchFormCard: a row of results and a line about how it is going. */
.wp-streak {
    align-items: center;
    display: flex;
    gap: 10px;
    margin-bottom: var(--wp-space-md);
}

.wp-streak svg {
    height: 22px;
    width: 22px;
}

.wp-streak-win {
    color: var(--wp-color-feedback-success);
}

.wp-streak-loss {
    color: var(--wp-color-feedback-danger);
}

.wp-streak-headline {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    margin: 0;
}

/* --- season ranking tile ------------------------------------------------------------ */
.wp-rank {
    align-items: center;
    display: flex;
    gap: var(--wp-space-sm);
    margin: var(--wp-space-sm) 0;
}

.wp-rank-position {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-large-title);
    font-variant-numeric: tabular-nums;
}

/* --- ranking history -------------------------------------------------------------- */
/* The shape of a season, as an SVG the server wrote: monthly positions, smoothed, with a
 * gradient fading under the line.
 *
 * `preserveAspectRatio="none"` in the markup is what lets one 100x100 path stretch to a card of
 * any width, and `vector-effect: non-scaling-stroke` on the line is what keeps the stroke an
 * even weight through that stretch rather than fattening horizontally with it.
 *
 * Decorative: every number it encodes is already written on the card as text, so the markup
 * hides it from a screen reader rather than saying it twice. */
.wp-sparkline {
    display: block;
    height: 48px;
    overflow: visible;
    width: 100%;
}

.wp-sparkline-sm {
    height: 24px;
}

.wp-sparkline-line {
    fill: none;
    stroke: var(--wp-color-brand-primary);
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
}

.wp-sparkline-area {
    fill: url('#wp-sparkline-fill');
    stroke: none;
}

/* The fade's two stops, coloured here rather than in the markup so a theme switch moves them.
 *
 * Not `currentColor`: a gradient stop resolves it against the gradient's own element, and the
 * gradient lives in a standalone `<svg>` of definitions rather than inside the card that paints
 * with it, so it came out the colour of the page's text instead of the brand's blue. */
.wp-sparkline-fill-from {
    stop-color: var(--wp-color-brand-primary);
    stop-opacity: 0.35;
}

.wp-sparkline-fill-to {
    stop-color: var(--wp-color-brand-primary);
    stop-opacity: 0;
}

/* The "more to come" shape a first month gets, in the line's place: this month as a solid dot,
 * then three faint ones rising away from it on a dashed line. It encodes nothing - the dots are
 * at fixed positions and rise only because up means better here - which is why it is its own
 * shape rather than a line through one point. An empty gap on the card reads as a graph that
 * failed to load, because it was reported as one. */
.wp-sparkline-projection {
    fill: none;
    stroke: color-mix(in srgb, var(--wp-color-brand-primary) 35%, transparent);
    stroke-dasharray: 4 4;
    stroke-linecap: round;
    stroke-width: 1.5;
    vector-effect: non-scaling-stroke;
}

/* The dots are zero-length subpaths with a round cap rather than circles: the box is stretched
   to the card's width, which turns a circle into an ellipse, and a stroke is the one thing that
   can opt out of that stretch. The width is the dot's diameter. */
.wp-sparkline-dot {
    stroke: color-mix(in srgb, var(--wp-color-text-secondary) 35%, transparent);
    stroke-linecap: round;
    stroke-width: 6;
    vector-effect: non-scaling-stroke;
}

.wp-sparkline-dot-current {
    stroke: var(--wp-color-brand-primary);
}

.wp-sparkline-sm .wp-sparkline-dot {
    stroke-width: 5;
}

/* The tinted capsule beside a position: how far it moved since last month.
 *
 * The arrow carries the direction on its own, so colour is never the only signal - which matters
 * for the ~8% of men with a red/green deficiency and for anyone reading a printout. A position
 * that held is neutral grey rather than either direction's colour, and a position with nothing
 * to compare against draws no pill at all, which is a different thing from an unchanged one. */
.wp-change-pill {
    --wp-change-pill-tint: var(--wp-color-text-secondary);

    background: color-mix(in srgb, var(--wp-change-pill-tint) 15%, transparent);
    border-radius: var(--wp-radius-full);
    color: var(--wp-change-pill-tint);
    display: inline-flex;
    flex: none;
    font: var(--wp-font-style-caption);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    gap: 2px;
    padding: 4px var(--wp-space-sm);
}

.wp-change-pill[data-direction='up'] {
    --wp-change-pill-tint: var(--wp-color-ranking-up);
}

.wp-change-pill[data-direction='down'] {
    --wp-change-pill-tint: var(--wp-color-ranking-down);
}

/* An active event's card: which event, how the season went, where that leaves you, in that order
 * down the card. The position is the loudest thing on it on purpose - it is the number the
 * reader came for, and it used to be a small grey label at the end of a row. */
.wp-season {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-md);
}

.wp-season-head {
    align-items: baseline;
    display: flex;
    gap: var(--wp-space-md);
}

.wp-season-text {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.wp-season-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    margin: 0;
}

/* Pinned to one line. The line is as wide as whatever month it names, so left free to wrap it
   changes height as the month rolls over, which moves the position and the line above it. */
.wp-season-subtitle {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wp-season-figure {
    align-items: center;
    display: flex;
    gap: var(--wp-space-sm);
}

/* The same card as a square tile, which is what a player with more than one event still running
   gets. The position is pinned to the foot however short the name above it is, so a row of them
   reads as a row of positions rather than as ragged cards of different heights.
   `min-height` rather than `height`: at a larger text size the tile grows taller and keeps its
   width, so the name and the position stay readable instead of truncating to stay square. */
.wp-season-tile {
    min-height: 220px;
    width: 220px;
}

.wp-season-tile .wp-season-figure {
    margin-top: auto;
}

/* --- ranking history, one event ------------------------------------------------------ */
/* The screen's headline: the number the reader came for, the month it belongs to, the other
   number underneath, and how far it moved. Sized against the widest value the season can show
   rather than the one on screen, so picking a different month cannot change the headline's width
   and shuffle everything under it - which is what `reservedPositionWidth` does on the app. Here
   that is `ch` units on tabular digits, which is the same promise without measuring anything. */
.wp-headline {
    align-items: flex-start;
    display: flex;
    gap: var(--wp-space-md);
}

.wp-headline-text {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: var(--wp-space-xs);
    min-width: 0;
}

.wp-headline-primary {
    align-items: baseline;
    display: flex;
    gap: var(--wp-space-sm);
}

.wp-headline-value {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-large-title);
    font-variant-numeric: tabular-nums;

    /* Four figures and a hash, which is the widest a single event's standing realistically gets.
       Reserved rather than fitted, so the month beside it never moves when the reader picks a
       different one. */
    min-width: 5ch;
}

.wp-headline-month {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wp-headline-secondary {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-3);
    font-variant-numeric: tabular-nums;
    margin: 0;
}

/* The season's chart. Straight between months and a marker on each, where the sparkline is
   smoothed and bare: this one carries labels a reader compares against, and a curve would bend
   away from the month it is labelled with.
   The box is stretched to the card's width the same way, so the line opts out of the stretch and
   every marker is drawn as a round cap on a zero-length segment rather than as a circle, which
   would come out an ellipse. */
.wp-chart {
    display: block;
    height: 180px;
    overflow: visible;
    width: 100%;
}

.wp-chart-line {
    fill: none;
    stroke: var(--wp-color-brand-primary);
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
}

.wp-chart-area {
    fill: url('#wp-sparkline-fill');
    stroke: none;
}

/* The mean, which the app draws as a dashed rule with its value written above it. */
.wp-chart-average {
    fill: none;
    stroke: color-mix(in srgb, var(--wp-color-text-secondary) 70%, transparent);
    stroke-dasharray: 5 5;
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

.wp-chart-caption {
    color: var(--wp-color-text-secondary);
    display: flex;
    font: var(--wp-font-style-caption);
    gap: var(--wp-space-md);
    justify-content: space-between;
    margin: var(--wp-space-sm) 0 0;
}

/* --- discovery tile ------------------------------------------------------------------ */
/* The brand band at the top of a tournament tile, with the event's logo or a trophy on it. */
.wp-banner {
    background: linear-gradient(135deg, var(--wp-color-brand-primary), color-mix(in srgb, var(--wp-color-brand-primary) 78%, transparent));
    color: var(--wp-color-text-on-brand);
    display: grid;
    height: 72px;
    place-items: center;
}

.wp-banner img {
    height: 52px;
    object-fit: contain;
    width: 52px;
}

.wp-banner svg {
    height: 28px;
    width: 28px;
}

/* The WhatsApp and Instagram cards are the same tile with somebody else's colours on the band.
   The mark is the service's own app icon at 52px and slightly knocked back, which is what the
   app draws; the trophy beside it is a 28px glyph, because that one stands in for a thing rather
   than being somebody's logo. */
.wp-banner-whatsapp img,
.wp-banner-instagram img {
    opacity: 0.9;
}

.wp-banner-whatsapp {
    background: linear-gradient(
            135deg,
            color-mix(in srgb, var(--wp-color-brand-external-whatsapp) 85%, transparent),
            color-mix(in srgb, var(--wp-color-brand-external-whatsapp) 55%, transparent)
    );
}

.wp-banner-instagram {
    background: linear-gradient(
            135deg,
            color-mix(in srgb, var(--wp-color-brand-external-instagram-start) 90%, transparent),
            color-mix(in srgb, var(--wp-color-brand-external-instagram-middle) 80%, transparent),
            color-mix(in srgb, var(--wp-color-brand-external-instagram-end) 85%, transparent)
    );
}

.wp-tile-body {
    align-items: flex-start;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: var(--wp-space-md);
}

.wp-tile-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-subheadline);
    font-weight: 600;
    margin: 0;
}

.wp-tile-text {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    margin: 0;
}

/* A status word on a tile. Grey rather than tinted: "open" is the only value it ever takes, so
   a colour would be decoration carrying no information. */
.wp-tile-badge {
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption-2);
    font-weight: 600;
    padding: 3px var(--wp-space-sm);
    text-transform: uppercase;
}

/* ====================================================================== skeleton ==
 * The equivalent of .redacted(reason: .placeholder). Blocks are sized to realistic
 * content widths at the call site so the layout does not jump when the real rows
 * land.
 */
.wp-skeleton {
    animation: wp-pulse 1.4s ease-in-out infinite;
    background: var(--wp-color-surface-skeleton);
    border-radius: var(--wp-radius-outline);
    display: inline-block;
    height: 0.75em;
}

.wp-skeleton-avatar {
    border-radius: 0;
    clip-path: url(#wp-avatar-shape);
    height: var(--wp-size-avatar);
    width: var(--wp-size-avatar);
}

@keyframes wp-pulse {
    0%,
    100% {
        opacity: 1;
    }

    50% {
        opacity: 0.45;
    }
}

@media (prefers-reduced-motion: reduce) {
    .wp-skeleton {
        animation: none;
    }
}

/* ======================================================================== empty ==
 * ContentUnavailableView. Narrow measure on the description because a centred
 * paragraph running the full width of a desktop card is unreadable.
 */
.wp-empty {
    padding: 56px var(--wp-space-xl);
    text-align: center;
}

/* `margin-inline: auto` and not the `text-align: center` above: reset.css lays an SVG out as a
   block, the way it does an image, and a block box is centred by its own margins rather than by
   the text alignment of the box around it. The glyph sat flush against the leading edge with the
   title and the description centred under it. */
.wp-empty svg {
    color: var(--wp-color-text-secondary);
    height: 44px;
    margin: 0 auto var(--wp-space-md);
    width: 44px;
}

.wp-empty-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    margin: 0 0 var(--wp-space-xs);
}

.wp-empty-description {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    margin: 0 auto;
    max-width: 34ch;
}

/* ======================================================================== screen ==
 * A screen with no app chrome: signing in, signing up, the password flows, an error.
 *
 * Not everything is a tab. iOS puts all of this behind a sheet or a push with no TabView
 * under it, because none of it is a place you move back and forth between; it is one
 * errand with one way out. So the shell's bars are absent here deliberately rather than
 * by omission, and the column centres itself at a readable measure instead of filling
 * the width, which is what stops a twenty-character field being twelve hundred pixels
 * wide on a laptop.
 */
.wp-screen {
    background: var(--wp-color-surface-grouped);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
    /* The top inset gets a space unit added to it the way the bottom one does. On a phone the
       inset alone already clears the notch, but on every other viewport it resolves to zero and
       whatever the header holds -- on this app, the logo -- sat flush against the top edge. */
    padding: calc(var(--wp-space-lg) + env(safe-area-inset-top)) var(--wp-space-lg) calc(var(--wp-space-lg) + env(safe-area-inset-bottom));
}

.wp-screen-header,
.wp-screen-content,
.wp-screen-footer {
    margin: 0 auto;
    max-width: 420px;
    width: 100%;
}

.wp-screen-header {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: center;
    min-height: var(--wp-size-touch-target);
}

/* `flex: 1` with `justify-content: center` rather than a top margin: a short form sits in
   the middle of the screen the way the sheet does, and a long one grows downwards from
   its natural height instead of being pushed off the bottom. */
.wp-screen-content {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: var(--wp-space-lg);
    justify-content: center;
    padding: var(--wp-space-lg) 0;
}

.wp-screen-footer {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    padding-top: var(--wp-space-lg);
    text-align: center;
}

/* The headline above the fields. `title`, not `large-title`: a large title belongs to a
   scrolling screen that shrinks it into the nav bar, and there is no nav bar here. */
.wp-screen-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title);
    margin: 0 0 var(--wp-space-xs);
}

.wp-screen-text {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-body);
    margin: 0 0 var(--wp-space-md);
}

.wp-screen-text:last-child {
    margin-bottom: 0;
}

/* --- error screen ----------------------------------------------------------------------- */
/* What an error screen adds to the one above it. Five of them -- a 500, a 404, a 403, no
   connection, maintenance -- are one template driven by a type, so none of this knows which it
   is drawing; the copy and the buttons change and the shape does not.
 *
 * Centred, which the sign-in screen is not: that one is a form, read top-down against a left
 * edge, and this one is a sentence with two buttons under it. */
.wp-screen-message {
    /* The optical centre rather than the true one. `.wp-screen-content` centres what it holds
       between the header and the footer, and a block centred there reads as sitting low, because
       the eye puts the middle of a page above its middle. The margin is the only thing that moves
       it, and it moves it by half of itself. */
    margin-bottom: var(--wp-space-2xl);
    text-align: center;
}

/* `large-title`, one step above `.wp-screen-title`. The sign-in screen deliberately stops at
   `title` -- a large title belongs to a scrolling screen that shrinks it into the nav bar -- but
   an error screen's headline is one word and that word is the whole page. The long headlines
   keep plain `.wp-screen-title`, so they wrap to two lines on a phone rather than to four. */
.wp-screen-title-lg {
    font: var(--wp-font-style-large-title);
}

/* The error reference in the footer: short, selectable, and copied by tapping it.
 *
 * Outlined rather than set as bare text, because a code somebody is asked to read back needs an
 * edge to tell it from the sentence around it -- the same outline `.wp-status` wears, which is
 * what the system already has for a token-sized fact.
 *
 * A <button>, so the tap has somewhere to land, and its own chrome is reset here: the library's
 * reset deliberately stops at a control's typeface and leaves the rest to whichever component
 * the control is wearing. */
.wp-screen-reference {
    background: none;
    border: 1px solid var(--wp-color-border-subtle);
    border-radius: var(--wp-radius-outline);
    color: var(--wp-color-text-secondary);
    cursor: pointer;
    font: var(--wp-font-style-caption);
    font-variant-numeric: tabular-nums;
    padding: var(--wp-space-xs) var(--wp-space-sm);
}

/* ========================================================================== plan ==
 * A subscription plan on the pricing page: what it is called, what it costs, what it gets
 * you, and the one thing you can do about it.
 *
 * Another component with no SwiftUI view behind it. iOS has a Subscriptions screen, but it
 * lists the ones you already hold; nothing in the app sells you one, because selling a
 * subscription outside Apple's own purchase flow is not a thing an app may do. So the
 * pricing page is the web's alone, and this is what it is made of.
 */

/* The page's own opening: an eyebrow, a headline and a sentence, centred over the cards.
   Named `plan-hero` and not `hero`, because `.wp-hero` already means the brand-filled card
   on the dashboard and the two are different things that would otherwise share a prefix. */
.wp-plan-hero {
    margin-inline: auto;
    max-width: 620px;
    text-align: center;
}

/* The capsule over the headline. A wash of the brand rather than a fill of it, the same way
   `.wp-button-tinted` is: it labels the page, it does not compete with the card below. */
.wp-plan-hero-eyebrow {
    background: color-mix(in srgb, var(--wp-color-brand-primary) 12%, transparent);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-brand-primary);
    display: inline-block;
    font: var(--wp-font-style-caption);
    font-weight: 700;
    letter-spacing: 0.08em;
    margin: 0;
    padding: 6px var(--wp-space-md);
    text-transform: uppercase;
}

/* `clamp` rather than a step, and the one place in this stylesheet that does it: this
   headline is the only type here that has to read as a poster at 1040px and still fit "Get
   the most out of WePlay" on two lines at 375px. A fixed step does one or the other.
   `large-title` first so the family and weight still come from the scale; only the size and
   the leading are overridden, because 34/41 is the wrong ratio once the size is variable. */
.wp-plan-hero-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-large-title);
    font-size: clamp(28px, 5vw, 40px);
    letter-spacing: -0.02em;
    line-height: 1.1;
    margin: var(--wp-space-lg) 0 var(--wp-space-md);
}

.wp-plan-hero-text {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-body);
    margin: 0;
}

/* Two columns from 680px up, one below it, and the order is not the source order: Core
   reads first on a wide screen because a left-to-right comparison runs cheapest-first, and
   Plus reads first on a phone because a stack holds no comparison, only a sequence, and the
   recommended plan should not be the one you have to scroll to.
   `auto-fit` above the breakpoint rather than two fixed columns: there are two plans today
   and the page should not have to be rewritten when there is one, or three. */
.wp-plan-list {
    display: grid;
    gap: var(--wp-space-lg);
    grid-template-columns: 1fr;

    /* Twice the largest step under it, which is the one gap on this page that is not a gap
       between two parts of the same thing: above it is the offer, below it the small print, and
       at one step the comparison's heading read as a caption on the cards. */
    margin-block: var(--wp-space-2xl) calc(var(--wp-space-2xl) * 2);
}

@media (width >= 680px) {
    .wp-plan-list {
        grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    }
}

/* Left-aligned, where this card used to be centred: it now carries a list of what the plan
   includes, and a centred bulleted list gives the eye no edge to come back to. */
.wp-plan {
    border-radius: var(--wp-radius-hero);
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-xs);
    padding: var(--wp-space-xl);
    position: relative;
}

/* The recommended plan, drawn the way the dashboard's hero is drawn: the brand gradient, and
   no border and no shadow, so the one card that is selling something is the one card that is
   not made of the same surface as the page. The radius is on `.wp-plan`: both cards take it,
   because they sit side by side. */
.wp-plan-featured {
    background: linear-gradient(135deg, var(--wp-color-brand-primary), color-mix(in srgb, var(--wp-color-brand-primary) 78%, transparent));
    color: var(--wp-color-text-on-brand);
    order: 1;
}

/* The plain plan, which is `.wp-card` and nothing else: the card surface already separates it
   from the page, and a hairline on top of that read as a second edge drawn just inside the
   first. It is here for the order alone, which the stylesheet flips at the breakpoint. */
.wp-plan-plain {
    order: 2;
}

@media (width >= 680px) {
    .wp-plan-featured {
        order: 2;
    }

    .wp-plan-plain {
        margin: var(--wp-space-lg) 0;
        order: 1;
    }
}

/* The one accent colour this product has outside the brand blue, named rather than written
   in place: a literal repeated is how two things that should match drift apart. Not a
   semantic token, because nothing outside this card uses it and the token file is shared
   with three other clients that have no pricing screen -- `--wp-medal-gold` is here for the
   same reason. The ink is part of the pair and not a separate choice: #1a2e05 on #c6f432 is
   12.4:1, where white on it is 1.5:1 and unreadable. */
:root {
    --wp-plan-accent: #c6f432;
    --wp-plan-accent-ink: #1a2e05;
}

.wp-plan-badge {
    background: var(--wp-plan-accent);
    border-radius: var(--wp-radius-full);
    color: var(--wp-plan-accent-ink);
    font: var(--wp-font-style-caption-2);
    font-weight: 700;
    letter-spacing: 0.06em;
    margin: 0;
    padding: var(--wp-space-xs) var(--wp-space-sm);
    position: absolute;
    right: var(--wp-space-lg);
    text-transform: uppercase;
    top: var(--wp-space-lg);
}

/* The quiet line above the price. Stated twice rather than left to inherit, because
   `color: inherit` in this library re-inherits the HOST page's ink and not a token. */
.wp-plan-name {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    letter-spacing: 0.08em;
    margin: 0;
    text-transform: uppercase;
}

.wp-plan-featured .wp-plan-name {
    color: var(--wp-color-text-on-brand);
    opacity: 0.75;
}

/* The number is the whole point of the card, so it gets the one type size nothing else on
   a grouped screen uses. The period sits on its baseline rather than under it, which is
   what `align-items: baseline` is for and why this is a row and not two lines. */
.wp-plan-price {
    align-items: baseline;
    color: var(--wp-color-text-primary);
    display: flex;
    flex-wrap: wrap;
    font: var(--wp-font-style-large-title);
    gap: 6px;
    margin: var(--wp-space-md) 0 0;
}

.wp-plan-featured .wp-plan-price {
    color: var(--wp-color-text-on-brand);
}

.wp-plan-period {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
}

.wp-plan-featured .wp-plan-period {
    color: var(--wp-color-text-on-brand);
    opacity: 0.75;
}

/* "Less than EUR 2 a month", "Forever": the line that makes the number mean something.
   `min-height` so the two cards' feature lists start level even though only one card has
   anything to say here. */
.wp-plan-note {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
    margin: 0;
    min-height: 20px;
}

.wp-plan-featured .wp-plan-note {
    color: var(--wp-color-text-on-brand);
    opacity: 0.75;
}

.wp-plan-feature-list {
    display: grid;
    gap: var(--wp-space-md);
    list-style: none;
    margin: var(--wp-space-xl) 0 var(--wp-space-xl);
    padding: 0;
}

/* `align-items: flex-start` and a nudge on the glyph, not `center`: a bullet that wraps to
   two lines would otherwise centre its tick against both of them and leave it floating in
   the middle of the sentence. */
.wp-plan-feature {
    align-items: flex-start;
    color: var(--wp-color-text-primary);
    display: flex;
    font: var(--wp-font-style-subheadline);
    gap: var(--wp-space-sm);
}

.wp-plan-feature svg {
    color: var(--wp-color-brand-primary);
    flex: none;
    height: 18px;
    margin-top: 1px;
    stroke-width: 2.5;
    width: 18px;
}

.wp-plan-featured .wp-plan-feature {
    color: var(--wp-color-text-on-brand);
}

/* The lime again: a white tick on this gradient is the same white as the words beside it
   and stops reading as a mark at all. */
.wp-plan-featured .wp-plan-feature svg {
    color: var(--wp-plan-accent);
}

/* `margin-top: auto` rather than a spacer: the action sits at the foot of the card whether
   the list above it is three bullets or five, which is what keeps the two cards' buttons on
   one line. */
.wp-plan-action {
    margin-top: auto;
    width: 100%;
}

.wp-plan-fine-print {
    color: var(--wp-color-text-on-brand);
    font: var(--wp-font-style-caption);
    margin: var(--wp-space-sm) 0 0;
    opacity: 0.75;
    text-align: center;
}

/* ======================================================================= compare ==
 * The plan comparison: every feature either plan has, grouped, with a mark per plan.
 *
 * A real <table> and not a pair of lists, because the question it answers is "which column
 * has a mark in it", which is a two-dimensional question. The marks are never omitted: an
 * empty cell reads as "we forgot" rather than as "no", and a screen reader announces
 * nothing at all for it, so a row with one tick in it tells a non-sighted reader half of
 * what it tells everybody else.
 *
 * The head is deliberately NOT sticky. It was, and at the app shell's bar height it came to
 * rest on top of the first rows rather than above them.
 */
/* A title and a subtitle, where this was a section caption. `.wp-list-title` is the small
   uppercase header a grouped list wears, which is right for "Account" above four rows and wrong
   for the one heading on this page that introduces a block of its own. */
.wp-compare-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-2);
    letter-spacing: -0.01em;
    margin: 0;
}

/* Aligned with the card below it rather than inset the way `.wp-list-header` is: a title this
   size reads as the heading of the block, and the block's edge is the card's. */
.wp-compare-text {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-body);
    margin: var(--wp-space-xs) 0 var(--wp-space-lg);
}

/* The two mark columns are as narrow as a glyph plus its padding, so the feature and its
   description get every pixel that is left. `.wp-col-check`'s own 20% is a share of a table
   whose other column is one word; here it is two lines of prose and a percentage gives the
   ticks a third of a phone. */
.wp-compare .wp-col-check {
    width: 64px;
}

@media (width >= 680px) {
    .wp-compare .wp-col-check {
        width: 110px;
    }
}

/* The Plus column, washed so the eye can follow it down thirteen rows without counting
   across. On the head and the group rows too, or the band breaks wherever a heading is. */
.wp-compare-plus {
    background: color-mix(in srgb, var(--wp-color-brand-primary) 6%, transparent);
}

.wp-compare thead th.wp-compare-plus {
    color: var(--wp-color-brand-primary);
}

/* The section headings inside the table. A <th scope="colgroup"> and not a styled <td>: it is a
   heading for the rows under it and announcing it as one is the whole point.
   The markup pairs this with `.wp-table-aside`, which is the library's own word for a row that
   is not a data row: it keeps the separator and takes none of the row affordances. Without it
   the heading lit up on hover like the features under it, promising a press that does nothing. */
.wp-compare-group th {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption-2);
    letter-spacing: 0.06em;
    padding-top: var(--wp-space-lg);
    text-transform: uppercase;
}

/* Two lines per feature: what it is, and one line saying what that means. The description
   is what makes the table readable by somebody who does not already know the product, which
   is exactly who is on this page. */
.wp-compare-name {
    color: var(--wp-color-text-primary);
    display: block;
    font: var(--wp-font-style-subheadline);
    font-weight: 600;
}

.wp-compare-note {
    color: var(--wp-color-text-secondary);
    display: block;
    font: var(--wp-font-style-caption);
    font-weight: 400;
    margin-top: 2px;
}

/* The mark itself. The label beside it is for a screen reader only: on screen the column
   says which plan this is and the glyph says whether it is in, and spelling "Included" out
   thirteen times would be more words than the table holds. */
.wp-compare-mark {
    display: inline-flex;
}

.wp-compare-mark svg {
    color: var(--wp-color-text-secondary);
    height: 18px;
    stroke-width: 2.5;
    width: 18px;
}

.wp-compare-plus .wp-compare-mark svg {
    color: var(--wp-color-brand-primary);
}

/* A dash and not a cross: "not in this plan" is an absence, and a cross reads as an error.
   Quieter than either tick, because the row's answer is the mark that is there. */
.wp-compare-mark-absent svg {
    color: var(--wp-color-control-track);
}

.wp-compare-plus .wp-compare-mark-absent svg {
    color: var(--wp-color-control-track);
}

.wp-compare-mark-label {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

/* ========================================================================= sheet ==
 * `.presentationDetents([.medium])`: a panel that rises from the bottom edge with the page
 * dimmed behind it, holding one question and its answer.
 *
 * A <dialog>, because the platform already does the parts that are easy to get wrong and
 * tedious to get right: the backdrop, the focus trap, Escape to dismiss, `inert` on
 * everything behind it, and returning focus to whatever opened it. A div with a z-index
 * does none of that.
 *
 * `margin: auto auto 0` is what puts it at the bottom: a modal dialog is centred in its
 * containing block by `margin: auto`, so overriding only the bottom edge drops it. On a
 * wide screen it stops at the content column's width rather than running the full span,
 * because a form field 1400px wide is not a form field.
 */
.wp-sheet {
    /* The elevated grouped surface, not the plain one. In the dark appearance `surface.grouped`
       is pure black and so is the page under the backdrop, which dims black to black: the sheet
       had no edge and no body, only its corners. `surface.grouped-elevated` is what iOS puts
       behind a presented sheet for exactly this reason. In the light appearance the two are the
       same colour and nothing changes. */
    background: var(--wp-color-surface-grouped-elevated);
    border: 0;
    border-radius: var(--wp-radius-card) var(--wp-radius-card) 0 0;
    color: var(--wp-color-text-primary);
    margin: auto auto 0;

    /* The browser's own dialog rule caps a dialog at `calc(100% - 6px - 2em)`, which left the
       sheet short of the bottom of the screen with the backdrop showing under it. */
    max-height: 100dvh;
    max-width: var(--wp-width-content);
    padding: 0;
    width: 100%;
}

/* The panel takes the focus itself when it opens, so that nothing inside it is singled out -- see
   the script. That makes it a focus anchor rather than a control, and an anchor must not advertise
   the focus it is holding: a sheet opened by a page that came back with errors was drawing a ring
   around the whole panel. The controls inside it keep their own rings, which is where a ring
   means something. */
.wp-sheet:focus {
    outline: none;
}

/* Pulling a panel down across its own text should not select the text. Outside the motion query
   below, because this is not about motion: a reader who has turned animation off still drags. */
.wp-sheet[data-wp-sheet-dragging] {
    user-select: none;
}

/* Everything about an open sheet goes here and not on `.wp-sheet`, `display` above all: the
   browser hides a closed dialog with `dialog:not([open]) { display: none }`, and an author
   `display` of any kind beats a UA rule whatever its specificity. A `display: flex` up there
   put all fourteen closed sheets on the page, stacked, in the open. */
.wp-sheet[open] {
    /* A column, so the body takes the slack and scrolls while the header stays at the top. */
    display: flex;
    flex-direction: column;

    /* Nine tenths of the screen. The strip left showing is what the rounded corners need to be
       corners rather than two notches cut out of the top edge, and it is what iOS leaves showing
       of the page behind a `.large` detent -- enough of it to read as a sheet over the page you
       came from rather than as a new screen. A proportion and not `100dvh` minus a gap, so the
       strip stays in proportion on a tall phone instead of shrinking to a hairline. */
    height: 90dvh;
}

/* The cards step up with the sheet they are on, or in the dark appearance they would be the
   same #1C1C1E as the sheet and the grouping would vanish. Written here rather than as a
   variant class on the card, because what elevates is the sheet: anything inside one is on an
   elevated surface whether or not whoever wrote it knew. */
/* A sheet sized to its content rather than the screen: iOS gives a picker its own detent
   rather than the full height, and a wheel with a whole empty screen under it reads as a page
   that failed to load. */
.wp-sheet-auto[open] {
    /* `top: auto` is what makes `height: auto` mean the content: a dialog is positioned against
       all four edges, and a box with both a top and a bottom stretches between them whatever its
       height says. Pinned to the bottom edge instead, it is as tall as what is in it. */
    bottom: 0;
    height: auto;

    /* The same ceiling a full-height sheet sits at, so a picker that happens to outgrow the screen
       stops where every other sheet stops rather than at a different line. */
    max-height: 90dvh;
    top: auto;
}

/* Everything in a sheet steps up with it, or in the dark appearance it is the same #1C1C1E as the
   sheet behind it and the grouping vanishes -- a list of rows with no card under them, floating on
   the panel. `surface.grouped-raised` and the sheet's own `surface.grouped-elevated` are different
   colours on a light appearance and the same one on a dark appearance, so this is invisible until
   it is not.
   The two that carry fields say so through `--wp-field-surface`, because the controls inside them
   read it too; a card and a list only have to paint themselves. */
.wp-sheet .wp-calendar,
.wp-sheet .wp-field-list {
    --wp-field-surface: var(--wp-color-surface-grouped-raised-elevated);
}

.wp-sheet .wp-card,
.wp-sheet .wp-list {
    background: var(--wp-color-surface-grouped-raised-elevated);
}

/* The page behind must not scroll while the sheet is over it, or dismissing it leaves you
   somewhere you did not navigate to. A modal dialog makes the page inert to clicks but not
   to a wheel or a trackpad. CSS rather than script, so it cannot be left switched on. */
:root:has(.wp-sheet[open]) {
    overflow: hidden;
}

/* The app wraps a sheet's contents in a <form> and the gallery does not. Either way whatever
   sits between the dialog and its three parts has to pass the height through, or the footer
   floats up under the body instead of resting on the bottom edge. */
/* The heading a sheet carries when it is a decision rather than a form: a glyph, what the
   decision is about, and what saying yes does. `EventEnrollmentView`'s first section, which is a
   centred VStack with no row background behind it. */
.wp-sheet-hero {
    align-items: center;
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-sm);

    /* One inset on all four edges, which is what the section this is a port of has: it is a row in
       a grouped list with its background cleared, so iOS gives it the list's own symmetric insets
       rather than anything of its own. It was a step deeper at the foot, which read as the heading
       being pushed away from the list under it rather than sitting above it. The gap to the next
       section is that section's own `margin-top` and is not this element's to set. */
    padding: var(--wp-space-lg);
    text-align: center;
}

.wp-sheet-hero-icon {
    color: var(--wp-color-brand-primary);
    display: inline-flex;
}

.wp-sheet-hero-icon svg {
    height: 44px;
    width: 44px;
}

.wp-sheet-hero-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-2);
    margin: 0;
}

.wp-sheet-hero-description {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-body);
    margin: 0;
}

/* `safeAreaInset(edge: .bottom)`: the sentence and the button it is about travel together at the
   foot of the sheet rather than scrolling away from each other. */
.wp-sheet-footer {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-md);
    padding: var(--wp-space-xl) var(--wp-space-lg) var(--wp-space-lg);
}

.wp-sheet-consent {
    align-items: center;
    color: var(--wp-color-text-secondary);
    display: flex;
    flex-direction: column;
    font: var(--wp-font-style-caption);
    gap: var(--wp-space-xs);
    margin: 0;
    text-align: center;
}

.wp-sheet-consent-icon {
    color: var(--wp-color-brand-primary);
    display: inline-flex;
}

.wp-sheet-consent-icon svg {
    height: 20px;
    width: 20px;
}

/* The rules are the one thing on this line worth aiming at, so they are the brand colour and the
   sentence around them is not. */
.wp-sheet-consent a {
    color: var(--wp-color-brand-primary);
    font-weight: 600;
    text-decoration: none;
}

/* A list row whose value is a control rather than text. The control fills the row and takes the
   row's padding onto itself, so that the padding is part of what you can click: a button inset
   inside a padded row leaves a dead margin all the way round it that looks like the row and does
   nothing.
   Which control that is belongs to whoever is using the row -- the library styles the row and
   not the consumer's own class -- so the consumer restates this padding on it, from these same
   two tokens. */
.wp-list-row-picker {
    padding: 0;
}

.wp-sheet > form {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-height: 0;
}

.wp-sheet::backdrop {
    background-color: rgb(0 0 0 / 0%);
}

.wp-sheet[open]::backdrop {
    background-color: rgb(0 0 0 / 40%);
}

/* A transition in both directions, not a keyframe on open.
   This was an `animation` on `[open]`, which is the cheaper way to say "it slides up when it
   appears" and cost the exit: a dialog stops being rendered the moment it closes, so there was
   nothing to animate out. That was a fair trade when the only ways out were Escape, the backdrop
   and a Cancel button - none of which anyone watches. It stopped being fair when the sheet became
   draggable: a panel the reader is pulling down has to carry on down when released and come back
   up when not, and a jump cut at the end of a gesture reads as a bug.
   Both directions needs the four moving parts the old comment named: `@starting-style` for a
   before-change style to animate from, and `allow-discrete` on `display` and `overlay` so the
   dialog stays rendered and in the top layer while it leaves.
   `0.32, 0.72, 0, 1` is the curve a UIKit sheet travels on - most of the distance early, settling
   without a bounce. */
@media (prefers-reduced-motion: no-preference) {
    .wp-sheet {
        opacity: 0;
        transition: display 0.32s allow-discrete,
        opacity 0.32s cubic-bezier(0.32, 0.72, 0, 1),
        overlay 0.32s allow-discrete,
        translate 0.32s cubic-bezier(0.32, 0.72, 0, 1);
        translate: 0 100%;
    }

    .wp-sheet[open] {
        opacity: 1;
        translate: 0 0;
    }

    @starting-style {
        .wp-sheet[open] {
            opacity: 0;
            translate: 0 100%;
        }
    }

    /* The drag writes `translate` inline on every pointer move, and a transition would make the
       panel chase the finger a third of a second behind it. The attribute is set for the length of
       the gesture and removed on release, which is when the curve above takes over - either back up
       to nothing or on down and out. */
    .wp-sheet[data-wp-sheet-dragging] {
        transition: none;
    }

    .wp-sheet::backdrop {
        transition: background-color 0.32s cubic-bezier(0.32, 0.72, 0, 1),
        display 0.32s allow-discrete,
        overlay 0.32s allow-discrete;
    }

    @starting-style {
        .wp-sheet[open]::backdrop {
            background-color: rgb(0 0 0 / 0%);
        }
    }
}

/* A nav bar, because that is what the screen behind this has: `ProfileEditAttributeView` is a
   pushed screen with an inline title and its confirming action in the toolbar, not a panel with
   a button at the foot of it.
   Three tracks and not flex, for the same reason `.wp-navbar` uses three: the title stays
   optically centred whether or not both sides carry something.
   No grabber either. One belongs on a sheet you can drag between detents, and this covers the
   screen; the leading button dismisses it, along with Escape and the backdrop. */
/* Three blocks: the dismissing button, the title, and an optional confirming one. The two side
   tracks are a matching `1fr` each, which is what centres the title IN THE SHEET rather than in
   whatever is left over -- a header with a button on one side only, which the match-up sheets
   have, put the title off to one side when the tracks were `auto 1fr auto` and the middle one
   merely centred its own text.
   The title's track is `minmax(0, auto)`: it asks for its content and may be given less. That is
   what keeps the old fix working -- with the title sized to its content and nothing else, a long
   one squeezed the buttons until "Cancel\00B7lar" wrapped onto two lines across it. A title that
   does not fit is still the one that gives, now by ellipsis rather than by shoving.
   Each block is pinned to its own column rather than placed in order, so a header with no
   dismissing button still puts its title in the middle instead of sliding everything left. */
.wp-sheet-header {
    align-items: center;
    display: grid;
    gap: var(--wp-space-md);
    grid-template-columns: 1fr minmax(0, auto) 1fr;
    /* A step more below than beside, so the two actions clear the first card rather than sitting on
       it. They used to be bare words and could afford to sit close; a bezelled capsule has an edge
       of its own and reads as crowded against one. */
    padding: calc(var(--wp-space-md) + env(safe-area-inset-top)) var(--wp-space-md) var(--wp-space-lg);

    /* The grab handle, in effect: a touch starting on the header is the sheet's to interpret, and
       without this the browser claims the gesture for a scroll before the drag has moved a pixel.
       Only here, because the body below genuinely does scroll and must keep the gesture it needs. */
    touch-action: none;
}

.wp-sheet-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    grid-column: 2;
    margin: 0;

    /* A grid item floors at its own min-content unless told otherwise, and a floor is the one
       thing an ellipsis cannot survive. */
    min-width: 0;
    overflow: hidden;
    text-align: center;
    text-overflow: ellipsis;

    /* Stated, not inherited. It is a heading, and a host that styles its headings uppercase --
       this library's own gallery does -- would otherwise shout a nav title that iOS sets in
       sentence case. */
    text-transform: none;
    white-space: nowrap;
}

/* `.cancellationAction` and `.confirmationAction` at the two ends of the bar, the confirming one
   in semibold.
   A small bezelled capsule rather than bare tinted text: a hairline rim and a neutral wash, with
   the label in `text.primary` -- which is white on a dark appearance and near-black on a light
   one, so the pair reads the same way round in both without either being stated twice. The brand
   colour is what a link in a row uses; a toolbar action is a control and looks like one.
   Not `.wp-glass`, which is the material for a bar floating over content that scrolls under it.
   Inside a sheet there is nothing behind these to blur, and a `backdrop-filter` over an opaque
   panel costs a compositing layer to achieve nothing. */
.wp-sheet-cancel,
.wp-sheet-submit {
    background: var(--wp-color-surface-chip);
    border: 1px solid var(--wp-color-border-subtle);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-primary);
    cursor: pointer;
    font: var(--wp-font-style-body);
    padding: var(--wp-space-xs) var(--wp-space-md);
    white-space: nowrap;
}

.wp-sheet-cancel {
    grid-column: 1;
    justify-self: start;
    text-align: left;
}

.wp-sheet-submit {
    font-weight: 600;
    grid-column: 3;
    justify-self: end;
    text-align: right;
}

/* The Form section's footer: what this attribute is for and who sees it. Long on purpose --
   it is the app's own copy -- so it reads as a paragraph under the card rather than a hint. */
.wp-sheet-note {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    margin: var(--wp-space-sm) 0 0;
    padding: 0 var(--wp-space-lg);
}

.wp-sheet-body {
    flex: 1;

    /* A flex item will not shrink below its content without this, and the body is the part
       that is supposed to scroll. */
    min-height: 0;
    overflow-x: hidden;
    overflow-y: auto;

    /* A body scrolled to either end must not hand the rest of the gesture to the page behind the
       sheet, which would scroll the thing the sheet is covering and, at the top of the window,
       offer to reload it. */
    overscroll-behavior: contain;
    padding: 0 var(--wp-space-md) var(--wp-space-md);
}

/* ========================================================================= prose ==
 * A document: the terms, the privacy policy, the cookie policy, the FAQ.
 *
 * These are the only screens in the product that are running text rather than rows of
 * data, and iOS has no counterpart for them -- it links out to these very pages. So this
 * is the one component with no SwiftUI view behind it, and the one that styles plain tags
 * rather than classes: a legal document is headings and paragraphs, and asking whoever
 * next edits the terms to remember a class per element is asking for the day they forget.
 *
 * A document that marks its own sections gets a card each. This used to be naked on the page's
 * background, on the reasoning that a wall of text in a rounded box reads as a thing to skim
 * past - true of one box round a whole document, and the opposite of true for one per section: a
 * reader of a legal page is looking for a particular clause, and a card is where the eye stops.
 * A run of prose with no sections in it is left alone, which is what the bare paragraph on a
 * player's profile is.
 */
.wp-prose {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-body);
}

/* A document's own title, one step above the headings inside it. There was no rule for this
   until `reset.css` arrived: the browser's default was covering for it, which is exactly the kind
   of borrowed styling a reset exists to stop a stylesheet from relying on without saying so. */
.wp-prose h1 {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-2);
    margin: 0 0 var(--wp-space-md);
}

/* Space above, not below: a heading belongs to what follows it, and the gap that
   separates one section from the last is the heading's own. */
.wp-prose h2 {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title-3);
    margin: var(--wp-space-xl) 0 var(--wp-space-sm);
}

.wp-prose h3 {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    margin: var(--wp-space-lg) 0 var(--wp-space-xs);
}

.wp-prose > :first-child {
    margin-top: 0;
}

/* One card per section, on the grouped background the app shell already paints - the same pairing
   the profile's own sections use. Painting only: the gap between sections and the first child's
   dropped top margin are already stated below, where sections were styled before they were cards. */
.wp-prose > section {
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    padding: var(--wp-space-lg);
}

.wp-prose li,
.wp-prose p {
    margin: 0 0 var(--wp-space-md);
}

.wp-prose li:last-child,
.wp-prose p:last-child {
    margin-bottom: 0;
}

/* The markers back, for the one place that wants them. `reset.css` takes `list-style` off every
   list, which is right for a component - a list of rows is not a bulleted list - and wrong for
   prose, where this rule's own indent exists to make room for exactly the marker the reset
   removed. Set here and not there because the reset may name no component, and because which
   lists are prose is this stylesheet's question rather than the reset's. */
.wp-prose ol,
.wp-prose ul {
    margin: 0 0 var(--wp-space-md);
    padding-left: var(--wp-space-xl);
}

.wp-prose ol {
    list-style: decimal;
}

.wp-prose ul {
    list-style: disc;
}

/* Running text's own links, which is why this is secondary rather than the brand colour: a
   legal page is mostly prose and a page of blue would read as a page of links.
   `:not(.wp-button)` because a button is not one of them. This rule is a type-and-class
   selector and `.wp-button-prominent` is a single class, so without the exclusion the prose
   wins on specificity and paints a filled button's label grey on its own brand fill -- which
   is what the Swing Vision call to action was doing. */
.wp-prose a:not(.wp-button) {
    color: var(--wp-color-text-secondary);
}

/* A document long enough to need a table of contents wraps each part in a <section> with
   an id. The spacing has to come out the same either way, so a section's first child drops
   its top margin and the section itself carries the gap instead.
   `scroll-margin-top` is the point of the wrapper being styled at all: without it, jumping
   to a section from the contents puts its heading underneath the fixed nav bar. */
.wp-prose section {
    scroll-margin-top: calc(var(--wp-navbar-height) + env(safe-area-inset-top) + var(--wp-space-md));
}

.wp-prose section + section {
    margin-top: var(--wp-space-xl);
}

.wp-prose section > :first-child {
    margin-top: 0;
}

/* The definitions table: two columns of running text rather than figures, so a rule under
   each row and no card around the lot. A card would make it a thing to skim past. */
.wp-prose table {
    border-collapse: collapse;

    /* `block` so the element can scroll: a table box cannot. A five-column table does not fit a
       phone, and on the page's own background it used to overflow a column with no visible edge -
       inside a card it breaks the card, and the app shell clips rather than scrolls, so whatever
       sat past the edge was unreachable. Scrolling it keeps the columns and the card.
       `contain`, so flicking off the end of the table does not take the page with it. */
    display: block;
    margin: 0 0 var(--wp-space-md);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    width: 100%;
}

.wp-prose td,
.wp-prose th {
    border-bottom: 1px solid var(--wp-separator);
    padding: var(--wp-space-sm) var(--wp-space-md) var(--wp-space-sm) 0;
    text-align: left;
    vertical-align: top;
}

/* `:nth-last-child(2)` is how a two-column row is told from any other: a first cell that is also
   the second from last. The 30% is written for the definitions table and squeezes every column of
   a wider one into a ribbon, which is what the cookie table was doing. */
.wp-prose tr > td:first-child:nth-last-child(2) {
    font: var(--wp-font-style-headline);
    width: 30%;
}

/* The floor goes on the cells, not on the table: the table is the scroll container here, and a
   min-width on a scroller just makes the scroller wider - it grew past the card it sits in instead
   of scrolling inside it. Widening the cells makes the content outgrow the container, which is the
   thing that scrolls.
   Only for a table with a third column - a data table rather than a definitions list, where five
   columns of running text compress to one word per line on a phone. */
.wp-prose table:has(td:nth-child(3)) td,
.wp-prose table:has(td:nth-child(3)) th {
    min-width: 7rem;
}

/* The standfirst under the title, and the "last updated" line under that. Both are the
   page talking about itself rather than saying anything, so both step back. */
.wp-prose-lead {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-callout);
}

.wp-prose-meta {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
}

/* ========================================================================== form ==
 * A Form's Section: its fields in one inset-grouped card, with a footer beneath.
 *
 * A text field carries a real, visible <label> that floats: centred in the row while the
 * field is empty, risen and scaled down once it has focus or a value, with what was typed
 * underneath. That is the field Apple draws on its own sign-in pages, and it is here for more
 * than looks. The alternative was the placeholder standing in as the label with the real one
 * hidden off-screen, which spends a 1px element per field saying the same thing twice and
 * loses the name of the field the moment the reader starts typing into it.
 */
/* The stack of Sections a Form is. Spacing only: everything visible belongs to the
   sections themselves. */
.wp-form {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-lg);
}

.wp-field-list {
    --wp-field-surface: var(--wp-color-surface-grouped-raised);
    background: var(--wp-field-surface);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* A select's popup and arrow and a checkbox's box are drawn by the browser against the
   page's `color-scheme`, not against any token, so they are the one part of a form the
   stylesheet cannot repaint. Binding the scheme to the theme the same way tokens.css binds
   the colours keeps them from staying light inside a dark card.
   `.wp-flyout-group` is the other card that can hold one: the menu's language picker is a
   native select, and its popup was arriving light against the panel behind it. */
[data-theme="dark"] .wp-field-list,
[data-theme="dark"] .wp-flyout-group {
    color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .wp-field-list,
    :root:not([data-theme="light"]) .wp-flyout-group {
        color-scheme: dark;
    }
}

/* Whatever the browser would bring of its own to a text control, taken off in one place.
 *
 * `appearance: none` is the one that matters on a phone: mobile Safari gives a text input an
 * inset shadow and a rounding of its own, which inside one of these rows draws a second field
 * inside the row. It also takes off Chrome's own control chrome, and `margin` is Safari giving
 * form controls a margin no other element gets.
 *
 * The exclusions are the controls this library draws itself or deliberately leaves native: a
 * slider is a `.wp-slider` with a painted track, a switch is a checkbox with a drawn knob, a
 * checkbox's tick, a radio and a select's arrow are conventions a reader already knows, and a
 * file input is a button. A blanket reset would quietly undo all of them.
 *
 * `select` is not here for the same reason: its arrow is the only thing saying it is a select.
 */
.wp-field input:not([type='range']):not([type='checkbox']):not([type='radio']):not([type='file']),
.wp-field textarea,
.wp-field-floating input:not([type='checkbox']):not([type='radio']):not([type='file']),
.wp-field-floating textarea,
.wp-search input {
    appearance: none;
    margin: 0;
}

/* The spinner on a number field is a pseudo-element in Chrome and Safari, which a reset on the
   control itself does not reach, and part of the control in Firefox, which takes `textfield`
   and not `none`. Two mechanisms, one spinner, and neither engine understands the other's. */
.wp-field input[type='number']::-webkit-inner-spin-button,
.wp-field input[type='number']::-webkit-outer-spin-button,
.wp-field-floating input[type='number']::-webkit-inner-spin-button,
.wp-field-floating input[type='number']::-webkit-outer-spin-button {
    appearance: none;
    margin: 0;
}

.wp-field input[type='number'],
.wp-field-floating input[type='number'] {
    -moz-appearance: textfield;
}

/* A date or time field is several little edit boxes to the browser, each with padding of its
   own, which puts its text off the line every other row in the card sits on. */
.wp-field input::-webkit-datetime-edit,
.wp-field-floating input::-webkit-datetime-edit {
    padding: 0;
}

/* Firefox puts a red glow on a control that fails validation, the moment it fails it. The form
   says what is wrong in its own words, in its own place, and says it once. */
.wp-field :-moz-ui-invalid,
.wp-field-floating :-moz-ui-invalid,
.wp-search :-moz-ui-invalid {
    box-shadow: none;
}

/* A text field: label over value, both inside the row.
 *
 * `:placeholder-shown` is what tells the label whether there is anything to get out of the way
 * of, and it only answers while the input has a placeholder at all, which is why every one of
 * these carries `placeholder=" "`. The placeholder itself is never seen: it is a single space,
 * and transparent besides, so a consumer that passes a real one out of habit does not end up
 * with two labels fighting over the same line.
 *
 * `transform` rather than `font-size` for the shrink, because a transform animates on the
 * compositor where a font size reflows the row on every frame. */
.wp-field-floating {
    display: block;
    min-height: 56px;
    padding: 24px var(--wp-space-lg) var(--wp-space-sm);
    position: relative;
}

.wp-field-floating input,
.wp-field-floating textarea {
    background: transparent;
    border: 0;
    color: var(--wp-color-text-primary);
    display: block;
    font: var(--wp-font-style-body);
    padding: 0;
    width: 100%;
}

.wp-field-floating textarea {
    min-height: 66px;
    resize: vertical;
}

.wp-field-floating input::placeholder,
.wp-field-floating textarea::placeholder {
    color: transparent;
}

/* 17px puts a 22px line on the row's centre; floated is 11px above that, which clears the
   value's own line without the two ever meeting. */
.wp-field-floating label {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-body);
    left: var(--wp-space-lg);
    pointer-events: none;
    position: absolute;
    top: 17px;
    transform-origin: left top;
    transition: transform 180ms ease;
}

.wp-field-floating input:focus + label,
.wp-field-floating input:not(:placeholder-shown) + label,
.wp-field-floating textarea:focus + label,
.wp-field-floating textarea:not(:placeholder-shown) + label {
    transform: translateY(-11px) scale(0.76);
}

/* An autofilled field counts as a field with something in it, which `:placeholder-shown` does
   not always know. A password the browser filled on load is withheld from the page until the
   reader does something -- the dots are on screen, but the value is empty as far as script and
   `:placeholder-shown` are concerned -- so the label stayed sitting across them. Chrome's
   fill-preview, before anything is committed at all, reads the same way.
   Two rules rather than one selector list, and neither joined to the list above: an engine that
   meets a pseudo-class it does not know drops the whole list it appears in, and no engine knows
   both of these. One list would have cost every browser the other three selectors. */
.wp-field-floating input:-webkit-autofill + label {
    transform: translateY(-11px) scale(0.76);
}

.wp-field-floating input:autofill + label {
    transform: translateY(-11px) scale(0.76);
}

@media (prefers-reduced-motion: reduce) {
    .wp-field-floating label {
        transition: none;
    }
}

/* `-webkit-text-fill-color` as well as `color`: WebKit greys a disabled control's text through
   the fill colour and ignores `color` entirely. */
.wp-field-floating input:disabled {
    -webkit-text-fill-color: var(--wp-color-text-secondary);
    color: var(--wp-color-text-secondary);
}

/* A field in focus looks no different from a field at rest, which is what UIKit does: tapping
   into one of these on an iPhone moves the caret and nothing else. The browser's own ring is
   the only thing that would say otherwise, so it goes.
   This does leave somebody navigating by keyboard with no indication of where they are, which
   on the web is a real loss rather than a stylistic one. The caret is the only remaining
   answer. If that proves too little, `:focus-visible` is the way back: it paints on a Tab and
   stays away on a tap, so the field would still look like the iOS one to everybody using a
   pointer. */
.wp-field-floating input:focus,
.wp-field-floating textarea:focus {
    outline: none;
}

/* Chrome paints its own background on an autofilled control and ignores `background-color`
   while it does; an inset shadow is the one thing that covers it and `-webkit-text-fill-color`
   the one thing that overrides the text colour it picks. Two rules rather than one selector
   list, because an engine that does not know one of these pseudo-classes drops the whole list
   and no engine knows both. */
.wp-field input:-webkit-autofill,
.wp-field-floating input:-webkit-autofill {
    -webkit-text-fill-color: var(--wp-color-text-primary);
    box-shadow: 0 0 0 100px var(--wp-field-surface) inset;
}

.wp-field input:autofill,
.wp-field-floating input:autofill {
    -webkit-text-fill-color: var(--wp-color-text-primary);
    box-shadow: 0 0 0 100px var(--wp-field-surface) inset;
}

/* The padding is the row's, not the control's, so a row holding a label and a control
   lines up with a row holding only a control. */
.wp-field {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
    min-height: var(--wp-size-touch-target);
    padding: var(--wp-space-sm) var(--wp-space-lg);
}

/* Every ordered pair of row kinds, written out rather than `* + *`, so that what counts as a
   row is stated here and a card can hold something that is not one without acquiring a line
   above it.
   .wp-select is one of those kinds. It was missing, which is why the country picker and the
   phone number under it on the registration flow's contact step ran together as one tall row
   while the two name fields a step earlier were properly divided. */
.wp-field + .wp-field,
.wp-field + .wp-field-floating,
.wp-field + .wp-check,
.wp-field + .wp-select,
.wp-field-floating + .wp-field,
.wp-field-floating + .wp-field-floating,
.wp-field-floating + .wp-check,
.wp-field-floating + .wp-select,
.wp-check + .wp-field,
.wp-check + .wp-field-floating,
.wp-check + .wp-select,
.wp-select + .wp-field,
.wp-select + .wp-field-floating,
.wp-select + .wp-check,
.wp-select + .wp-select {
    box-shadow: inset 0 1px 0 var(--wp-separator);
}

.wp-field-label {
    color: var(--wp-color-text-primary);
    flex: none;
    font: var(--wp-font-style-body);
}

/* The same exclusions as the reset above, and for the same reason: a slider is a .wp-slider
   with a painted track, a switch is a checkbox with a drawn knob, and a rule that strips the
   background and border off every input in a row quietly undoes both. It did: a .wp-switch in
   one of these rows had its track taken away by the `background: transparent` here and was
   stretched the width of the row by the `flex: 1`, leaving the knob alone on the line. This
   rule is specific enough to beat `.wp-switch`'s own, so the exclusion is the only way out.
   A select is not here either: it is a .wp-select, below, which is a control of its own under
   its label rather than a value sharing the row with one. */
.wp-field input:not([type='range']):not([type='checkbox']):not([type='radio']):not([type='file']),
.wp-field textarea {
    background: transparent;
    border: 0;
    color: var(--wp-color-text-primary);
    flex: 1;
    font: var(--wp-font-style-body);
    min-width: 0;
    padding: var(--wp-space-xs) 0;
}

/* A labelled row reads label-left, value-right, the way LabeledContent does. Left alone,
   a right-hand field's text starts in the middle of the row and the column of values
   never lines up. */
.wp-field:not(.wp-field-stacked) .wp-field-label ~ input {
    text-align: right;
}

/* A control that wants the full width under its label rather than the space beside it:
   a slider, a textarea of any length. */
.wp-field-stacked {
    align-items: stretch;
    flex-direction: column;
    gap: var(--wp-space-xs);
}

.wp-field textarea {
    min-height: 88px;
    resize: vertical;
}

.wp-field input:not([type='range'])::placeholder,
.wp-field textarea::placeholder {
    color: var(--wp-color-text-secondary);
}

/* `-webkit-text-fill-color` as well as `color`: WebKit greys a disabled control's text
   through the fill colour and ignores `color` entirely, so without it the one field the
   user cannot edit is the one that looks most editable. */
.wp-field input:not([type='range']):disabled {
    -webkit-text-fill-color: var(--wp-color-text-secondary);
    color: var(--wp-color-text-secondary);
}

/* A select carries no ring, in any state. What it opens is a list the browser draws and places
   itself -- a popup on a desktop, a wheel or a sheet on a phone -- and a ring drawn round the
   closed control is a mark left somewhere the reader is no longer looking.
   `:focus` as well as `:focus-visible`, which is how gasclic's `select` writes it too: the ring
   on a select is the engine's own, and engines disagree about which of the two states they hang
   it off, so taking one off can leave the other drawing. */
.wp-score-cell select:focus,
.wp-score-cell select:focus-visible,
.wp-select select:focus,
.wp-select select:focus-visible {
    outline: none;
}

/* ====================================================================== select ==
 * A select, laid out the way gasclic's settings tray lays one out: the name of the thing is a
 * section title above the card rather than a label beside the control, and the control itself
 * is a row of the card, on the card's own background. gasclic's `select` takes
 * `--section-background`, which is the colour of the cells around it, so nothing about it
 * reads as a second surface floating inside the first. The same here: the row is the card.
 *
 * A .wp-list-header with a `<label>` in it is what puts the title above, which keeps the name
 * attached to the control it names -- a heading would look the same and say nothing to a
 * screen reader.
 *
 * The caret is a sibling element rather than a masked image on a `::after`, which is how
 * gasclic draws its own: the icons here are generated from icons/icons.json into
 * component/icon.html, and a path copied into this stylesheet by hand is a second copy that
 * the generator does not know about and cannot keep in step. It is the same caret the list
 * rows use, at the same 14px, and `pointer-events: none` so the click goes through it to the
 * control underneath.
 */
.wp-select {
    display: block;
    position: relative;
    width: 100%;
}

.wp-select select {
    appearance: none;
    background: transparent;
    border: 0;
    color: var(--wp-color-text-primary);
    cursor: pointer;
    font: var(--wp-font-style-body);
    min-height: var(--wp-size-touch-target);

    /* The row's own padding, because here the control IS the row. The trailing side is the
       caret's inset plus the caret plus a gap, so a long value runs out of room before it runs
       underneath it. */
    padding: var(--wp-space-sm) var(--wp-space-lg);
    padding-inline-end: calc(var(--wp-space-lg) + 14px + var(--wp-space-sm));
    text-align: start;
    width: 100%;
}

.wp-select svg {
    color: var(--wp-color-text-secondary);
    height: 14px;
    pointer-events: none;
    position: absolute;
    right: var(--wp-space-lg);
    stroke-width: 2;
    top: 50%;
    translate: 0 -50%;
    width: 14px;
}

/* A consent or preference row. Not a .wp-field: the control leads, the copy wraps beside
   it, and the two align to the top rather than the middle because the copy is usually
   several lines and a checkbox centred against a paragraph floats.
   Uppercase, which is the `.textCase(.uppercase)` SubmitResultView puts on this row's
   footnote: it is a declaration rather than a sentence, and it reads as one. */
.wp-check {
    align-items: flex-start;
    color: var(--wp-color-text-secondary);
    display: flex;
    font: var(--wp-font-style-footnote);
    gap: var(--wp-space-md);
    padding: var(--wp-space-md) var(--wp-space-lg);
    text-transform: uppercase;
}

/* A circle, not a box. iOS draws this one control with `circle` and `checkmark.circle.fill`
   rather than a tick box, and it is the only checkbox in the app that is a declaration rather
   than a setting -- the switch is the setting.
   `appearance: none` with the tick as the input's own `::before`, the way .wp-switch draws its
   knob: the checkbox keeps its role, its focus and its keyboard behaviour, and there is no
   second element to keep in sync with its state. */
.wp-check input {
    align-items: center;
    appearance: none;
    border: 1.5px solid var(--wp-color-text-secondary);
    border-radius: var(--wp-radius-full);
    cursor: pointer;
    display: flex;
    flex: none;
    height: 22px;
    justify-content: center;
    margin: 0;
    width: 22px;
}

.wp-check input:checked {
    background: var(--wp-color-brand-primary);
    border-color: var(--wp-color-brand-primary);
}

/* Two sides of a box, turned: the tick is drawn rather than taken from the icon set, which
   would mean an element beside the control holding half of its state. */
.wp-check input:checked::before {
    border-bottom: 2px solid var(--wp-color-text-on-brand);
    border-right: 2px solid var(--wp-color-text-on-brand);
    content: '';
    height: 9px;
    margin-bottom: 3px;
    rotate: 45deg;
    width: 5px;
}

/* The Section footer: inset to the card's text rather than its edge, quiet, and the one
   place a form explains why it is asking for what it is asking for. */
.wp-field-note {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    margin: var(--wp-space-sm) 0 0;
    padding: 0 var(--wp-space-lg);
}

.wp-field-note a {
    color: var(--wp-color-brand-primary);
}

/* Under the card, on the page, never a child of the list: inside one it was a row of the card
   in everything but name -- red text on the raised surface, stretching the group to hold it,
   and standing between two fields where the separator between them should have been.
   Inset to the same edge as the header above the card and the note below it: the three are the
   only text in a group that is not a row, and they line up.
   The gap is on top rather than underneath, which is the header's own spacing turned over. A
   header is followed by what it introduces; this follows what it is about. */
.wp-field-error {
    color: var(--wp-color-feedback-danger);
    font: var(--wp-font-style-footnote);
    padding: var(--wp-space-sm) var(--wp-space-lg) 0;
}

/* ======================================================================== versus ==
 * Two players side by side: the record between them, then a row per fact with the label down
 * the middle and each player's value facing it.
 *
 * There is no counterpart in the app -- comparing two players is a thing a wide screen is for --
 * so this follows the library's own idiom rather than porting a view. The row is LabeledContent
 * with two values instead of one, which is why it reads like the rest of the app.
 *
 * The two sides take the match colours, so a side keeps the same colour here as it does on a
 * fixture; the leader is the one in brand purple.
 */
.wp-versus {
    align-items: center;
    display: flex;
    gap: var(--wp-space-lg);
    justify-content: center;
    margin-bottom: var(--wp-space-lg);
}

.wp-versus-player {
    align-items: center;
    color: var(--wp-color-text-primary);
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    gap: var(--wp-space-xs);
    min-width: 0;
    text-decoration: none;
}

.wp-versus-wins {
    font: var(--wp-font-style-title-2);
    margin: 0;
}

.wp-versus-percentage {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    margin: 0;
    text-transform: uppercase;
}

/* The donut and the total it wraps. The chart itself is drawn by the host, which owns the data;
   this only places it and centres the count over it. */
.wp-versus-chart {
    display: grid;
    flex: none;
    place-items: center;
    position: relative;
}

.wp-versus-chart > svg {
    grid-area: 1 / 1;
}

.wp-versus-total {
    align-items: center;
    display: flex;
    flex-direction: column;
    grid-area: 1 / 1;
}

.wp-versus-total-count {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    margin: 0;
}

.wp-versus-total-label {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption-2);
    margin: 0;
    text-transform: uppercase;
}

/* A fact about both of them. The label is the middle column rather than the leading one, which
   is the whole reason this is not a .wp-list-row: each value belongs to the player it is under. */
.wp-versus-row {
    align-items: center;
    color: var(--wp-color-text-primary);
    display: grid;
    font: var(--wp-font-style-body);
    gap: var(--wp-space-md);
    grid-template-columns: 1fr auto 1fr;
    min-height: var(--wp-size-touch-target);
    padding: var(--wp-space-sm) var(--wp-space-lg);
}

.wp-versus-row + .wp-versus-row {
    border-top: 1px solid var(--wp-separator);
}

.wp-versus-row > :first-child {
    text-align: right;
}

.wp-versus-row > :last-child {
    text-align: left;
}

.wp-versus-label {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    text-align: center;
    text-transform: uppercase;
}

/* ==================================================================== score entry ==
 * One set of a match being reported: each side's games, with the tie-break under them.
 *
 * A grid and not a row of fields, because the four numbers are a 2x2 and reading them as one
 * block is the whole point -- the set is "6-4, 7-6" and the form should look like that too.
 */
.wp-score-set {
    align-items: center;
    border: 0;
    display: grid;
    gap: var(--wp-space-sm) var(--wp-space-md);
    grid-template-columns: auto 1fr 1fr;
    margin: 0;
    min-width: 0;
    padding: var(--wp-space-sm) var(--wp-space-lg);
}

.wp-score-set + .wp-score-set {
    border-top: 1px solid var(--wp-separator);
}

.wp-score-set-label {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    padding: 0;
    white-space: nowrap;
}

/* Games over tie-break, stacked, so a column reads as one side's set. */
.wp-score-cell {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-xs);
    min-width: 0;
}

/* The label is for a screen reader: on screen the column position says which side this is, and
   a visible label per cell would be four words of chrome over four numbers. */
.wp-score-cell-label {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

.wp-score-set:disabled .wp-score-cell {
    opacity: 0.5;
}

/* ======================================================================== wheel ==
 * UIPickerView, as a scroller.
 *
 * The options are a list and the browser's own scroll snapping is what lands on one, so the
 * momentum, the rubber band at the ends and the keyboard's arrow keys are all the platform's
 * rather than a script's. The value is whichever option is under the band in the middle: the
 * list is padded by two rows at each end, so the first and the last can reach it.
 *
 * The band is on the frame and the scroller is above it, because a `::before` inside a scroller
 * scrolls away with the content. The fade at the top and bottom edges is a mask on the scroller
 * itself, which is the only part that moves.
 */
.wp-wheel {
    --wp-wheel-row: 34px;

    height: calc(var(--wp-wheel-row) * 5);
    position: relative;
}

.wp-wheel::after {
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-control);
    content: '';
    height: var(--wp-wheel-row);
    left: 0;
    pointer-events: none;
    position: absolute;
    right: 0;
    top: 50%;
    translate: 0 -50%;
}

.wp-wheel-list {
    /* Stated, not inherited. The two rows of padding that let the first and last options reach the
       band are inside this box; on `content-box` they are added to the height instead, which made
       the scrollport nine rows tall rather than five and left the last four options unreachable --
       the hour wheel could not be scrolled past 18, by dragging or any other means. */
    box-sizing: border-box;
    height: 100%;
    list-style: none;
    margin: 0;
    mask: linear-gradient(to bottom, transparent, #000 25%, #000 75%, transparent);
    overflow-y: scroll;
    padding: calc(var(--wp-wheel-row) * 2) 0;
    position: relative;
    scroll-snap-type: y mandatory;
    scrollbar-width: none;
    z-index: 1;
}

/* Chrome and Safari take the scrollbar off this way; `scrollbar-width` above is for the rest. */
.wp-wheel-list::-webkit-scrollbar {
    display: none;
}

.wp-wheel-option {
    align-items: center;
    color: var(--wp-color-text-primary);
    display: flex;
    font: var(--wp-font-style-title-2);
    font-variant-numeric: tabular-nums;
    height: var(--wp-wheel-row);
    justify-content: center;
    scroll-snap-align: center;
}

/* Two of them side by side, which is how a time is picked: hours in one, minutes in the other. */
.wp-wheel-group {
    display: flex;
    gap: var(--wp-space-md);
}

.wp-wheel-group .wp-wheel {
    flex: 1;
}

.wp-segmented + .wp-wheel {
    margin-top: var(--wp-space-lg);
}

/* The side a set-score row belongs to, as the same colour the match-up row's bar uses -- the
   colour classes are that component's, because the colour is the same fact about the same side.
   iOS draws it as a dot here and a bar there. */
.wp-score-dot {
    border-radius: var(--wp-radius-full);
    flex: none;
    height: 8px;
    width: 8px;
}

/* A row's label is a line of text, so a dot inside it has no box of its own to be 8px of. This
   is the one case where it is two things on a line, and the gap is the app's `HStack(spacing: 8)`. */
.wp-list-label:has(.wp-score-dot) {
    align-items: center;
    display: flex;
    gap: var(--wp-space-sm);
}

/* ======================================================================== switch ==
 * SwiftUI's `Toggle`, as the checkbox it already is.
 *
 * `appearance: none` on the control itself rather than a hidden input with a drawn sibling: the
 * checkbox keeps its own role, focus and keyboard behaviour, and there is no second element to
 * keep in sync with it. The knob is the `::before`.
 *
 * The one control in this library the browser does not draw for itself -- a checkbox's tick and
 * a select's arrow stay native, because those are conventions a reader already knows. A switch
 * is not: the native checkbox says nothing about a setting being on.
 */
.wp-switch {
    appearance: none;
    background: var(--wp-color-control-track);
    border: 0;
    border-radius: var(--wp-radius-full);
    cursor: pointer;
    flex: none;
    height: 31px;

    /* The browser's own margin on a checkbox, taken off, and the switch sent to the trailing
       edge of its row the way a Toggle sits in a Form. Nothing else in the row claims the space
       between: the label is `flex: none` and so is this. */
    margin: 0;
    margin-inline-start: auto;
    position: relative;
    transition: background 200ms ease-in-out;
    width: 51px;
}

.wp-switch::before {
    background: var(--wp-color-text-on-brand);
    border-radius: var(--wp-radius-full);
    box-shadow: 0 1px 3px rgb(0 0 0 / 0.2);
    content: '';
    height: 27px;
    left: 2px;
    position: absolute;
    top: 2px;
    transition: translate 200ms ease-in-out;
    width: 27px;
}

.wp-switch:checked {
    background: var(--wp-color-feedback-success);
}

.wp-switch:checked::before {
    translate: 20px 0;
}

.wp-switch:disabled {
    cursor: default;
    opacity: 0.5;
}

/* ======================================================================== upload ==
 * A picture somebody attaches: the one they have now, a control to replace it, and a line
 * saying what it should be of. Same shape as the profile picture's header, because it is the
 * same job; the label is the control because a file input draws a button nobody can style.
 */
.wp-upload {
    align-items: center;
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-sm);
    text-align: center;
}

.wp-upload-preview {
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-card);
    max-width: 280px;
    object-fit: contain;
    width: 100%;
}

.wp-upload-action {
    color: var(--wp-color-brand-primary);
    cursor: pointer;
    font: var(--wp-font-style-footnote);
}

.wp-upload-action:focus-within {
    border-radius: var(--wp-radius-tile);
    outline: 2px solid var(--wp-color-brand-primary);
    outline-offset: 2px;
}

.wp-upload-note {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    margin: 0;
}

/* ======================================================================== button ==
 * .borderedProminent at .controlSize(.large), and .buttonStyle(.plain) beside it.
 *
 * One class for both, because the difference between a primary and a secondary action
 * on iOS is a fill and nothing else: same height, same radius, same type. A button that
 * also changes size when it changes importance reads as a different control.
 */
.wp-button {
    align-items: center;
    background: transparent;
    border: 0;
    border-radius: var(--wp-radius-control);
    color: var(--wp-color-brand-primary);
    cursor: pointer;
    display: inline-flex;
    font: var(--wp-font-style-headline);
    gap: var(--wp-space-sm);
    justify-content: center;
    min-height: var(--wp-size-touch-target);
    padding: var(--wp-space-md) var(--wp-space-xl);
    text-align: center;
    text-decoration: none;
}

.wp-button-prominent {
    background: var(--wp-color-brand-primary);
    color: var(--wp-color-text-on-brand);
}

/* `Label(_, systemImage:)`: the glyph sits at the size of the words beside it. An inline SVG with
   no size of its own is as big as the box it is in, and the first button here to carry an icon
   filled its whole card with a speech bubble. */
.wp-button svg {
    height: 18px;
    width: 18px;
}

/* `.bordered` against `.borderedProminent`: the same brand, at a weight that does not compete
   with the action beside it. A wash of the brand rather than a fill of it, so a pair of buttons
   reads as one primary and one secondary instead of two primaries. Same wash the calendar's
   chosen day wears. */
/* `role: .destructive` on a filled button: the same weight as `.wp-button-prominent` in the
   feedback colour, for the one action on a screen that undoes something.
   Filled and not merely red text, because this is a screen's primary action while it applies --
   leaving an event is what the event screen offers once you are in it, exactly where joining sat
   before -- and a primary action that is also destructive should look like both. */
.wp-button-destructive {
    background: var(--wp-color-feedback-danger);
    color: var(--wp-color-text-on-brand);
}

.wp-button-tinted {
    background: color-mix(in srgb, var(--wp-color-brand-primary) 15%, transparent);
    color: var(--wp-color-brand-primary);
}

.wp-button-block {
    display: flex;
    width: 100%;
}

/* The plan you are already on. Still a button in shape, because it holds the place next to
   the one you can act on and a gap there would read as something missing. An <a> cannot be
   `disabled`, so the plan card drops the href and says `aria-disabled` instead, which is
   also what tells a screen reader. */
.wp-button[aria-disabled='true'],
.wp-button:disabled {
    background: var(--wp-color-surface-grouped);
    color: var(--wp-color-text-secondary);
    cursor: default;
}

.wp-button:disabled {
    background: var(--wp-color-surface-disabled);
    color: var(--wp-color-text-secondary);
    cursor: default;
}

@media (hover: hover) and (pointer: fine) {
    .wp-button:hover:not(:disabled) {
        opacity: 0.85;
    }
}

/* ==================================================================== action bar ==
 * .safeAreaInset(edge: .bottom): the primary action at the foot of the screen rather
 * than at the end of the form, so it stays under the thumb however long the form runs.
 *
 * Sticky, not fixed. Fixed would need the screen to reserve a matching gap at the
 * bottom and the two would drift apart the first time the bar gained a second button.
 *
 * No fill of its own. It used to paint `surface.grouped`, which is the page's colour and not
 * the colour of everything the bar is put on: in a sheet that is pure black against the panel's
 * own #1C1C1E, a dark band across the foot of it. The bar sits on whatever it is in now. The
 * cost is that content scrolls visibly under the buttons rather than behind a scrim; a bar that
 * wants one should take the surface it is on rather than name the page's.
 */
.wp-action-bar {
    bottom: 0;
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-sm);
    padding: var(--wp-space-md) 0 calc(var(--wp-space-xs) + env(safe-area-inset-bottom));
    position: sticky;
    width: 100%;
}

/* ======================================================================== notice ==
 * A message the page came back carrying, said on the page itself.
 *
 * The ones worth leaving on the page, which is now the ones that are not good news. A
 * confirmation that something worked is an `.alert` in the app and is one here too -- see
 * the next section -- but what went wrong with the thing you just tried has to stay where
 * it can be read next to the form that caused it, for as long as it takes to fix it. A
 * panel you have to dismiss before you can see the field it is talking about is the wrong
 * shape for that, and it is the right shape for the other.
 *
 * A tinted wash and not a solid fill: a failed sign-in should not turn the screen red.
 * It carried a leading rule as well, so that the colour was not the only thing saying
 * which kind of notice this was; what a notice says is in its own words, which is where
 * that meaning actually lives, and the rule read as a quotation mark against the rest of
 * a page whose cards have no such edge.
 */
.wp-notice {
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-control);
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-footnote);

    /* A notice is usually written as a <p>, and a consumer's reboot gives every <p> a bottom
       margin -- 1rem in the first consumer's. That margin is the paragraph rhythm of running
       prose, which a notice is not part of: it sits above the form it is about, and whatever
       stacks the screen is already spacing the two. Left in, it reads as the notice having
       drifted away from the field it is talking about.
       Stated here rather than left to the host because the component cannot be dropped into a
       page and look right otherwise, which is the whole promise of the class. The block axis
       only -- the inline margin is the consumer's business, and a notice never had one. */
    margin-block: 0;
    padding: var(--wp-space-md) var(--wp-space-lg);
}

.wp-notice-danger {
    background: color-mix(in srgb, var(--wp-color-feedback-danger) 12%, var(--wp-color-surface-grouped-raised));
    color: var(--wp-color-feedback-danger);
}

/* ======================================================================== footer ==
 * The legal line a site carries on every screen: the copyright, and the two documents a reader
 * is entitled to reach from anywhere.
 *
 * Quiet by design. It is the last thing on the page and the least important thing on it, so it
 * is caption-sized and secondary, and the links are underlined rather than tinted -- brand
 * colour here would make the two smallest items on the screen the loudest.
 *
 * The column is the page's own, so the notice lines up with whatever it sat under rather than
 * running the full width of a desktop window.
 */
.wp-footer {
    margin-inline: auto;
    max-width: var(--wp-width-content);
    padding: var(--wp-space-xl) var(--wp-space-lg) calc(var(--wp-space-xl) + env(safe-area-inset-bottom));
    text-align: center;
}

.wp-footer-copyright,
.wp-footer-links {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);

    /* A consumer's reboot gives a <p> a bottom margin, which between these two lines is a gap far
       larger than the type wants; the pair is spaced by its own rule below instead. */
    margin-block: 0;
}

.wp-footer-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--wp-space-xs) var(--wp-space-md);
    justify-content: center;
    margin-block-start: var(--wp-space-xs);
}

/* The separator is drawn rather than written, so the markup stays two links and a translator
   never has to carry a piece of punctuation that belongs to the layout. `flex-wrap` above means
   the pair stacks on a narrow screen, and a leading dot on a line of its own would be wrong, so
   this hangs in the gap rather than sitting inside either link. */
.wp-footer-links a + a::before {
    content: '·';

    /* `inline-block` so the dot is not underlined with the link it hangs off. Text decoration
       propagates into inline descendants and cannot be switched off from inside one, but it does
       not cross into an inline-block's own box. */
    display: inline-block;
    margin-inline-end: var(--wp-space-md);
}

/* Every link in the footer, the company's name in the copyright line included, not just the two
   legal ones below it.

   The token and not `inherit`: a component that takes its colour from whatever it was dropped
   into is a component whose contrast nobody can promise, which the library's own test enforces.
   Secondary rather than the brand tint, so the smallest items on the page are not also the
   loudest; the underline is what says they are links. */
.wp-footer a {
    color: var(--wp-color-text-secondary);
    text-decoration: underline;
}

/* ====================================================================== location ==
 * `ClubLocationCard`: a picture of where a club is, with the address under it, the whole card
 * being the link to a maps app.
 *
 * The map is an <img> and not an embedded map. A club's location is a fact on a page about the
 * club, not a thing to pan around, which is what the app decided too -- and an iframe of somebody
 * else's map is a third-party frame on every club page, with its own scripts and its own cookies.
 */
.wp-location {
    display: flex;
    flex-direction: column;

    /* The card clips the map's corners to its own radius: the tile is a rectangle and the card is
       not, and a square corner poking out of a rounded card is the kind of thing nobody can name
       but everybody sees. */
    overflow: hidden;
    padding: 0;
}

.wp-map {
    display: block;
}

/* The tile fills the card's width and keeps the aspect the server rendered. `aspect-ratio` rather
   than a fixed height, so the strip is the same shape on a phone and on a desktop column. */
.wp-map-tile {
    aspect-ratio: 64 / 28;
    display: block;
    height: auto;
    object-fit: cover;
    width: 100%;
}

/* The address is the last thing in this card, so the space `.wp-copy-text` keeps from a paragraph
   following it is dead space on top of the card's own bottom padding. */
.wp-location .wp-copy-text:last-child {
    margin-bottom: 0;
}

/* ========================================================================= alert ==
 * `.alert(_:isPresented:)`: the thing the app puts over a screen rather than on it.
 *
 * A confirmation is the case this exists for. "Your changes have been applied" is not
 * information the screen behind it needs to keep showing -- it is the answer to something
 * the reader just did, it is read once, and then it is in the way. The app says it in an
 * alert and waits to be told it was read; a page that came back with it washed green at the
 * top of the screen says it quieter than the thing deserves and then leaves it there for
 * the rest of the visit.
 *
 * A <dialog>, for the same reasons as the sheet: the backdrop, the focus trap, Escape, the
 * top layer, `inert` on the page behind, and focus handed back to wherever it came from are
 * all things the element already does correctly.
 *
 * As in the sheet, the panel takes the focus itself when it opens -- see the script. An
 * alert's first control is the button that acknowledges it, and letting `showModal()` land
 * there drew a ring round the one thing in the panel: an alert pressing for an answer rather
 * than waiting to be read. Nothing is singled out now, and a Tab reaches the action.
 *
 * It does not close on a backdrop click, which the sheet does. UIKit's alert does not
 * either: a sheet is a place you can decide to leave, and an alert is a sentence that has to
 * be acknowledged. Escape still closes it -- the platform gives that away free and a
 * keyboard reader expects it, and here it acknowledges rather than cancels, there being
 * nothing in a confirmation to cancel.
 */
.wp-alert {
    /* Prefixed first, as `.wp-glass`: Safari shipped this years before it was unprefixed. */
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);

    /* The vibrant material, not a flat surface. An alert is the one component that is
       definitely over something -- there is always a screen behind it, dimmed -- which is the
       condition the material was made for, and it is why a UIKit alert reads as a pane of
       glass with the screen showing through it rather than as a white card. */
    background: var(--wp-color-material-glass);
    border: 0;

    /* 16, where UIKit draws 14. The scale has no 14 and one corner is not worth a step of its
       own; 16 is the step next to it, and the one the cards behind the alert already use. */
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);

    /* Centred in the viewport, which is a modal dialog's own default and the reason this says
       nothing about `top`: `margin: auto` against all four edges is what centres it. */
    margin: auto;

    /* The actions run to the panel's edge, so the panel has to clip them back to its corners. */
    overflow: hidden;
    padding: 0;

    /* 270 is UIKit's alert width at every screen size, and a width rather than a proportion
       for a reason worth keeping: an alert holds a sentence or two, and a line of 13px text
       600px wide is not a sentence anybody takes in at a glance. The `min` is for the narrowest
       phone, where 270 plus a margin either side is wider than the screen. */
    width: min(270px, 100% - var(--wp-space-2xl));
}

/* Without the filter the fill is a 72% wash and the dimmed screen reads straight through the
   title -- the same fallback `.wp-glass` carries, and for the same reason. The elevated
   surface and not the plain one, for the reason the sheet takes it: in the dark appearance
   the page behind is already near-black, and a panel the same colour has no body. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .wp-alert {
        background: var(--wp-color-surface-grouped-raised-elevated);
        color: var(--wp-color-text-primary);
    }
}

/* The panel is a focus anchor rather than a control, and an anchor must not advertise the
   focus it is holding -- the same rule, and the same rule's wording, as `.wp-sheet:focus`. The
   action inside it keeps its own ring, which is where a ring means something. */
.wp-alert:focus {
    outline: none;
}

/* The text, inset from the panel; the actions below it are not, which is the whole of the
   arrangement. 16 on every side, where UIKit insets 19 above the title and 20 below the last
   line: both of those round to the same step, and one number is clearer here than four that
   all mean "about the same". */
.wp-alert-content {
    padding: var(--wp-space-lg);
    text-align: center;
}

/* An <h2> in the markup, so a reader landing in the panel is told what it is rather than
   being read a paragraph. `text-transform` is stated and not inherited for the reason
   `.wp-sheet-title` states it: a host that uppercases its headings -- this library's own
   gallery does -- would otherwise shout a sentence the app wrote in sentence case. */
.wp-alert-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    margin: 0;
    text-transform: none;
}

/* The second line, when there is one: what the title cannot say in a heading's worth of
   words. `text.primary` and not `text.secondary`, which is what UIKit does here and is
   unusual for 13px type -- an alert's message is the substance of it rather than a footnote
   to the title, and greying it would say the opposite. */
.wp-alert-message {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-footnote);
    margin: var(--wp-space-xs) 0 0;
}

/* The actions, full-bleed under a hairline. A grid and not flex, so that the one-per-row and
   the two-abreast arrangements are a change of flow rather than a change of model. */
.wp-alert-actions {
    border-top: 1px solid var(--wp-separator);
    display: grid;
}

/* Exactly two actions go side by side and any other number stacks, which is UIKit's own rule.
   It is about the labels rather than the count: two short words fit across 270px and read as
   a pair of equal choices, and the moment there are three they do not fit.
   `:has(> :nth-child(2):last-child)` is "a second child that is also the last one", there
   being no way to ask CSS directly how many children something has. */
.wp-alert-actions:has(> :nth-child(2):last-child) {
    grid-auto-columns: 1fr;
    grid-auto-flow: column;
}

/* A <button> or an <a>, the way `.wp-button` already serves both. An alert whose answer is an
   address rather than a form submission -- signing out is the one the app has -- has nothing to
   submit to, and a <form> is not open to this panel: several of the pages that raise one render
   it inside a form of their own, and a form inside a form is not HTML. So the row lays its own
   label out and drops the underline, instead of relying on a button's defaults for either. */
.wp-alert-action {
    align-items: center;
    background: none;
    border: 0;
    color: var(--wp-color-brand-primary);
    cursor: pointer;
    display: flex;
    font: var(--wp-font-style-callout);
    justify-content: center;

    /* The row is the target, so it is a full 44 tall however short the word in it is. */
    min-height: var(--wp-size-touch-target);
    padding: var(--wp-space-sm) var(--wp-space-md);
    text-decoration: none;
    width: 100%;
}

/* The crease between two stacked actions. On the first one it would be the panel's own top
   hairline a second time. */
.wp-alert-action + .wp-alert-action {
    border-top: 1px solid var(--wp-separator);
}

/* Side by side, the crease turns ninety degrees with them. */
.wp-alert-actions:has(> :nth-child(2):last-child) .wp-alert-action + .wp-alert-action {
    border-left: 1px solid var(--wp-separator);
    border-top: 0;
}

/* `.confirmationAction`: the one the alert is recommending, in semibold. A single-action
   alert is still recommending its single action -- there is nothing else to press -- so the
   OK on a confirmation carries this too. */
.wp-alert-action-preferred {
    font-weight: 600;
}

/* `role: .destructive`: the action that cannot be taken back, in red and at the normal weight.
   The emphasis goes on the one beside it -- UIKit makes Cancel the preferred action of a
   destructive alert, which is the opposite of the single-action case above. A tinted label and
   not a filled red row: the question is already the alert's title, and a block of red under it
   asks it twice. */
.wp-alert-action-destructive {
    color: var(--wp-color-feedback-danger);
}

.wp-alert::backdrop {
    background-color: rgb(0 0 0 / 0%);
}

.wp-alert[open]::backdrop {
    background-color: rgb(0 0 0 / 40%);
}

/* A grey wash in the dark appearance, where a black one is not a wash at all: `surface.base` is
   pure black there, so dimming it 40% towards black leaves it exactly as black as it was and the
   screen never reads as being behind anything. Lifting it instead is what makes the modal state
   visible -- the page goes grey, the alert stays the colour it was.
 *
 * The selectors are tokens.css's, as `.wp-logo-dark`'s are: `prefers-color-scheme` is the
 * default, `data-theme` overrides it either way, and it is honoured on any ancestor rather than
 * only on :root. `:where()` so the override weighs no more than the rule it overrides. */
@media (prefers-color-scheme: dark) {
    :where(:root:not([data-theme='light'])) .wp-alert[open]::backdrop {
        background-color: rgb(58 58 60 / 65%);
    }
}

:where([data-theme='dark']) .wp-alert[open]::backdrop {
    background-color: rgb(58 58 60 / 65%);
}

/* Down onto the screen from slightly in front of it, which is the motion UIKit gives an alert
   and the opposite of the sheet's: a sheet travels because it comes from somewhere, and an
   alert is already here and resolving into focus.
   The same four moving parts the sheet's own comment sets out -- `@starting-style` to animate
   from, `allow-discrete` on `display` and `overlay` so the panel stays rendered and in the top
   layer on the way out -- and faster, 0.2s against the sheet's 0.32s, because it travels a
   tenth of the distance. */
@media (prefers-reduced-motion: no-preference) {
    .wp-alert {
        opacity: 0;
        scale: 1.2;
        transition: display 0.2s allow-discrete,
        opacity 0.2s ease-out,
        overlay 0.2s allow-discrete,
        scale 0.2s ease-out;
    }

    .wp-alert[open] {
        opacity: 1;
        scale: 1;
    }

    @starting-style {
        .wp-alert[open] {
            opacity: 0;
            scale: 1.2;
        }
    }

    .wp-alert::backdrop {
        transition: background-color 0.2s ease-out,
        display 0.2s allow-discrete,
        overlay 0.2s allow-discrete;
    }

    @starting-style {
        .wp-alert[open]::backdrop {
            background-color: rgb(0 0 0 / 0%);
        }
    }
}

/* ==================================================================== section stacks ==
 * A ported screen is two nested stacks on iOS: an outer `VStack(spacing: 24)` holding the
 * sections, and an inner `VStack(spacing: 12)` holding a section's own heading and its content.
 * These are those two, so a screen states its rhythm once instead of hanging a margin off every
 * block it contains.
 *
 * The vertical padding is 16 where iOS pads 20. 20 is not on the spacing scale and adding it
 * would mean a new token for one screen; 16 is the step next to it.
 */
.wp-stack {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-xl);

    /* `padding-block` and not the `padding` shorthand. A screen that stacks its sections and also
       wants the centred column carries both classes, and this rule comes second in the file: the
       shorthand's own `0` for the inline edges was silently undoing `.wp-content`'s gutter, so the
       two pages written that way sat flush against the edge of the phone. A stack has an opinion
       about the space above and below its sections and none about the space either side. */
    padding-block: var(--wp-space-lg);
}

.wp-stack-tight {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-md);
}

/* `.wp-section` carries its own bottom margin for the pages that place a heading directly in the
   flow. Inside either stack the gap already says it, so the margin would say it twice. */
.wp-stack > .wp-section,
.wp-stack-tight > .wp-section {
    margin-bottom: 0;
}

/* Same again for a note, which carries a top margin for the pages that place it directly under a
   card in the flow. Inside a stack that margin lands on top of the gap and the card ends up with
   two different rhythms down it. */
.wp-stack > .wp-list-note,
.wp-stack-tight > .wp-list-note {
    margin-top: 0;
}

/* ==================================================================== contact actions ==
 * ContactActionsRow: round, icon-only buttons -- message, call, email, save to contacts --
 * on the pattern of the circular actions at the top of a contact card in the system
 * Contacts app.
 *
 * The values themselves are never drawn. A phone number and an email address are things to
 * act on, not things to publish on a page about somebody else, and the app takes the same
 * line. A button whose field is missing is simply not rendered, so the row is one to four
 * buttons wide and centred either way.
 *
 * Not `.wp-action-bar`, which is the full-width bar a screen's primary action sits in at
 * the foot of the page.
 */
.wp-actions {
    display: flex;
    gap: var(--wp-space-md);
    justify-content: center;
}

.wp-action {
    align-items: center;
    background: var(--wp-color-brand-primary);
    border: 0;
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-on-brand);
    cursor: pointer;
    display: inline-flex;
    flex: none;
    height: 56px;
    justify-content: center;
    width: 56px;
}

.wp-action svg {
    height: 20px;
    width: 20px;
}

/* ==================================================================== contact card ==
 * ContactUserView's `playerCard`: who you are about to write to, and the three ways to do it.
 *
 * Not `.wp-actions` above, though the two sit one screen apart and answer the same question. That
 * row is four equal circles on somebody's profile, where no one of them is the thing to do; this
 * is a sheet you opened in order to send a message, so Message is a full-width prominent button
 * and the other two are tinted and sized to their own labels. A sheet whose purpose is the top
 * button should look like it.
 *
 * The card is a `.wp-card .wp-card-body`, so it takes the sheet's own raised surface with every
 * other card in one. These are the three stacks inside it.
 */
.wp-contact-list {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-lg);
}

.wp-contact-card {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-lg);
}

.wp-contact-identity {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
}

/* The surname under the first name, at subheadline rather than the caption a row's second line
   usually takes.
   A contact card is not a row in a list: the name is the whole of its content, the avatar beside
   it is `wp-avatar-lg`, and a caption under that reads as a footnote to a heading rather than as
   the other half of somebody's name. Scoped to the card so the rows that do want a caption --
   the venue on this same screen, the club's own rows -- keep theirs. */
.wp-contact-identity .wp-row-subtitle {
    font: var(--wp-font-style-subheadline);
}

.wp-contact-actions {
    display: flex;
    gap: var(--wp-space-sm);
}

/* The message button takes the slack and the other two keep their own width, which is what makes
   the row read as one action with two alternatives rather than three of equal weight. The button
   padding is what sizes the other two, so Call is as wide as the word and the envelope is square. */
.wp-contact-actions > .wp-button-prominent {
    flex: 1;
}

/* Centred, under the cards rather than beside any one of them: it is true of the sheet and not of
   a particular player. Its own class and not `.wp-sheet-note`, which is a left-aligned footnote
   under a field and says what that field is for. */
.wp-contact-note {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    margin: var(--wp-space-lg) 0 0;
    text-align: center;
    text-wrap: pretty;
}

/* ==================================================================== card copy ==
 * A title over a paragraph inside a card: ClubAboutCard's heading and description, and
 * ClubLocationCard's street line over its city line. Two cards on one screen with the same
 * shape, so one pair of classes rather than a set each.
 *
 * The text wraps. It is the one thing that separates this from `.wp-row-title`/`.wp-row-subtitle`,
 * which truncate to one line because they sit in a fixed-height row; an address and a club's own
 * description are both as tall as they need to be.
 */
.wp-copy {
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-xs);
}

.wp-copy-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    margin: 0 0 var(--wp-space-xs);
}

/* The card can hold more than one of these pairs -- a club writes as many description paragraphs
   as it likes -- so the text carries the space that separates one pair from the next. The flex
   gap above cannot: it is one value between every child and would space a title from its own
   paragraph by as much as from the paragraph before it. */
.wp-copy-text {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
    margin: 0 0 var(--wp-space-sm);
}

/* ======================================================================== media ==
 * ClubMediaStrip: a club's photographs, and its film first when it has one, as a row that
 * scrolls. The strip itself is a `.wp-carousel`, which is what gives it the full-bleed scroll
 * and the gutter the first tile rests at; these are the tiles in it.
 *
 * A fixed width and a square crop, so a row of them reads as a row rather than as a ragged
 * line of whatever aspect each photograph happens to be. `object-fit: cover` does the cropping,
 * which is what `scaledToFill` does on the app's own.
 */
.wp-media {
    aspect-ratio: 1;
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-primary);
    display: block;
    flex: none;
    overflow: hidden;
    position: relative;
    width: 140px;
}

.wp-media img {
    height: 100%;
    object-fit: cover;
    width: 100%;
}

/* The film's tile says so with a glyph over the poster, centred. The scrim is on the glyph
   rather than over the whole image: a wash across the poster dims the one tile a reader is most
   likely to want, to say something the glyph already says. */
.wp-media-play {
    align-items: center;
    background: color-mix(in srgb, var(--wp-color-surface-grouped) 70%, transparent);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-primary);
    display: inline-flex;
    height: 44px;
    justify-content: center;
    left: 50%;
    position: absolute;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 44px;
}

.wp-media-play svg {
    height: 18px;
    width: 18px;
}

/* ================================================================== media viewer ==
 * MediaViewer: a club's film and its photographs, full screen on black, one per page.
 *
 * A native `<dialog>`, so Escape closes it, the page behind it goes inert and focus stays inside
 * without any of that being written here or in script. The pager is scroll snapping rather than
 * scripted: a swipe on a phone and a flick on a trackpad both land on a page, and the only thing
 * the controller has to do is say which page it opened on.
 *
 * Black, not the grouped surface: this is the one screen where the photograph is the whole of the
 * content and the page around it should stop existing, which is what the app does too.
 */
.wp-media-viewer {
    background: rgb(0 0 0);
    border: 0;
    color: var(--wp-color-text-on-brand);
    height: 100%;
    max-height: 100%;
    max-width: 100%;
    padding: 0;
    width: 100%;
}

.wp-media-viewer::backdrop {
    background: rgb(0 0 0);
}

.wp-media-viewer-track {
    display: flex;
    height: 100%;
    overflow-x: auto;

    /* The gesture must not chain to the page behind the dialog once the last photograph is
       reached, which is the same reason `.wp-carousel` contains its own. */
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
}

/* The track is focused by script so that the arrow keys reach it, not because the reader tabbed
   there, so it draws no ring. Paging a gallery should move the photograph and nothing else. The
   close button keeps its own focus ring, which is the one a reader does tab to. */
.wp-media-viewer-track:focus {
    outline: none;
}

.wp-media-viewer-item {
    align-items: center;
    display: flex;
    flex: none;
    height: 100%;
    justify-content: center;
    scroll-snap-align: center;
    width: 100%;
}

/* `contain`, so a tall photograph is shown whole rather than cropped to the viewport. The app
   uses `scaledToFit` for the same reason: in a viewer the subject is the picture itself, where in
   a strip it is one tile of a row and cropping is what keeps the row even. */
.wp-media-viewer-item img,
.wp-media-viewer-item video {
    max-height: 100%;
    max-width: 100%;
    object-fit: contain;
}

.wp-media-viewer-counter {
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-on-brand);
    font: var(--wp-font-style-subheadline);
    left: 50%;
    padding: 6px var(--wp-space-md);
    position: fixed;
    top: var(--wp-space-md);
    transform: translateX(-50%);
}

.wp-media-viewer-close {
    align-items: center;
    background: var(--wp-color-surface-chip);
    border: 0;
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-on-brand);
    cursor: pointer;
    display: inline-flex;
    height: 44px;
    justify-content: center;
    position: fixed;
    right: var(--wp-space-md);
    top: var(--wp-space-md);
    width: 44px;
}

.wp-media-viewer-close svg {
    height: 18px;
    width: 18px;
}

/* ==================================================================== metric tiles ==
 * SkillStatsSection, RankingTile and FormTile: a row of equal cards, each a number with a
 * caption under it.
 *
 * The children divide the row evenly, which is what gives the app's three arrangements --
 * level beside ranking, ranking beside form, or one card alone at full width -- off one
 * set of markup rather than a modifier per arrangement. A tile with nothing to show is left
 * out and the rest spread into its place.
 *
 * The surface is `.wp-card`; everything here is layout and type.
 */
.wp-metric-row {
    display: flex;
    gap: var(--wp-space-md);
}

.wp-metric {
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    gap: var(--wp-space-xs);
    min-width: 0;
}

/* The small uppercase label over a metric, where the tile needs naming before its number
   means anything ("WEPLAY LEVEL"). A tile whose number speaks for itself has none. */
.wp-metric-eyebrow {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    font-weight: 600;
    text-transform: uppercase;
}

.wp-metric-value {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-title);
    font-variant-numeric: tabular-nums;
}

/* FormTile's number is 22 where a ranking's is 28: a win-loss record is a pair of numbers
   and reads as one thing, so it carries less weight than a position does. */
.wp-metric-value-sm {
    font: var(--wp-font-style-title-2);
    font-variant-numeric: tabular-nums;
}

.wp-metric-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-subheadline);
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wp-metric-caption {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
}

/* The both-sports level card: two columns either side of a hairline, each a level over the
   discipline it belongs to. */
.wp-metric-split {
    display: flex;
}

.wp-metric-column {
    align-items: center;
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.wp-metric-column + .wp-metric-column {
    border-left: 1px solid var(--wp-color-border-subtle);
}

/* ==================================================================== medals ==
 * Gold, silver and bronze for the top three positions.
 *
 * Fixed decorative colours rather than a light/dark pair, because a medal does not change
 * colour with the system appearance, and deliberately not tokens: no semantic token exists
 * for them in weplay.tokens.json, which is the same decision MedalTier.swift records. The
 * two carry the same three values, so change them together or not at all.
 *
 * The tints set a background and nothing else, so they dress either shape: the dot below,
 * or the badge under it.
 */
.wp-medal {
    border-radius: var(--wp-radius-full);
    flex: none;
    height: 10px;
    width: 10px;
}

/* RankingsSection's badge: the position itself, always drawn, neutral when it is outside the
   top three. Deliberately unlike the dot above, which is omitted from fourth place down --
   one section wants "a medal if you earned one", the other wants "always a badge". */
.wp-rank-badge {
    align-items: center;
    background: var(--wp-color-text-secondary);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-on-brand);
    display: inline-flex;
    flex: none;
    font: var(--wp-font-style-subheadline);
    font-weight: 700;
    height: 36px;
    justify-content: center;
    width: 36px;
}

/* The one "best" colour this product has, named rather than written in place: a literal repeated
   is how two things that should match drift apart. */
:root {
    --wp-medal-gold: #d4ad36;
}

.wp-medal-gold {
    background: var(--wp-medal-gold);
}

.wp-medal-silver {
    background: #c7c7c7;
}

.wp-medal-bronze {
    background: #cc8033;
}

/* ==================================================================== detail chips ==
 * AboutSection's chips: a wrapping row of small facts about a player -- their age, height
 * and weight, which hand they play with, where they live, how long they have been here.
 *
 * There is no empty state and no placeholder: a fact without a value is not drawn, so the
 * row shrinks to what is actually known and disappears when nothing is.
 */
.wp-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--wp-space-sm);
}

/* The raised grouped surface, the same one the cards beside them paint, which is what iOS's
   `DetailChip` fills its capsule with too. Not `surface.chip` despite the name: that token is a
   translucent grey wash for a shape standing in for content it does not have -- an avatar with no
   picture, a loading block -- and a chip holds a fact. Against the white cards in the same section
   the wash read as a row of grey holes. */
.wp-chip {
    background: var(--wp-color-surface-grouped-raised);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-footnote);
    padding: 6px var(--wp-space-md);
}

/* ==================================================================== avatar badge ==
 * The nationality flag set into the corner of a profile picture, as ProfileHeaderView
 * draws it. It is the only nationality signal on that screen: the "birthplace" row the
 * page used to carry is gone, because a flag beside a face says it already.
 */
.wp-avatar-badged {
    display: inline-flex;
    position: relative;
}

.wp-avatar-badge {
    bottom: 6px;
    position: absolute;
    right: 6px;
}

/* ==================================================================== date tile ==
 * NextMatchCard's leading tile: the month over the day, in a neutral square. The month is the
 * accent and the day is the ink, which is what makes a column of these scannable by date
 * rather than by month.
 *
 * `radius.tile`, and 46px rather than the 36 iOS draws: a browser row has the height to give the
 * date more presence, and at 36 the month sitting over the day left neither room to breathe. The
 * size is local rather than a token because exactly one component uses it.
 */
.wp-date-tile {
    --wp-date-tile-size: 46px;
    align-items: center;
    background: var(--wp-color-surface-icon-badge);
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-primary);
    display: flex;
    flex: none;
    flex-direction: column;
    height: var(--wp-date-tile-size);
    justify-content: center;
    width: var(--wp-date-tile-size);
}

.wp-date-tile-month {
    color: var(--wp-color-feedback-danger);
    font: var(--wp-font-style-caption-2);
    font-size: 9px;
    font-weight: 700;
    line-height: 1;
    text-transform: uppercase;
}

.wp-date-tile-day {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    line-height: 1.2;
}

/* ==================================================================== outcome badge ==
 * RecentMatchesCard's leading badge: a won or lost match, as a letter in a tinted circle.
 *
 * Not `.wp-outcome`, which is the filled square of a form run: that one is read as part of a
 * sequence and wants to be solid, where this one sits at the head of a row of text and would
 * shout. It takes the same `-win`/`-loss` classes, so "this is a win" is still said one way,
 * and overrides the fill those carry.
 */
.wp-outcome-badge {
    --wp-outcome-badge-size: 28px;
    align-items: center;
    border-radius: var(--wp-radius-full);
    display: inline-flex;
    flex: none;
    font: var(--wp-font-style-caption);
    font-weight: 700;
    height: var(--wp-outcome-badge-size);
    justify-content: center;
    width: var(--wp-outcome-badge-size);
}

.wp-outcome-badge.wp-outcome-win {
    background: color-mix(in srgb, var(--wp-color-feedback-success) 15%, transparent);
    color: var(--wp-color-feedback-success);
}

.wp-outcome-badge.wp-outcome-loss {
    background: color-mix(in srgb, var(--wp-color-feedback-danger) 15%, transparent);
    color: var(--wp-color-feedback-danger);
}

/* A row whose leading glyph is followed by a line of its own, and sometimes a second under it:
   NextMatchCard's "who, then when", RecentMatchesCard's "who, then the score".

   Deliberately not `.wp-list-label`/`.wp-list-value`, which is the grouped list's idiom for a
   fact -- a secondary label leading, its value trailing and right-aligned. These rows are the
   other shape: the first line IS the content and takes the foreground, and anything under it is
   the footnote. Using the fact pair here put the opponent's name in the secondary ink and pushed
   the when-line to the right edge.

   One title weight for both rows. iOS draws a fixture's semibold and a result's medium; a step
   between two adjacent sections is the kind of difference nobody chose, so they share 600. */
.wp-row-text {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.wp-row-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-subheadline);
    font-weight: 600;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wp-row-subtitle {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A subtitle in the brand accent rather than in secondary ink: the "in two weeks" under a
   fixture's date, which is the part of that row worth reacting to. */
.wp-row-accent {
    color: var(--wp-color-brand-primary);
    font: var(--wp-font-style-caption);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The score at the trailing edge of a recent-match row, from the viewer's side. Tabular so a
   column of them lines up on the hyphen. */
.wp-row-score {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    font-variant-numeric: tabular-nums;
    margin-left: auto;
    white-space: nowrap;
}

/* A result badge inside a list row sits where that row's score sits, because for a walkover it
   IS the score: there was no play, so there are no games to print. A retirement has both -- the
   partial score the match was abandoned at, and the badge saying why it stopped -- and they end
   up adjacent at the trailing edge, the badge first, because the auto margin ahead of it has
   already taken the free space. */
.wp-list-row .wp-match-badge {
    margin-left: auto;
}

/* ==================================================================== face-off ==
 * MatchHeroView: the two sides of a fixture facing each other across a VS medallion, with a
 * sentence under them saying who has to challenge whom.
 *
 * The same brand gradient `.wp-hero` wears and deliberately not `.wp-hero` itself. That one is
 * `radius.hero` with a 210px floor because it is a card in a carousel; this is the full-width
 * head of a page at `radius.card`, sized by whatever its two sides come to. The 20s are
 * MatchHeroView's own padding and spacing, which is why they are literals here: 20 is still not
 * `space.xl`, and a change to the spacing scale must not move them.
 *
 * The watermark is dialled right down, as it is on iOS: on the home hero the mark sits beside
 * the content, and here the two sides sit on top of it.
 */
.wp-faceoff {
    --wp-hero-watermark-opacity: 0.08;
    background: linear-gradient(135deg, var(--wp-color-brand-primary), color-mix(in srgb, var(--wp-color-brand-primary) 78%, transparent));
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-on-brand);
    display: flex;
    flex-direction: column;
    gap: 20px;
    overflow: hidden;
    padding: 20px;
    position: relative;
}

/* `alignment: .top`: the two sides hang from the same line whatever each one's name wraps to,
   so the medallion stays level with the avatars rather than with the middle of the taller side. */
.wp-faceoff-sides {
    align-items: flex-start;
    display: flex;
    gap: var(--wp-space-sm);
}

/* Each side takes half the card and its content is centred within that half, then pushed to the
   outer edge: the avatar and the name read as a pair, and the pair leans away from the
   medallion. `flex-basis: 0` so a long name grows the line height rather than the column. */
.wp-faceoff-side {
    align-items: center;
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}

.wp-faceoff-side:first-child {
    align-items: flex-start;
}

.wp-faceoff-side:last-child {
    align-items: flex-end;
}

/* Solid white with brand ink, the inverse of the resting medallion: on this fill a tinted one
   would vanish into it. The ring echoes the avatars either side of it, which carry the same
   half-opaque white. `padding-top` rather than `align-self`, because it hangs level with the
   avatars and not with the names under them. */
.wp-faceoff-vs {
    align-items: center;

    /* `text.on-brand`, not a raised surface: this is `.background(Color.white, in: Circle())`, and
       it has to stay white in the dark appearance because the fill behind it is brand-coloured in
       both. Same reasoning as `.wp-hero-cta`'s own capsule. */
    background: var(--wp-color-text-on-brand);
    border: 1.5px solid color-mix(in srgb, var(--wp-color-text-on-brand) 50%, transparent);
    border-radius: var(--wp-radius-full);
    box-sizing: border-box;
    color: var(--wp-color-brand-primary);
    display: inline-flex;
    flex: none;
    font: var(--wp-font-style-caption);
    font-weight: 700;
    height: 44px;
    justify-content: center;
    margin-top: var(--wp-space-xl);
    width: 44px;
}

.wp-faceoff .wp-avatar-ring {
    --wp-avatar-ring-color: color-mix(in srgb, var(--wp-color-text-on-brand) 50%, transparent);
    --wp-avatar-ring-gap-fill: var(--wp-color-brand-primary);
}

/* `placeholderTint` is the card's own ink at 0.2, so a player with no picture reads as part of
   the card rather than a hole in it. */
.wp-faceoff .wp-avatar {
    background: color-mix(in srgb, var(--wp-color-text-on-brand) 20%, transparent);
}

/* A singles side is one tap: avatar and name together, 92px because the face-off is the one
   place a player's picture is the subject rather than a row's leading glyph. */
.wp-faceoff-player {
    align-items: center;
    color: var(--wp-color-text-on-brand);
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 100%;
    min-width: 0;
    text-decoration: none;
}

.wp-faceoff-player .wp-avatar-ring {
    --wp-size-avatar: 92px;
}

/* A doubles pair's avatars overlap by 18 of their 64, drawn in the side's own reading order so
   the front one is the one nearer the medallion. */
.wp-faceoff-pair {
    --wp-size-avatar: 64px;
    display: flex;
    flex: none;
}

.wp-faceoff-pair .wp-avatar-ring + .wp-avatar-ring {
    margin-left: -18px;
}

/* The pair's names, one per line and each its own link. `spacing: 2`, against the side's own 10
   between the avatars and this block. */
.wp-faceoff-names {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-width: 100%;
    min-width: 0;
}

/* Each name fills the block and is placed by its text alignment, rather than being a
   shrink-wrapped box centred in it: the pair's two lines have to agree on an edge. */
.wp-faceoff-names .wp-faceoff-player {
    align-items: stretch;
}

.wp-faceoff-side:first-child .wp-faceoff-names {
    text-align: left;
}

.wp-faceoff-side:last-child .wp-faceoff-names {
    text-align: right;
}

/* A singles side's two lines are centred on each other, under a picture that is centred on them
   too: `VStack(alignment: .center)` with `.multilineTextAlignment(.center)`. Left to itself the
   shorter line sits against the longer one's leading edge, which reads as a name that has slipped
   rather than as one that is placed.
   Only here. A pair's names are edge-aligned instead, because there the block is wider than
   either line and iOS aligns that one to the side's own edge -- see `.wp-faceoff-names`. */
.wp-faceoff-player .wp-name {
    text-align: center;
}

/* The names take the card's ink rather than the grouped list's: `text.secondary` is a grey
   smudge on purple. The surname carries the side, so it is a headline here and not the
   subheadline a row's name is. */
.wp-faceoff .wp-name-first {
    color: color-mix(in srgb, var(--wp-color-text-on-brand) 85%, transparent);
}

.wp-faceoff .wp-name-last {
    color: var(--wp-color-text-on-brand);
    font: var(--wp-font-style-headline);
    font-weight: 600;
}

.wp-faceoff .wp-name-single {
    color: var(--wp-color-text-on-brand);
    font-weight: 600;
}

/* Who has to challenge whom. Centred under both sides, because it is about the pair of them. */
.wp-faceoff-note {
    color: color-mix(in srgb, var(--wp-color-text-on-brand) 85%, transparent);
    font: var(--wp-font-style-caption);
    margin: 0;
    text-align: center;
}

/* ==================================================================== calendar ==
 * `DatePicker(...).datePickerStyle(.graphical)`: a month laid out in full, so a date is chosen by
 * looking at the month rather than by opening the browser's own picker over the sheet it was
 * asked for from.
 *
 * The month is drawn by script from a real `<input type="date">`, which stays in the markup as
 * the value the form posts and as the control a page with no script still has. Nothing here is
 * required for the field to work; only the grid goes missing. That is also why the month and the
 * weekday names are not copy: they come from `Intl.DateTimeFormat` in the page's own language,
 * so the library ships no calendar strings to translate.
 */
.wp-calendar {
    /* The same surface the fields under it sit on, and the same name for it, so that a sheet
       raising one raises the other: a picker and the two boxes beside it are one group of controls
       and would read as two cards if they were different colours. The horizontal padding matches a
       field row's, which is what puts the grid's edge on the same line as the labels below it. */
    /* The wash behind a day the pointer is on and behind the day that is chosen. One value, because
       hovering is meant to show what choosing would look like; two copies of it would drift. */
    --wp-calendar-day-tint: color-mix(in srgb, var(--wp-color-brand-primary) 15%, transparent);
    --wp-field-surface: var(--wp-color-surface-grouped-raised);

    background: var(--wp-field-surface);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-sm);
    padding: var(--wp-space-md) var(--wp-space-lg);
}

/* The month leads and the steps sit together at the trailing edge, where a thumb finds them
   without crossing the name. The month takes the slack, so the buttons stay put as it changes
   length from month to month.
   However many steps there are: a birthdate calendar carries a year arrow on each side of the
   month pair, and a column count written out here would have put them on a second row. */
.wp-calendar-header {
    align-items: center;
    display: grid;
    grid-auto-columns: auto;
    grid-auto-flow: column;
    grid-template-columns: 1fr;
}

.wp-calendar-month {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-headline);
}

/* Most languages name a month in lower case and this one is a heading, so it takes a capital.
   The first letter and not `capitalize`, which capitalises every word and turns the Catalan
   "octubre del 2026" into "Octubre Del 2026". */
.wp-calendar-month::first-letter {
    text-transform: uppercase;
}

.wp-calendar-nav {
    align-items: center;
    background: none;
    border: 0;
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-brand-primary);
    cursor: pointer;
    display: inline-flex;
    height: var(--wp-size-touch-target);
    justify-content: center;
    padding: 0;
    width: var(--wp-size-touch-target);
}

.wp-calendar-nav svg {
    height: 16px;
    width: 16px;
}

.wp-calendar-nav:disabled {
    color: var(--wp-color-text-secondary);
    cursor: default;
    opacity: 0.4;
}

.wp-calendar-grid {
    display: grid;
    gap: 2px;
    grid-template-columns: repeat(7, 1fr);
}

.wp-calendar-weekday {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-caption-2);
    padding-bottom: var(--wp-space-xs);
    text-align: center;
    text-transform: uppercase;
}

/* Square cells, so a month reads as a block rather than as seven columns of different weights.
   `aspect-ratio` and not a height, because the grid's own column width is what sets the scale and
   a fixed height would stop agreeing with it the moment the sheet is a different width. */
.wp-calendar-day {
    align-items: center;
    aspect-ratio: 1;
    background: none;
    border: 0;
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-primary);
    cursor: pointer;
    display: flex;
    font: var(--wp-font-style-subheadline);
    justify-content: center;
    padding: 0;
}

/* Today is named rather than filled: the fill means "chosen", and a day that is merely today has
   not been chosen. */
.wp-calendar-day[data-today] {
    color: var(--wp-color-brand-primary);
    font-weight: 600;
}

/* The chosen day is named in the brand colour and sat on a wash of it, rather than reversed out
   of a solid disc. The same pair `.wp-hero-tint`'s own label wears: at 15% the circle reads as a
   highlight on the month rather than as a hole punched in it, and the date underneath stays a
   date -- which matters here because the neighbouring day is one tap away and a solid fill makes
   the chosen one the only legible thing in the row. */
.wp-calendar-day[aria-pressed='true'] {
    background: var(--wp-calendar-day-tint);
    color: var(--wp-color-brand-primary);
    font-weight: 600;
}

/* Hovering shows what choosing would look like, which is why it is the same wash and the same ink
   rather than a weaker rehearsal of them.
   Behind `hover: hover` because a touch screen reports a hover it never leaves: without the query
   the tint stays on the last day tapped and the month ends up looking like it has two chosen days.
   `:enabled`, so a day outside the window does not offer something a click will not do. */
@media (hover: hover) {
    .wp-calendar-day:enabled:hover {
        background: var(--wp-calendar-day-tint);
        color: var(--wp-color-brand-primary);
    }
}

/* The cells before the first of the month. Empty rather than filled with the previous month's
   dates, which would be days that cannot be chosen sitting among days that can, and a cell rather
   than an offset on the first day so the grid holds its own shape.
   Nothing pads the end: the month takes the rows it needs, so the card is shorter for a month
   that fits in five weeks than for one that spills into six. */
.wp-calendar-blank {
    aspect-ratio: 1;
}

/* A day the server would refuse. Shown rather than left out, because a month with holes in it
   reads as broken; it simply cannot be picked. */
.wp-calendar-day:disabled {
    color: var(--wp-color-text-secondary);
    cursor: default;
    opacity: 0.35;
}

/* The hour, under the month. `displayedComponents: [.date, .hourAndMinute]` puts the two in one
   picker rather than in two controls a card apart, and a fixture is scheduled for a moment rather
   than for a day: splitting them made the time read as a separate decision.
   A row showing the time, which opens the wheels when it is asked to, because that is what the
   app's own picker does: a month is worth the space it takes and two scrollers under it are not,
   until somebody wants to change the hour.
   A `<details>`, so the disclosure, the keyboard and the ARIA are the platform's and the row
   still opens on a page with no script. A hairline and not a gap, because it is another part of
   the same card. */
.wp-calendar-time {
    border-top: 1px solid var(--wp-separator);
    margin-top: var(--wp-space-xs);
    padding-top: var(--wp-space-sm);
}

.wp-calendar-time-summary {
    align-items: center;
    cursor: pointer;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: space-between;
    list-style: none;
    min-height: var(--wp-size-touch-target);
}

/* Safari draws its own triangle through `list-style`, so it takes a rule of its own. */
.wp-calendar-time-summary::-webkit-details-marker {
    display: none;
}

/* The value as a tappable pill, which is the shape a compact `DatePicker` puts its field in. It
   takes the brand while the wheels are open, the way that field does once it is being edited. */
.wp-calendar-time-value {
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-control);
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-body);
    font-variant-numeric: tabular-nums;
    padding: var(--wp-space-xs) var(--wp-space-md);
}

.wp-calendar-time[open] .wp-calendar-time-value {
    color: var(--wp-color-brand-primary);
}

/* The field the wheels are drawn from, once they have been drawn. Until then it is the control,
   and the row it sits in has the inset the card already gives everything else. */
.wp-calendar-time .wp-field {
    padding-inline: 0;
}

[data-wp-time-wheel][data-enhanced='true'] .wp-calendar-field {
    display: none;
}

/* The field the grid is drawn from, once it has been drawn. Until then it is the control. */
.wp-calendar[data-enhanced='true'] .wp-calendar-field {
    display: none;
}

/* ==================================================================== pagination ==
 * Discrete pages, drawn as a segmented control.
 *
 * The app has no such thing: `StandingsViewModel` asks for page one and appends the next as the
 * last row appears, so there is nothing to press and no page you are on. That is the right answer
 * for a list under a thumb and the wrong one for a document: a server-rendered page that a
 * browser can link to, bookmark, open in a tab and go back through is a different kind of object,
 * and faking infinite scroll on top of it would make it an application that merely looks like
 * one. So this is the web's own control rather than a port of something -- but drawn in the
 * language the app already speaks, because being on page three is the same sort of statement as
 * being on the Standings segment, and the two should not look like they came from different
 * products.
 *
 * One capsule, chevrons included. Pulling the steps out into buttons of their own reads as three
 * controls that happen to be adjacent; keeping them inside the trough makes it one thing.
 */
.wp-pagination {
    align-items: center;
    display: flex;
    justify-content: center;
    margin: var(--wp-space-lg) 0;
}

.wp-pagination-list {
    /* The trough around the stops, as `.wp-segmented` does it, so the two controls are visibly the
       same component wearing different contents. */
    --wp-pagination-inset: 2px;
    align-items: center;
    background: var(--wp-color-control-track);
    border-radius: var(--wp-radius-full);
    display: flex;
    gap: var(--wp-pagination-inset);
    padding: var(--wp-pagination-inset);
}

.wp-pagination-item,
.wp-pagination-step {
    align-items: center;
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-footnote);
    justify-content: center;

    /* Wide enough that a one-digit page and a three-digit one are the same shape, so the control
       does not change width as you walk it. */
    min-width: 34px;
    padding: 6px var(--wp-space-sm);
    text-decoration: none;
}

.wp-pagination-step {
    display: inline-flex;
}

.wp-pagination-step svg {
    height: 16px;
    width: 16px;
}

/* The step at the end of the run stays in place rather than disappearing: a control that loses a
   button when you reach the last page moves the other one under the finger that was aiming at it. */
.wp-pagination-step[aria-disabled='true'] {
    color: var(--wp-color-text-secondary);
    opacity: 0.4;
    pointer-events: none;
}

/* The page you are on is a raised tile, the way the selected segment is: lifted off the trough
   rather than tinted, so the control reads the same whatever the brand colour is. */
.wp-pagination-item[aria-current='page'] {
    background: var(--wp-color-surface-grouped-raised);
    box-shadow: 0 0.5px 3px rgb(0 0 0 / 0.12);
    color: var(--wp-color-text-primary);
    font-weight: 600;
}

.wp-pagination-gap {
    align-items: center;
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    justify-content: center;
    min-width: var(--wp-space-xl);
}

/* What the control says when there is no room for the stops themselves. */
.wp-pagination-status {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-footnote);
    padding: 6px var(--wp-space-md);
    white-space: nowrap;
}

/* Narrow first: a phone gets the two chevrons and a statement of where it is, because seventeen
   stops across 320px is either a wrap or a crush. The list is still walkable, one page at a time,
   which is the only thing a thumb was going to do with it anyway. */
.wp-pagination-item,
.wp-pagination-gap {
    display: none;
}

@media (width >= 480px) {
    .wp-pagination-item,
    .wp-pagination-gap {
        display: inline-flex;
    }

    .wp-pagination-status {
        display: none;
    }
}

@media (hover: hover) and (pointer: fine) {
    .wp-pagination-item:not([aria-current]):hover,
    .wp-pagination-step:not([aria-disabled='true']):hover {
        background: color-mix(in srgb, var(--wp-color-text-secondary) 10%, transparent);
    }
}

.wp-pagination-item:focus-visible,

/* ================================================================== upload sheet ==
 * The sheet that asks for a photo of something: a membership card today, any document
 * the club has to see with its own eyes tomorrow.
 *
 * It is a sheet rather than a page because the answer is one picture and a number, and
 * because the list it is opened from is the context. What it adds over `.wp-sheet` is a
 * header that is an eyebrow and a way out rather than two competing actions, a drop zone
 * shaped like the thing being photographed, and a footer pinned under the content with
 * the one action in it.
 *
 * The accent throughout is `--wp-color-brand-primary`, which is palette.blue.500 in light
 * and palette.blue.400 in dark, so nothing here needs a second colour per theme.
 */

/* The empty state: a box the shape of the thing it is asking for. 1.586 is the ID-1 card
   ratio every membership card, bank card and driving licence is cut to, so the box reads
   as a card-shaped hole before a word of it is read. */
.wp-dropzone {
    align-items: center;
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    display: flex;
    flex-direction: column;
    gap: var(--wp-space-md);
    justify-content: center;
    padding: var(--wp-space-lg);
    text-align: center;
}

.wp-dropzone-badge {
    align-items: center;
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-brand-primary);
    display: flex;
    justify-content: center;
}

.wp-dropzone-badge svg {
    height: 56px;
    width: 56px;
}

.wp-dropzone-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-callout);
    font-weight: 600;
    margin: 0;
}

.wp-dropzone-hint {
    color: var(--wp-color-text-secondary);
    font: var(--wp-font-style-subheadline);
    margin: 0;
}

/* The attached state, in the same box the empty state drew, so the photo lands where the
   outline promised it would. */
.wp-upload-frame {
    aspect-ratio: 1.586;
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-card);
    color: var(--wp-color-text-primary);
    overflow: hidden;
    position: relative;
}

.wp-upload-image {
    display: block;
    height: 100%;
    object-fit: cover;
    width: 100%;
}

/* Over the photo rather than under it: the photo is the whole box, and a control below it
   would push the confirmation row off the first screenful on a short phone. */
.wp-upload-change {
    backdrop-filter: blur(12px);
    background: var(--wp-color-material-glass);
    border: 1px solid var(--wp-color-material-glass-border);
    border-radius: var(--wp-radius-full);
    color: var(--wp-color-text-primary);
    cursor: pointer;
    font: var(--wp-font-style-footnote);
    font-weight: 600;
    min-height: 32px;
    padding: 0 var(--wp-space-md);
    position: absolute;
    right: var(--wp-space-sm);
    top: var(--wp-space-sm);
}

.wp-upload-change:focus-visible {
    outline: 2px solid var(--wp-color-brand-primary);
    outline-offset: 2px;
}

.wp-upload-result {
    align-items: center;
    display: flex;
    gap: var(--wp-space-md);
    justify-content: space-between;
}

.wp-upload-result-text {
    align-items: center;
    color: var(--wp-color-text-primary);
    display: flex;
    font: var(--wp-font-style-subheadline);
    gap: var(--wp-space-sm);
    min-width: 0;
}

.wp-upload-result-text svg {
    color: var(--wp-color-feedback-success);
    flex: none;
    height: 18px;
    width: 18px;
}

/* The one filename on the screen, which is the only part of this row that can be long. */
.wp-upload-result-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}


/* ===================================================================== session ==
 * A sign-in, as a row of a grouped list: what the session is, when it was last used,
 * and the way to end it.
 *
 * Only the parts the list has no opinion about are here. The row is `.wp-list-row`, the
 * group is `.wp-list`, the heading is `.wp-list-header`, and the two tags are
 * `.wp-status` and `.wp-status-active` -- the same ones every other screen uses.
 */

/* The device glyph, in a tile rather than loose, so a phone and a laptop occupy the same
   square and the titles beside them line up down the list. */
.wp-session-icon {
    align-items: center;
    background: var(--wp-color-surface-chip);
    border-radius: var(--wp-radius-tile);
    color: var(--wp-color-text-secondary);
    display: flex;
    flex: none;
    height: var(--wp-size-icon-badge);
    justify-content: center;
    width: var(--wp-size-icon-badge);
}

.wp-session-icon svg {
    height: 20px;
    width: 20px;
}

/* The column between the glyph and the action. `min-width: 0` is what lets the title
   inside it ellipsise rather than push the action off the row. */
.wp-session-info {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

/* One line, and an ellipsis rather than a second: the tag and the action beside it are what the
   row's width is for, and a wrapped title pushes them around. */
.wp-session-title {
    color: var(--wp-color-text-primary);
    font: var(--wp-font-style-subheadline);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A tag sits against the right edge: the row is the flex container, the column beside it takes
   the slack, and the tag keeps its own width. */
.wp-session-info + .wp-status {
    flex: none;
}

.wp-session-meta {
    align-items: center;
    color: var(--wp-color-text-secondary);
    display: flex;
    font: var(--wp-font-style-footnote);
    gap: var(--wp-space-xs);
}

/* Live, as a colour rather than a word, beside the sentence that says it in words. */
.wp-session-dot {
    background: var(--wp-color-feedback-success);
    border-radius: var(--wp-radius-full);
    flex: none;
    height: 7px;
    width: 7px;
}

/* An address is read a character at a time, which a proportional face makes harder: the
   digits have to line up for two of them to be compared at a glance. */
.wp-session-ip {
    color: var(--wp-color-text-secondary);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11px;
    letter-spacing: -0.01em;
}

/* Destructive, small, and on the right. Not `.wp-button`: that is a 44px control with its
   own padding, and this sits inside a row that already has a height. The target is kept by
   the row around it, which is 44px of its own. */
.wp-session-revoke {
    background: transparent;
    border: 0;
    color: var(--wp-color-feedback-danger);
    cursor: pointer;
    flex: none;
    font: var(--wp-font-style-footnote);
    font-weight: 600;
    padding: var(--wp-space-sm) 0;
    white-space: nowrap;
}

.wp-session-revoke:focus-visible {
    border-radius: var(--wp-radius-outline);
    outline: 2px solid var(--wp-color-brand-primary);
    outline-offset: 2px;
}

/* "Close session" where there is room for it and "Close" where there is not: the row has a
   title to fit as well, and the verb alone is unambiguous beside it. */
@media (width <= 24rem) {
    .wp-session-revoke-rest {
        display: none;
    }
}

/* A session that has ended is a record and not a worry, so the whole group recedes. Stated
   on the list rather than per row, because it is true of every row in it. */
.wp-session-past .wp-list-row {
    color: var(--wp-color-text-secondary);
}

.wp-session-past .wp-session-title {
    color: var(--wp-color-text-secondary);
}
