  /* ================= design tokens ================= */
  :root{
    --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px; --sp-6:32px;
    --r-xs:9px; --r-sm:12px; --r-md:18px; --r-lg:26px;   /* --r-xs: tight radius for the 26px form-dot (--r-sm would over-round it) */
    --nav-h:66px;   /* bottom-nav height (excl. safe-area): pad6 + (icon22 + label) + pad6 */
    /* HOW TALL A LEAGUE TABLE MAY STAND BEFORE IT SCROLLS INSIDE ITSELF — owner ruling
       2026-08-13: "הטבלאות של הליגות יכולות לגדול לגלילה ענקית... צריך שכל טבלה יהיה לה
       גלילה פנימית". Viewport-relative (dvh, the --kbd-reserve precedent) so a phone shows
       a good column of rows while the podium, season control and h2h below stay one thumb
       away; the px arm keeps a tablet from turning "bounded" into "most of the screen".
       ~10-11 rows on the owner's devices. Consumed at the #d1gen42 seam only — every league
       table instance (global/group, singles/doubles, tennis/padel) is a repaint of
       #leagueTable inside that one container, so one token bounds them all. */
    --table-viewport:min(60dvh, 520px);
    --fs-body:16px; --fs-small:13px;
    --tap-min:48px;
    /* HOW MUCH OF THE SCREEN THE ANDROID KEYBOARD IS ASSUMED TO COVER — a declared BOUND, and
       the only value in this file that is not a measurement of anything the page can see.
       On Android the WebView is never resized for the IME (MainActivity declares
       adjustNothing, which is the white-screen fix — tests/android-webview-background.test.js
       carries the mechanism), and measured live in the real WebView with the keyboard up,
       innerHeight, visualViewport height/offsetTop/pageTop, the injected safe-area variables,
       env(keyboard-inset-height) and navigator.virtualKeyboard.boundingRect are ALL unchanged
       or zero, with not one resize event. The page therefore CANNOT learn the height, so this
       is sized from the device: the Samsung IME on the owner's Galaxy A50 covers 321 of 630
       CSS px, 51%, and a suggestion strip and an emoji row are a few points each. 60% covers
       them with margin. Over-reserving costs a shorter dialog while typing; under-reserving
       costs the button the player is trying to press, so it errs upward on purpose.
       When @capacitor/keyboard lands (owed on a native build, deliberately not added from a
       lane) this stops being a bound and becomes a reading. See
       tests/android-keyboard-fold.test.js. */
    --kbd-reserve:60dvh;
    --ease:cubic-bezier(.2,.8,.25,1);

    /* Stage 3.5e D1: off-scale values pulled out of index.html inline styles.
       Each holds the EXACT value that was inline — none of these were rounded to
       the --sp-1..6 scale, because rounding is an owner-visible spacing change on
       a phone and nothing in the suite can see spacing. Geometry only, so one
       shared block (not split per day/night theme). */
    --sp-2px:2px; --sp-5px:5px; --sp-6px:6px; --sp-10px:10px; --sp-14px:14px;
    --sp-18px:18px; --sp-22px:22px;
    --fs-12-5px:12.5px; --fs-13-5px:13.5px; --fs-14px:14px; --fs-15px:15px;
    --fs-18px:18px; --fs-19px:19px; --fs-20px:20px;
    --r-7px:7px; --r-999px:999px;

    /* ---- RQS-01 board scales, VALUE-NAMED. ----------------------------------------
       The board's spacing is 4 8 12 16 20 24 32 40 48 64 and its radii are 8 12 16 20 24.
       These are ADDED, never renumbered onto --sp-1..6: --sp-5 is 24px here and the board's
       fifth step is 20px, so re-pointing the index would move every --sp-5 site by 4px —
       and the comment ten lines above records this project's own rule that spacing is an
       owner-visible change on a phone and nothing in the suite can see it. New screens use
       these; the old scale is migrated on a commit that is accepted on the device. */
    --space-4:4px;  --space-8:8px;  --space-12:12px; --space-16:16px; --space-20:20px;
    --space-24:24px; --space-32:32px; --space-40:40px; --space-48:48px; --space-64:64px;
    --radius-8:8px; --radius-12:12px; --radius-16:16px; --radius-20:20px; --radius-24:24px;

    /* ---- the QR plate, AND IT IS DELIBERATELY THE SAME IN BOTH THEMES. ----------------
       Every other colour in this file is re-valued per theme; these two are not, and the
       reason is that the consumer is a CAMERA rather than an eye. The QR specification is
       dark modules on a light background, and while many decoders cope with an inverted
       code, "many" is not a property to ship a two-people-standing-on-a-court interaction
       on — the failure mode is a phone that simply never beeps, with nothing on either
       screen explaining why. So the code gets its own guaranteed-light plate in night as
       well as day, exactly like the neutral plot scrim the chart-ink rule requires.
       They live in :root rather than in the two theme blocks BECAUSE they must not
       diverge: a future re-valuation that gives night its own darker plate would silently
       stop cameras working, and the only way to see it would be to scan one.
       21.0:1 — the maximum the sRGB gamut allows, which is the right target when the
       reader is an image sensor in bad light rather than a person. */
    --qr-plate:#FFFFFF;
    --qr-ink:#000000;

    /* ---- RQS-01 type scale. -------------------------------------------------------
       THE FACE NOW SHIPS. Owner instruction 2026-08-08: download the fonts and use them.
       `fonts/heebo-hebrew.woff2` + `fonts/heebo-latin.woff2` are self-hosted, precached in
       sw.js and carried by the OTA bundle (assemble-www.js ships `fonts` as a whole DIR).
       Heebo is a VARIABLE font — one file per subset covers 100..900, which is why four
       weights cost two files and 42 KB rather than eight files. Google serves the same
       woff2 for every weight; that is not a mistake, it is the variable axis.
       Split by unicode-range so a Latin-only screen never fetches the Hebrew subset.

       "Heebo Mono" IS NOT A REAL FAMILY — Heebo ships no monospace cut, so the board's
       number face cannot be honoured literally. The intent behind it (digits that do not
       jump as a score changes) is delivered by `font-variant-numeric:tabular-nums`, which
       is what this project's own numeric rule already requires; `--font-num` therefore
       names Heebo and the tabular feature is applied at the use site.
       Anton stays the scoreboard digits face — it is a display cut, not a body font.
       ~~The FACE is a separate, later owner decision~~ — taken 2026-08-08. */
    --font-title:var(--font-body); --font-body:"Heebo", -apple-system, "SF Hebrew", "Segoe UI", Roboto, Arial, sans-serif;
    --font-num:var(--font-body);   /* + font-variant-numeric:tabular-nums at the use site */
    --fs-title-32:32px; --fs-title-28:28px; --fs-title-24:24px;
    --fs-body-20:20px;  --fs-body-18:18px;  --fs-body-16:16px;
    --fs-num-24:24px;   --fs-num-20:20px;   --fs-num-16:16px;  --fs-num-14:14px;

    /* ---- the league podium bar's edge. -------------------------------------------
       --pc was CONSUMED at .pbar::before with NO definition in any stylesheet, and shipped
       that way: league-groups.js writes it per element, so the podium renders and every
       other .pbar draws an invisible bar. Declared here so `var(--pc)` always resolves.
       This makes the podium legal; it does not bless writing a colour into a custom
       property from JS, which is still one of only two properties this app does that to. */
    --pc:var(--line-interactive);
    --size-24px:24px; --size-26px:26px; --size-30px:30px; --size-32px:32px;
    --size-38px:38px; --size-40px:40px; --size-42px:42px; --size-44px:44px;
    --size-46px:46px; --size-50px:50px; --size-52px:52px; --size-52vh:52vh;
    --size-64px:64px; --size-96px:96px; --size-110px:110px; --size-120px:120px;
    --size-130px:130px;
    --maxw-420px:420px; --maxw-430px:430px; --maxw-440px:440px; --maxw-90dvh:90dvh;
    --z-16:16;

    /* Theme-invariant colours pulled out of index.html inline styles. These do NOT
       split into [data-theme="day"]/[data-theme="night"] because they never varied
       by theme before this refactor and must not start varying now:
       - google-btn-*: Google's own Sign-in-button brand colours (white fill, near-
         black text, #dadce0 border) — fixed by Google's brand guidelines, not by
         Raqio's theme.
       - sheet-scrim / sheet-shadow-color: the iOS-install bottom-sheet backdrop and
         drop shadow are a constant black scrim in both themes (unlike --scrim, which
         is theme-tinted for a different surface). */
    --google-btn-bg:#fff; --google-btn-fg:#1f1f1f; --google-btn-border:#dadce0;
    --sheet-scrim:rgba(0,0,0,.55); --sheet-shadow-color:rgba(0,0,0,.45);

    /* ---- design isolation: the last colours that lived in component rules ----
       Every value below is the EXACT literal that was inline in a rule, moved here
       unchanged. The sweep is a refactor, so nothing may render differently: these stay
       theme-invariant because they WERE theme-invariant, and unifying the near-identical
       alphas (.28/.3 bevel, .5/.55/.58 scrim) would be a rendering change wearing a
       tidy-up's clothes. Named for ROLE, never for palette — a token called --blue-900
       makes the next reskin depend on the palette this one is replacing.

       Scrims — the darkness a modal layer dims the app with. Not a brand colour, which
       is why they do not split per theme: a scrim has to dim day AND night. */
    --scrim-overlay:rgba(0,0,0,.5);    /* full-screen overlay: statistics, clicker sheet */
    --scrim-match:rgba(0,0,0,.58);     /* in-match dialog — dims a live scoreboard hardest */
    /* Elevation — the ink a floating layer's drop shadow is drawn in. A shadow tuned to
       this navy looks wrong on another brand, and that is exactly how a reskin lands
       "almost right" and takes a day to diagnose. */
    --shadow-sheet-color:rgba(0,0,0,.35);   /* the clicker sheet's lip */
    --shadow-control-color:rgba(0,0,0,.3);  /* switch knob */
    --shadow-badge-color:rgba(0,0,0,.4);    /* podium medal */
    --track-shade:rgba(0,0,0,.12);          /* inset shading in a switch track */
    /* Bevel — the inset top-edge light that gives a filled control its lift. */
    --bevel-hi:rgba(255,255,255,.28);
    --bevel-hi-pressed:rgba(255,255,255,.2);
    --bevel-hi-strong:rgba(255,255,255,.35);
    --bevel-hi-soft:rgba(255,255,255,.3);
    /* The pole a token is mixed TOWARD to darken it. Reskinning to a light brand makes
       this the one line that has to change for every shaded gradient in the app. */
    --mix-darken:#000;
    /* The serving-player indicator. Whole-value tokens, following --acc-grad/--acc-glow:
       one reskin point instead of three stops and two shadow layers. It stays theme-
       invariant, and under RQS-01 that costs nothing: --accent is now ONE lime in both
       themes, so the reason this was held away from the brand ("pointing it at --accent
       would change how it renders in day") no longer applies. Re-valued to the board's
       lime here rather than left behind as the last of the old palette. */
    --serve-dot-grad:radial-gradient(circle at 32% 30%, #D4FF7A 0%, #A8FF00 45%, #86CC00 100%);
    --serve-dot-shadow:0 0 10px rgba(168,255,0,.55), inset 0 -1px 2px rgba(0,0,0,.25);

    /* ---- the share card ----
       The night-session broadcast palette that drawShareImage and drawRecapCard paint an
       exported PNG in. THEME-INVARIANT ON PURPOSE, and that is the whole reason these are
       here rather than in the two theme blocks: the image leaves the app, so its colours
       must not depend on whichever theme the person who OPENS it happens to be in. Reading
       them through tok() against a :root definition gives both things at once — the card
       stays identical in day and night (tests/canvas-token-ratchet.test.js proves that at
       runtime, by logging every colour the canvas assigns in each theme and comparing), and
       a reskin still reaches it, which a literal never allowed. */
    --share-bg-top:#10284E; --share-bg-mid:#0B1D3A; --share-bg-bottom:#06142B;
    --share-court-line:rgba(255,255,255,.07);
    --share-court-line-soft:rgba(246,251,242,.09);
    --share-beam-from:rgba(199,242,10,.16); --share-beam-to:rgba(199,242,10,0);
    --share-grain-light:#fff; --share-grain-dark:#000;
    --share-ball-hi:#D8FF4D; --share-ball-core:#C7F20A; --share-ball-shade:#A6D700;
    --share-ball-seam:rgba(255,255,255,.9);
    --share-fg:#F8FBFF; --share-muted:#8EA7C5; --share-accent:#C7F20A;
    --share-mark-ink:#06142B;
    --share-glow-strong:rgba(199,242,10,.45); --share-glow-soft:rgba(199,242,10,.35);
    --share-chip-fill:rgba(199,242,10,.12);
    --share-footer:rgba(246,251,242,.5);
    --share-stat-label:#7fa78f;

    /* ---- league podium ----
       Silver and bronze are metallic DECORATIVE fills; every name, rating and rank numeral
       on the podium sits on the guaranteed-contrast --fg in both themes, and none of that
       changes here. They are theme-invariant because a medal is a medal in both themes —
       which is exactly why they can be tokenised with no per-theme contrast risk. */
    --mix-lighten:#fff;                      /* the pole a token is mixed toward to lighten */
    --medal-champion-ink:#12210a;
    --medal-silver-fill-strong:rgba(212,222,236,.28);
    --medal-silver-fill-faint:rgba(212,222,236,.05);
    --medal-silver-rim:#c6d1e0; --medal-silver-hi:#f3f7fc; --medal-silver-shade:#bcc7d6;
    --medal-silver-ink:#1b2536;
    --medal-bronze-fill-strong:rgba(233,164,98,.28);
    --medal-bronze-fill-faint:rgba(233,164,98,.05);
    --medal-bronze-rim:#e3a869; --medal-bronze-hi:#f4bf88; --medal-bronze-shade:#cd7f43;
    --medal-bronze-ink:#2a1608;
  }
  html[data-sport="tennis"]{
    --sport-accent:var(--accent);
    --sport-accent-ink:var(--accent-ink);
    --sport-tint:var(--tint);
    --sport-edge:var(--accent-edge);
    /* The court lines behind an empty state. 24% was the old palette's strength; under
       RQS-01 the board's --muted (#97A3B5) reads 3.38:1 on an --accent stripe at 24% over
       --surface, and scripts/a11y.js found it on the league route in both viewports. 12% is
       the SAME budget --tint is solved at, and it is the only accent-over-surface strength
       in this stylesheet for that reason: one accent wash, one meaning. --muted 4.89:1. */
    --sport-line-image:
      linear-gradient(90deg, transparent 0 23%, color-mix(in srgb, var(--accent) 12%, transparent) 23% 24%, transparent 24% 76%, color-mix(in srgb, var(--accent) 12%, transparent) 76% 77%, transparent 77%);
    --sport-card-radius:var(--r-md);
  }
  html[data-sport="padel"]{
    --sport-accent:var(--clay);
    --sport-accent-ink:var(--clay-ink);
    --sport-tint:var(--tint-clay);
    --sport-edge:var(--clay-edge);
    --sport-line-image:
      repeating-linear-gradient(90deg, transparent 0 17px, color-mix(in srgb, var(--clay) 16%, transparent) 17px 18px),
      repeating-linear-gradient(0deg, transparent 0 13px, color-mix(in srgb, var(--clay) 13%, transparent) 13px 14px);
    --sport-card-radius:var(--r-sm);
  }
  /* ===================== RQS-01 — the 2026 reskin =====================
     Board: Raqio_Latest10_Package/mockups/01_מערכת_עיצוב_2026.png.
     The BOARD wins over the package handoff MD (owner ruling 3, 2026-08-07), and both
     modes are mandatory (ruling 4) — the board draws DARK ONLY, so `day` below is a
     DERIVATION, not a transcription. Every ratio in these comments was computed with
     scripts/a11y.js's own channel/luminance/contrast, and tests/rqs01-tokens.test.js
     recomputes all of them on every run: no number here is asserted.

     THREE THINGS THIS PALETTE DOES DIFFERENTLY FROM THE PROPOSAL, each because the
     proposal's own value was measured and failed:

     1. `--graph` IS NOT RETIRED into `--accent-ink`. It is a FILL that carries
        `--on-accent` text at `.pDot.w`; folding a fill into a 4.5:1 text ink put
        **2.93:1** on that chip on day. A fill and an ink are different guarantees —
        app.js:6571 says exactly that — so `--graph` stays, re-valued per theme, and it
        keeps `.completion .cbar > i`'s gradient a gradient instead of one flat colour.
     2. `--muted` IS SOLVED AGAINST THE PAINT, NOT THE TOKENS. Four component rules mixed
        their own accent wash (16/18/20/22%), so no token named those containers and no
        solve saw them; `--muted` measured 3.60-4.34:1 there. They now use `--tint`, which
        already owns that role and IS solved: night `--tint` is `--accent` at 12% over
        `--surface`, and 12% is exactly the budget `--muted` #97A3B5 has (4.89:1).
     3. THE BRAND KEEPS ONE VALUE AND GAINS AN EDGE. `--accent` and `--clay` are the
        board's #A8FF00 / #00CFFF in BOTH themes. On white they are 1.24:1 and 1.85:1 —
        perfect as fills under a paired dark ink, illegible as the bare border, dot and
        ring the stylesheet also draws them as, including `.panel.serving`, the serve
        indicator. So `--accent-edge` / `--clay-edge` carry the 3:1 the fill cannot, and
        the brand does not have to be diluted on half the product to get it.

     `--line-interactive` is the token the board is missing and §8.4 named: `--line`
     #223048 on #0E1626 is **1.27:1**, which is right for a decorative rule and forbidden
     as the ONLY boundary of a control, where WCAG 2.2 §1.4.11 requires 3:1.
     ==================================================================== */
  [data-theme="night"]{
    /* dropdown chevron, drawn from the theme's own ink so it can never be a dark arrow
       on a dark surface (which is what the native `appearance:auto` arrow did).
       stroke = --muted: 6.60:1 on --surface, well past the 3:1 a control indicator needs. */
    --select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2397A3B5' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 10l5 5 5-5'/%3E%3C/svg%3E");

    /* ---- surfaces. The board gives bg and surface; the ladder above them is derived at
       the same OKLCH hue (263.4°) and the same +3 step the board itself uses. */
    --bg:#0E1626;                    /* board */
    --bg-grad:
      radial-gradient(90% 55% at 18% -8%, rgba(168,255,0,.05) 0%, rgba(168,255,0,0) 60%),
      radial-gradient(90% 55% at 82% -8%, rgba(0,207,255,.045) 0%, rgba(0,207,255,0) 60%),
      radial-gradient(150% 120% at 50% 0%, #16223B 0%, #101A2C 50%, #0E1626 100%);
      /* every stop's luminance is 0.008-0.017; the darkest ink that lands on this (--muted,
         0.361) is OUTSIDE that band in one direction, so a11y.js's `straddles` 1:1 penalty
         cannot fire here. Worst ink-over-gradient 6.20:1. */
    --surface:#141D2F;               /* board */
    --surface-2:#1B2537;
    --sub:#202A3E;                   /* the recessed chip reads ABOVE the card on dark */

    /* ---- ink. The board gives TWO tiers, and the contrast budget proves a third is not
       available: --muted's own worst container is 4.89:1, so anything dimmer fails AA.
       --faint therefore holds --muted's value rather than inventing a tier that cannot
       clear the floor. Its NAME survives only because admin.html still reads it. */
    --fg:#EBEDF5;                    /* board */
    --muted:#97A3B5;                 /* board — worst surface 4.89:1 */
    --faint:#97A3B5;

    /* ---- boundaries. THREE roles, and the middle one is what §8.4 asked for. */
    --line:#223048;                  /* board. DECORATIVE ONLY — 1.27:1 on --surface, by design */
    --line-interactive:#7A869D;      /* a control edge: 3.41:1 worst, 4.59:1 on --surface */
    --line-soft:rgba(235,237,245,.08);
    --chalk-strong:#7A869D;          /* the ad-hoc "stronger line"; --line-interactive is that role */
    --hair:rgba(255,255,255,.14);    /* a bevel HIGHLIGHT, white in both themes — see day */

    /* ---- accent. One lime in both themes; what changes is the ink and the edge. */
    --accent:#A8FF00;                /* board */
    --on-accent:#0E1626;             /* 14.61:1 on --accent, 14.61:1 on --graph */
    --accent-ink:#C4FF4D;            /* text: 10.57:1 worst. Lighter than --accent, as day is darker */
    --accent-edge:#A8FF00;           /* graphical: 10.10:1 worst — on dark the brand clears 3:1 unaided */
    --graph:#A8FF00;                 /* FILL for chart marks and the win dot; carries --on-accent */
    --tint:#263829;                  /* OPAQUE: --accent at 12% over --surface. --muted 4.89:1 */
    --acc-grad:linear-gradient(160deg,#C4FF4D,#A8FF00 55%,#86CC00);
    --acc-glow:0 0 18px rgba(168,255,0,.22);
    --glow:0 0 14px rgba(168,255,0,.08);
    --ring:#A8FF00;                  /* focus ring: 10.10:1 worst */
    --digit:#C4FF4D;  --digit-glow:0 0 18px rgba(168,255,0,.14);

    /* ---- padel. Cyan re-values --clay, so html[data-sport="padel"] re-skins through its
       existing aliases untouched. --clay-ink stays DISTINCT from --clay: where they were
       equal, every inset ring drawn as color-mix(--clay-ink …) on a --clay fill vanished. */
    --clay:#00CFFF;                  /* board */
    --on-clay:#0E1626;               /* 9.79:1 */
    --clay-ink:#7FE3FF;              /* text: 8.53:1 worst */
    --clay-edge:#00CFFF;             /* graphical: 6.77:1 worst */
    --tint-clay:#16303F;             /* OPAQUE: --muted 5.38:1 */

    /* ---- status. The board's five states, each a triple: a 3:1 dot, an opaque fill, and
       the 4.5:1 ink that fill is solved against (Material-3 pairing, the house rule). */
    --status-approved:#9CD71E;  --status-approved-fill:#243120;  --status-approved-on:#A9E132;
    --status-pending:#F5A51B;   --status-pending-fill:#332A1C;   --status-pending-on:#F7B443;
    --status-disputed:#E95B5D;  --status-disputed-fill:#33222A;  --status-disputed-on:#F08A86;
    --status-expired:#B1B7C7;   --status-expired-fill:#232C3E;   --status-expired-on:#B8BECD;
    --status-cancelled:#7A869D; --status-cancelled-fill:#1C2434; --status-cancelled-on:#98A3B8;
      /* the board's cancelled dot #667187 is 2.4:1 on a container; the value that fixes it is
         --line-interactive's, carried here rather than aliased so the two can diverge later. */

    /* ---- refusal. A full-width refusal notice stays SATURATED: #liveSportMismatch and
       #writeNotice[data-tone="refused"] exist to be unmissable, and demoting them to the
       pale pending chip would keep their suites green while removing the thing they are
       for. Same amber as --status-pending, a different container weight. */
    --warn-surface:#F5A51B;  --warn-ink:#2A1B00;      /* 8.19:1 */
    --danger:#F08A86;                /* destructive ink: 6.97:1 on --surface, 5.93:1 on --tint-danger */
    --tint-danger:#3A2429;           /* OPAQUE — replaces color-mix(--danger 15%, transparent) */

    --sel-bg:#A8FF00; --sel-fg:#0E1626;   /* 14.61:1 */
    --kudos-flame:#E07A45;           /* a warm flame, not the accent green; 4.19:1 worst */

    /* ---- elevation. The board's "צל רך" is rgba(0,0,0,.35). */
    --shadow-sm:0 1px 4px rgba(0,0,0,.25), 0 10px 26px rgba(0,0,0,.35);
    --shadow-md:0 2px 10px rgba(0,0,0,.35), 0 18px 50px rgba(0,0,0,.5);
    --glass:linear-gradient(165deg, rgba(255,255,255,.045), rgba(255,255,255,0) 42%);
    --scrim:rgba(14,22,38,.82);
    --grain-op:.026;
  }
  [data-theme="day"]{
    --select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235A6376' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 10l5 5 5-5'/%3E%3C/svg%3E");

    /* ---- surfaces. DERIVED: same rung COUNT and same direction of elevation as night
       (the raised card is nearer the light), but the step size is set by the ink budget
       rather than copied — a light theme has less ink headroom, and copying night's step
       failed two dozen pairs. Only the recessed chip flips below the card, which is what
       the shipped day theme already did. */
    --bg:#F3F5FA;
    --bg-grad:
      radial-gradient(90% 55% at 18% -8%, rgba(168,255,0,.08) 0%, rgba(168,255,0,0) 60%),
      radial-gradient(90% 55% at 82% -8%, rgba(0,207,255,.05) 0%, rgba(0,207,255,0) 60%),
      linear-gradient(180deg, #F9FAFD 0%, #F5F7FB 50%, #F3F5FA 100%);
    --surface:#FFFFFF;               /* board "לבן" */
    --surface-2:#F9FAFD;
    --sub:#EDF0F6;

    --fg:#0A111F;                    /* the board's neutral hue, taken to the dark end */
    --muted:#5A6376;                 /* solved: 5.09:1 worst */
    --faint:#5A6376;

    --line:#D8DDE7;                  /* mirrors the dark border's RATIO (1.36:1), not its lightness */
    --line-interactive:#6F7889;      /* 3.75:1 worst, 4.45:1 on --surface */
    --line-soft:rgba(10,17,31,.09);
    --chalk-strong:#6F7889;
    --hair:rgba(255,255,255,.85);    /* still a WHITE bevel: this is an inset top-edge LIGHT.
                                        Retiring it into --line-soft would have inverted its
                                        polarity on day — a 5px highlight becoming a shadow —
                                        and nothing in the suite measures a bevel. */

    --accent:#A8FF00;                /* identical to night: one brand value */
    --on-accent:#0E1626;             /* 14.61:1 — the dark ink on lime in BOTH themes */
    --accent-ink:#456F00;            /* text: 5.03:1 worst */
    --accent-edge:#5C8B00;           /* graphical: 3.44:1 worst. This is what makes the
                                        brand survivable on light — --accent is 1.24:1 here. */
    --graph:#658F00;                 /* FILL: carries --on-accent at 4.72:1 AND clears 3:1
                                        as a chart mark. Both constraints, one narrow window. */
    --tint:#F1FFD6;                  /* OPAQUE */
    --acc-grad:linear-gradient(160deg,#C4FF4D,#A8FF00 55%,#86CC00);
    --acc-glow:0 0 14px rgba(168,255,0,.12);
    --glow:0 0 0 0 rgba(0,0,0,0);
    --ring:#5C8B00;                  /* 3.44:1 worst */
    --digit:#456F00;  --digit-glow:none;

    --clay:#00CFFF;  --on-clay:#0E1626;   /* 9.79:1 */
    --clay-ink:#00637F;              /* text: 5.74:1 worst */
    --clay-edge:#00849F;             /* graphical: 3.69:1 worst — --clay is 1.85:1 here */
    --tint-clay:#DFF7FF;

    --status-approved:#4B7400;  --status-approved-fill:#EFF9DC;  --status-approved-on:#3C5C00;
    --status-pending:#8F5400;   --status-pending-fill:#FDF2DF;   --status-pending-on:#7E4900;
    --status-disputed:#BC3A3D;  --status-disputed-fill:#FDEAEC;  --status-disputed-on:#9C2C33;
    --status-expired:#666C7E;   --status-expired-fill:#F2F4F7;   --status-expired-on:#575D6F;
    --status-cancelled:#6F7889; --status-cancelled-fill:#E9ECF1; --status-cancelled-on:#525A6D;

    --warn-surface:#F5A51B;  --warn-ink:#2A1B00;      /* the same saturated refusal amber */
    --danger:#A02F3C;                /* 7.08:1 on --surface, 6.21:1 on --tint-danger */
    --tint-danger:#FDECEE;

    --sel-bg:#0E1626; --sel-fg:#EBEDF5;   /* 15.47:1 */
    --kudos-flame:#B84E1C;           /* 4.28:1 worst */

    --shadow-sm:0 1px 3px rgba(10,17,31,.05), 0 8px 20px rgba(10,17,31,.10);
    --shadow-md:0 1px 4px rgba(10,17,31,.07), 0 16px 36px rgba(10,17,31,.16);
    --glass:linear-gradient(165deg, rgba(255,255,255,.7), rgba(255,255,255,0) 45%);
    --scrim:rgba(243,245,250,.85);
    --grain-op:.02;
  }

  /* dedicated scoreboard digits face, self-hosted, precached for offline */
  @font-face{
    font-family:'Anton';
    src:url('fonts/anton.woff2') format('woff2');
    font-weight:400; font-style:normal; font-display:swap;
  }
  /* RQS-01's family, self-hosted for the same reason Anton is: the app runs offline and
     inside a native WebView, where a CDN is a dependency that fails silently on a court
     with no signal. `font-weight:100 900` declares the VARIABLE axis — one file answers
     every weight the scale asks for. `font-display:swap` so Hebrew text is never invisible
     while 12 KB arrives; the system stack behind it in --font-body is what paints first. */
  @font-face{
    font-family:'Heebo';
    src:url('fonts/heebo-hebrew.woff2') format('woff2');
    unicode-range:U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
    font-weight:100 900; font-style:normal; font-display:swap;
  }
  @font-face{
    font-family:'Heebo';
    src:url('fonts/heebo-latin.woff2') format('woff2');
    unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
      U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
      U+FEFF, U+FFFD;
    font-weight:100 900; font-style:normal; font-display:swap;
  }
  *{ box-sizing:border-box; margin:0; padding:0; -webkit-tap-highlight-color:transparent; }
  .hidden{ display:none!important; }
  .srOnly{
    position:absolute!important; width:1px!important; height:1px!important;
    padding:0!important; margin:-1px!important; overflow:hidden!important;
    clip:rect(0,0,0,0)!important; white-space:nowrap!important; border:0!important;
  }
  html,body{ height:100%; }
  body{
    background:var(--bg); background-image:var(--bg-grad); background-attachment:fixed;
    color:var(--fg);
    font:var(--fs-body)/1.5 -apple-system, "Segoe UI", Roboto, "Heebo", Arial, sans-serif;
    overscroll-behavior:none; user-select:none; -webkit-user-select:none;
    touch-action:manipulation;
    transition:background-color .3s, color .3s;
  }
  .num{ direction:ltr; unicode-bidi:isolate; font-variant-numeric:tabular-nums; white-space:nowrap; }

  .screen{ display:none; min-height:100dvh; }
  .screen.active{ display:flex; flex-direction:column; animation:screenIn .3s var(--ease); }
  @keyframes screenIn{ from{ opacity:0; transform:translateY(8px); } }

  button{
    font:inherit; color:var(--fg); background:var(--surface);
    border:1.5px solid var(--line-soft); border-radius:var(--r-sm);
    min-height:var(--tap-min); padding:var(--sp-2) var(--sp-4); cursor:pointer;
    box-shadow:var(--shadow-sm);
    transition:transform .12s var(--ease), box-shadow .2s, background-color .2s, border-color .2s;
  }
  button:active{ transform:scale(.965); }
  fieldset button:not(.primary):not(.chip):not(.histDel):not(.switch):not([aria-pressed]){ background:var(--sub); border-color:transparent; }
  button:disabled{ opacity:.35; cursor:default; box-shadow:none; }
  .appCard{
    background-color:var(--surface);
    border:1px solid var(--line-soft);
    border-radius:var(--sport-card-radius, var(--r-md));
    box-shadow:var(--shadow-sm);
    padding:var(--sp-4);
  }
  .appBadge{
    display:inline-flex; align-items:center; justify-content:center; gap:6px;
    min-height:32px; max-width:100%; padding:5px 10px;
    border:1px solid var(--sport-edge, var(--accent-edge));
    border-radius:999px; background:var(--sport-tint, var(--tint));
    color:var(--sport-accent-ink, var(--accent-ink)); font-size:13px; font-weight:800;
  }
  .appButton{
    min-height:var(--tap-min); display:inline-flex; align-items:center;
    justify-content:center; gap:8px; font-weight:750;
  }
  .uiState{
    display:grid; grid-template-columns:auto minmax(0, 1fr);
    align-items:center; gap:var(--sp-3); margin-block:var(--sp-3);
    background-image:var(--sport-line-image);
    background-blend-mode:soft-light;
  }
  .uiStateIcon{
    width:38px; height:38px; padding:8px; border-radius:50%;
    color:var(--sport-accent-ink); background:var(--sport-tint);
  }
  .uiStateCopy{ min-width:0; }
  .uiStateCopy h3{ margin:0; font-size:15px; line-height:1.35; }
  .uiStateCopy p{ margin:3px 0 0; color:var(--muted); font-size:13px; line-height:1.5; }
  .uiStateActions{
    grid-column:1 / -1; display:flex; flex-wrap:wrap; gap:var(--sp-2);
  }
  .uiStateActions .appButton{ flex:1; min-width:min(150px, 100%); }
  .uiState-error .uiStateIcon{ color:var(--danger); background:color-mix(in srgb, var(--danger) 12%, var(--surface)); }
  .uiState-offline .uiStateIcon{ color:var(--clay-ink); background:var(--tint-clay); }
  .uiStateCompact{
    display:flex; padding:8px 10px; border-radius:999px; box-shadow:none;
    background-image:none;
  }
  .uiStateCompact .uiStateIcon{ width:26px; height:26px; padding:5px; }
  .uiStateCompact .uiStateCopy h3{ font-size:12.5px; }
  .uiStateCompact .uiStateCopy p,
  .uiStateCompact .uiStateActions{ display:none; }
  .uiStateSkeleton{
    grid-column:1 / -1; display:grid; gap:7px; width:100%;
  }
  .uiStateSkeleton span{
    display:block; height:10px; border-radius:999px;
    background:linear-gradient(90deg, var(--sub), var(--sport-tint), var(--sub));
    background-size:200% 100%; animation:uiStateShimmer 1.4s linear infinite;
  }
  .uiStateSkeleton span:nth-child(2){ width:78%; }
  .uiStateSkeleton span:nth-child(3){ width:54%; }
  @keyframes uiStateShimmer{ to{ background-position:-200% 0; } }
  .appOverlay[hidden]{ display:none !important; }
  #iosGuideWrap.show{ display:flex; }
  button.primary{
    background:linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 84%, var(--mix-darken) 16%));
    color:var(--on-accent); border:1px solid var(--accent-edge); font-weight:800;
    box-shadow:var(--shadow-md);
  }
  button:focus-visible{ outline:3px solid var(--ring); outline-offset:2px; }
  /* Destructive account action: --danger clears AA only on the opaque surface,
     not the generic fieldset --sub button fill (verified in both themes). */
  #acctDeleteBtn{
    background:var(--surface);
    color:var(--danger);
    border-color:color-mix(in srgb, var(--danger) 55%, transparent);
    font-weight:700;
  }
  .accountActionHelp{ margin:var(--sp-1) 0 var(--sp-2); }
  .accountActionStatus{ min-height:1.45em; margin-top:var(--sp-1); }
  .accountActionStatus:empty{ min-height:0; margin-top:0; }
  .accountDangerZone{
    margin-top:var(--sp-4);
    padding-top:var(--sp-4);
    border-top:1px solid var(--line-soft);
  }
  .accountDangerTitle{ color:var(--danger); }
  .accountDeleteStatus{ margin-inline:var(--sp-4); }

  /* ================= setup ================= */
  #setup{ padding:max(var(--sp-5), env(safe-area-inset-top)) var(--sp-4) calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--sp-4)); gap:var(--sp-4); max-width:520px; margin:0 auto; width:100%; }   /* bottom pad clears the fixed #bottomNav */
  .hero{ display:flex; align-items:center; justify-content:space-between; }
  .hero h1{ font-size:32px; font-weight:900; letter-spacing:-.02em; line-height:1.15; }
  .hero .sub{ color:var(--muted); font-size:var(--fs-small); margin-top:2px; }
  .iconBtn{ width:48px; min-height:48px; padding:0; border-radius:14px; font-size:20px; display:flex; align-items:center; justify-content:center; }
  #appMain{ display:flex; flex-direction:column; gap:var(--sp-4); min-width:0; }
  .sportContextSwitch{
    position:relative; isolation:isolate; overflow:hidden;
    display:grid; grid-template-columns:1fr 1fr; gap:4px; padding:4px;
    background:var(--sub); border:1px solid var(--line-soft); border-radius:var(--r-sm);
  }
  .sportContextBadge::before{
    content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
    background-image:var(--sport-line-image); opacity:.72;
  }
  .sportContextSwitch button{
    display:flex; align-items:center; justify-content:center; gap:7px;
    min-height:44px; padding:6px 10px; border:0; box-shadow:none; background:transparent;
    font-size:14px; font-weight:750;
  }
  .sportContextSwitch button[aria-pressed="true"]{
    background:var(--surface); color:var(--sport-accent-ink); box-shadow:var(--shadow-sm);
    border:1px solid var(--sport-edge);
  }
  .sportInheritedNote{
    margin:0; padding:9px 11px; border-radius:var(--r-sm);
    background:var(--sub); color:var(--muted); line-height:1.45;
  }
  .stage3Setup{ position:relative; overflow:hidden; }
  .stage3ChoiceGrid{
    display:grid; grid-template-columns:1fr; gap:var(--sp-2);
  }
  .stage3ChoiceGrid > button{
    width:100%; min-height:64px; display:flex; align-items:center; gap:var(--sp-3);
    text-align:start; padding:10px 12px; background:var(--sub); color:var(--fg);
    border:1px solid var(--line-soft); box-shadow:none;
  }
  .stage3ChoiceGrid > button .ic{ flex:0 0 auto; color:var(--sport-accent-ink); }
  .stage3ChoiceGrid > button span{ display:flex; min-width:0; flex-direction:column; gap:2px; }
  .stage3ChoiceGrid > button b{ font-size:14px; }
  .stage3ChoiceGrid > button small{ color:var(--muted); font-size:12px; line-height:1.35; }
  .stage3ChoiceGrid > button[aria-pressed="true"]{
    background:var(--tint); border-color:var(--accent-ink);
    box-shadow:inset 0 0 0 1px var(--accent-ink);
  }
  .stage3ContextNote{
    padding:10px 12px; border-radius:var(--r-sm); background:var(--sub);
    color:var(--muted); font-size:12.5px; line-height:1.5;
    border-inline-start:3px solid var(--sport-accent);
  }
  .stage3PresetCards{
    display:grid; grid-template-columns:1fr; gap:var(--sp-2);
  }
  /* An author `display` rule outranks the browser's own [hidden]{display:none}, so
     setting .hidden in JS did nothing here: choosing Custom hid nothing and the ready-made
     formats stayed on screen underneath the custom panel, contradicting the choice just
     made. Any element whose class sets display needs this pairing. */
  .stage3PresetCards[hidden]{ display:none; }
  .stage3PresetCards > button{
    min-height:62px; padding:10px 12px; display:grid;
    grid-template-columns:1fr auto; gap:4px 10px; text-align:start;
    background:var(--sub); color:var(--fg); border:1px solid var(--line-soft);
    box-shadow:none;
  }
  .stage3PresetCards > button b{ font-size:13.5px; }
  .stage3PresetCards > button span{ color:var(--muted); font-size:12px; }
  .stage3PresetCards > button .stage3PresetCheck{
    grid-row:1 / span 2; grid-column:2; align-self:center;
    width:24px; height:24px; display:grid; place-items:center;
    border-radius:50%; border:1.5px solid var(--line-soft); color:transparent;
  }
  .stage3PresetCards > button[aria-checked="true"]{
    border-color:var(--accent-ink); background:var(--tint);
  }
  .stage3PresetCards > button[aria-checked="true"] .stage3PresetCheck{
    color:var(--on-accent); background:var(--accent); border-color:var(--accent);
  }
  .stage3CustomRules{
    border:1px solid var(--line-soft); border-radius:var(--r-sm); background:var(--sub);
  }
  .stage3CustomRules > summary{
    min-height:var(--tap-min); display:flex; align-items:center;
    padding:8px 12px; cursor:pointer; font-weight:750; color:var(--fg);
  }
  .stage3CustomRules > summary::-webkit-details-marker{ display:none; }
  .stage3CustomRules > summary::after{
    content:"›"; margin-inline-start:auto; color:var(--muted);
    transform:rotate(90deg); transition:transform .2s var(--ease);
  }
  .stage3CustomRules[open] > summary::after{ transform:rotate(-90deg); }
  .stage3CustomBody{
    display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-3);
    padding:0 12px 12px; border-top:1px solid var(--line-soft);
  }
  /* Labels wrap to different line counts ("יעד טאי־ברייק" vs "טאי־ברייק בשוויון
     משחקונים"), which used to push one control 20px below its neighbour in the same
     grid row. Stretch each field and pin the control to the bottom so controls line up
     regardless of how many lines their label takes. */
  .stage3CustomBody .field{
    padding-top:var(--sp-3); align-self:stretch;
    justify-content:space-between;
  }
  .stage3CustomBody .field > select,
  .stage3CustomBody .field > input{ margin-top:auto; }
  .stage3CustomBody select{
    /* background-COLOR, not the `background` shorthand: the shorthand resets
       background-image and silently erased the shared chevron, leaving the browser's
       native theme-blind arrow. */
    width:100%; min-height:var(--tap-min); font:inherit; color:var(--fg);
    background-color:var(--surface); border:1px solid var(--line-soft);
    border-radius:var(--r-sm); padding:8px; cursor:pointer;
  }
  .stage3CustomBody input[type="number"]{
    width:100%; min-height:var(--tap-min); color:var(--fg);
    background:var(--surface); border:1.5px solid var(--chalk-strong);
    border-radius:var(--r-sm); padding:8px 12px; cursor:text;
  }
  .stage3CustomBody select{
    border-color:var(--chalk-strong);
  }
  .stage3RulesSummary{
    padding:var(--sp-3); border-radius:var(--r-sm);
    background:var(--sub); border:1px solid var(--line-soft);
  }
  .stage3RulesSummary h2{ font-size:15px; margin:0 0 7px; }
  /* LONGHAND, and not as a matter of taste. `font:600 12.5px/1.7 inherit` parses as nothing:
     `inherit` is legal as the WHOLE value of the `font` shorthand and never as its family, so
     the declaration is invalid and the browser drops all four values — the parsed rule has no
     `font` in it at all. This node is a <pre>, so what survived was the UA stylesheet's
     `monospace`, i.e. Hebrew in Courier at 16px/400. The owner's device sweep found it as the
     "כך נשחק" card. tests/rules-card-font.test.js reads the COMPUTED values, because the source
     text looks correct and only the computed value is wrong. styles.css is unowned; these are
     the two lines. (The sixteen other `font:inherit` sites in this file are the bare form and
     are valid — only these two carried extra values.) */
  .stage3RulesSummary pre{
    margin:0; white-space:pre-wrap; color:var(--fg);
    font-family:inherit; font-weight:600; font-size:12.5px; line-height:1.7;
  }
  #stage3InviteSend{ margin-top:8px; width:100%; }
  #stage3ConsentMissing{ margin-top:8px; color:var(--danger); font-weight:700; }
  .stage3InvitePanel{ display:flex; flex-direction:column; gap:8px; margin-top:8px; }
  /* author display:flex/inline-flex must not defeat the hidden attribute.
     #stage3ExtendWindowBtn joined this list on 2026-08-02: .appButton sets
     display:inline-flex, which outranks the browser's own [hidden] rule, so the button was
     drawn in EVERY invite state — including the ones where the window is closed and
     tapping it can only answer that it cannot be extended.
     #reportResultSubmit joined it likewise (defect A): the same .appButton rule kept the
     SPENT submit button drawing after a report was filed or the daily right used up
     (submit.hidden = true, result-claim.js). styles.css is unowned; this is the one line. */
  #stage3InviteSend[hidden], #stage3InviteFriendlyBtn[hidden],
  #stage3ExtendWindowBtn[hidden], .stage3InvitePanel[hidden],
  #reportResultSubmit[hidden]{ display:none; }
  .stage3InviteSeats{ display:flex; flex-direction:column; gap:6px; list-style:none; }
  .stage3InviteSeat{
    display:flex; align-items:center; justify-content:space-between; gap:8px;
    min-height:44px; padding:8px 10px; border-radius:9px;
    background:var(--surface); color:var(--fg);
  }
  .stage3InviteName{
    font-weight:700; font-size:13.5px; min-width:0; overflow-wrap:anywhere;
  }
  .stage3InviteStatus{
    display:inline-flex; align-items:center; gap:5px; flex:0 0 auto;
    font-size:12px; font-weight:800; padding:4px 10px; border-radius:999px;
    white-space:nowrap; background:var(--sub); color:var(--muted);
  }
  .stage3InviteSeat[data-status="accepted"] .stage3InviteStatus{
    background:var(--tint); color:var(--accent-ink);
  }
  .stage3InviteSeat[data-status="declined"] .stage3InviteStatus{
    background:var(--tint-danger); color:var(--danger);
  }
  #stage3InviteFriendlyBtn, #stage3ExtendWindowBtn{ width:100%; }
  #stage3RulesModeField[hidden]{ display:none !important; }
  #stage3SetupStatus[data-kind="error"]{ color:var(--danger); font-weight:700; }
  #stage3SetupStatus[data-kind="ok"]{ color:var(--accent-ink); font-weight:700; }

  /* THE RATED-FLOOR NOTICE. Owner ruling 2026-08-09 — the player is told the minimum WHILE
     they choose, not after they file. Both states are drawn, never one: a notice that only
     appears when something is wrong makes "silent" and "fine" look identical, which is the
     whole defect.

     TOKENS ONLY, and the two it uses are the pair this panel already trusts three lines
     above. --accent-ink is 10.57:1 on night and 5.03:1 on day; --muted is 4.89:1 and 5.09:1.
     Both clear AA body text (4.5:1) in BOTH themes against --sub, which is the surface
     .stage3Setup actually paints. Deliberately NOT --danger: a format that does not carry a
     rating is a fact about the format, not an error the player made, and colouring it as one
     would push people off a format they chose on purpose. */
  .stage3RatingNote{
    margin:8px 0 0; padding:8px 10px; border-radius:var(--r-sm);
    background:var(--sub); line-height:1.5;
    border-inline-start:3px solid var(--line-soft);
  }
  /* ONE PAIR OF INKS FOR ONE RULE, on both screens that state it. #reportResultRulesetRating
     is the report screen's copy of this sentence (RaqioStage3Setup.rulesetFloorNotice writes
     both), and it is named here rather than given its own colours: a second pair is a second
     thing to hold at AA in two themes, and the day one moves is the day they disagree. The
     border marker is the setup panel's alone — the report screen's note has no left rule. */
  #stage3RulesRating[data-kind="rated"]{
    color:var(--accent-ink); font-weight:700; border-inline-start-color:var(--accent-ink);
  }
  #stage3RulesRating[data-kind="unrated"]{
    color:var(--muted); font-weight:600; border-inline-start-color:var(--status-pending);
  }
  #reportResultRulesetRating[data-kind="rated"]{ color:var(--accent-ink); font-weight:700; }
  #reportResultRulesetRating[data-kind="unrated"]{ color:var(--muted); font-weight:600; }
  /* An author `display` rule outranks the UA's [hidden]{display:none}; this block sets none,
     but the paired rule is kept so a later layout change here cannot resurrect that bug
     (the same note styles.css:559-564 carries for .stage3PresetCards). */
  .stage3RatingNote[hidden]{ display:none; }

  /* Task 18: the invitee's confirmation dialog. Reuses .stage3Dialog / .stage3DecisionActions
     / .stage3InviteSeats (task 17) verbatim for the shared chrome and the participant list,
     so both sides of ranked consent look and read identically in both themes. */
  .rankedInviteDialog h2{
    display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2);
  }
  /* Longhand for the same reason as .stage3RulesSummary pre above — the invalid `font`
     shorthand dropped all four values and left this <pre> on the UA's monospace. */
  #rankedInviteRules{
    margin:var(--sp-3) 0; padding:var(--sp-3); border-radius:var(--r-sm);
    background:var(--sub); border:1px solid var(--line-soft);
    white-space:pre-wrap; color:var(--fg);
    font-family:inherit; font-weight:600; font-size:12.5px; line-height:1.7;
  }
  /* --danger clears AA only on the opaque --surface fill, matching #acctDeleteBtn. */
  #rankedInviteDecline{
    background:var(--surface); color:var(--danger);
    border:1.5px solid color-mix(in srgb, var(--danger) 55%, transparent); font-weight:800;
  }

  #resumeBanner{
    display:none; border:none; border-radius:var(--r-md); padding:var(--sp-4); gap:var(--sp-2);
    background:linear-gradient(135deg, var(--tint), var(--surface) 65%);
    box-shadow:var(--shadow-md); border-inline-start:5px solid var(--accent-edge);
  }
  #resumeBanner.show{ display:flex; flex-direction:column; }
  /* C3 fix round 1 — THREE buttons can now share this row, and `flex:1` could not fit them.
     Until C3 at most two of #resumeBtn / #resumeWatchLiveBtn / #discardBtn were ever visible
     together; the alongside render makes three the ordinary case on a device holding a
     journal while another match waits for a decision. `flex:1` is `flex:1 1 0%` with the
     default `min-width:auto`, so each button refuses to shrink below its own label and the
     row spills — measured at 320px: scrollWidth 257 against clientWidth 251, every label
     wrapping. scripts/a11y.js runs this exact probe but structurally cannot reach this
     state, so no rail catches it and the owner meets it on a real phone.

     Mobile-first: wrap, and give each button a basis narrow enough that two still share a
     line at 320px while a third drops to its own full-width line, and wide enough that all
     three sit on one line as soon as there is room. min-width:0 lets them shrink to the
     shared line rather than re-overflowing it. */
  #resumeBanner .row{ display:flex; flex-wrap:wrap; gap:var(--sp-2); margin-top:var(--sp-1); }
  #resumeBanner .row button{ flex:1 1 6.5rem; min-width:0; }

  /* STEP 6A TASK 2B — the unsent-match queue's card. Token-driven throughout and deliberately
     QUIETER than #resumeBanner beside it: the resume banner is an action the player is being
     asked to take now, and this is a background process reporting on itself. Same surface and
     the same neutral ink, so every sentence in it clears AA in both themes without depending
     on a brand colour; --hair rather than --accent on the edge, for the same reason.

     Mobile-first: the row stacks on a narrow screen and only becomes a line when there is
     room, and the buttons carry min-width:0 so a long label shrinks instead of spilling the
     card — the measured failure #resumeBanner's own row comment above records. */
  #parkedMatches{
    border-radius:var(--r-md); padding:var(--sp-4); gap:var(--sp-2);
    display:flex; flex-direction:column;
    background:var(--surface); background-image:var(--glass);
    box-shadow:var(--shadow-md), inset 0 1px 0 var(--hair);
    border-inline-start:5px solid var(--hair);
  }
  #parkedMatches[hidden]{ display:none; }
  #parkedMatches strong{ display:flex; align-items:center; gap:var(--sp-2); color:var(--fg); }
  #parkedMatchesList{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .parkedRow{
    display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-2);
    padding-top:var(--sp-2); border-top:1px solid var(--hair);
  }
  .parkedRowName{ flex:1 1 100%; font-weight:700; color:var(--fg); min-width:0; }
  .parkedRowState{ flex:1 1 100%; min-width:0; }
  .parkedRowActions{ display:flex; flex-wrap:wrap; gap:var(--sp-2); flex:1 1 auto; }
  .parkedRowActions button{ flex:1 1 6.5rem; min-width:0; }

  fieldset{
    border:none; border-radius:var(--r-md); padding:var(--sp-4);
    display:flex; flex-direction:column; gap:var(--sp-3);
    background:var(--surface); background-image:var(--glass);
    box-shadow:var(--shadow-md), inset 0 1px 0 var(--hair);
  }
  .cardTitle{
    font-weight:800; font-size:14px; letter-spacing:.02em; color:var(--fg);
    border-inline-start:3px solid var(--accent-edge); padding-inline-start:10px; line-height:1.2;
  }
  .field{ display:flex; flex-direction:column; gap:6px; }
  .flabel, .field label{ font-size:var(--fs-small); color:var(--muted); font-weight:600; }
  /* Task 21: mint a spoken name for a GUEST seat (a manually-typed player, never a
     registered account) in a friendly match. Shown only beside a guest field with text
     in it — hidden (not removed) so its status hint survives toggling back and forth. */
  .guestVoiceRow{ display:flex; flex-direction:column; gap:4px; }
  .guestVoiceRow[hidden]{ display:none; }
  .guestVoiceBtn{
    display:inline-flex; align-items:center; gap:6px; align-self:flex-start;
    min-height:var(--tap-min); padding:0 14px; border-radius:var(--r-sm);
    background:var(--sub); border:1px solid var(--line-soft); color:var(--accent-ink);
    font-weight:700; font-size:13px; cursor:pointer;
  }
  .guestVoiceBtn:disabled{ opacity:.6; cursor:default; }
  .guestVoiceRow .hint{ margin:0; }
  input[type="text"], input[type="password"], input[type="email"], input[type="date"],
  input[type="tel"], input[type="number"], input[type="search"]{
    font:inherit; color:var(--fg); background:var(--sub);
    border:1.5px solid transparent; border-radius:var(--r-sm);
    min-height:var(--tap-min); padding:var(--sp-2) var(--sp-3);
    user-select:text; -webkit-user-select:text;
    transition:border-color .2s, background-color .2s;
  }
  input[type="text"]:focus-visible, input[type="password"]:focus-visible, input[type="email"]:focus-visible, input[type="date"]:focus-visible,
  input[type="tel"]:focus-visible, input[type="number"]:focus-visible, input[type="search"]:focus-visible{ outline:none; border-color:var(--ring); background:var(--surface); }

  /* EVERY select in the app, one treatment. The custom-rules and correction dropdowns
     used to keep `appearance:auto`, so the browser drew a native arrow that ignores the
     theme entirely — a dark arrow on a dark surface. Owning the chevron here means a new
     dropdown cannot reintroduce that by simply not opting in. */
  select{
    -webkit-appearance:none; appearance:none;
    background-image:var(--select-chevron);
    background-repeat:no-repeat;
    background-position:left 14px center;   /* RTL: the chevron sits at the inline end */
    background-size:17px;
    padding-inline-end:42px;                /* text never runs under the chevron */
  }
  select.playerSelect, select.groupSelect, #umpireVoice{
    font:inherit; color:var(--fg); background-color:var(--sub);
    border:1.5px solid transparent; border-radius:var(--r-sm);
    min-height:var(--tap-min); padding:var(--sp-2) var(--sp-3); width:100%;
    cursor:pointer;
  }
  select.playerSelect:focus-visible, select.groupSelect:focus-visible, #umpireVoice:focus-visible,
  select.playerSelect:focus, select.groupSelect:focus, #umpireVoice:focus{ outline:none; border-color:var(--ring); background-color:var(--surface); }

  .chips{ display:flex; flex-wrap:wrap; gap:6px; margin-top:2px; }
  /* TWO THINGS MEASURED HERE ON 2026-08-09 AND DELIBERATELY NOT CHANGED, recorded rather than
     silently skipped, because an absence fails nothing and no rail can see one.

     1. `min-height:38px` against `--tap-min:48px`. This is a HOUSE-floor miss, not a WCAG one:
        §2.5.8 Target Size (Minimum) is 24x24 CSS px and 38px clears it comfortably; §2.5.5's
        44px is AAA. A chip is a filter, not a primary action, so 38px is defensible — but the
        product picked 48px as its own floor and this does not meet it. Raising it changes the
        height of every chip row in the app, which is a layout change owed a device acceptance
        and not a rider on an accessibility fix to one row.
     2. SHARPER, AND THE ONE WORTH CARRYING: `border:none`, so an UNPRESSED chip's only boundary
        is this `--sub` fill against whatever is behind it — 1.17:1 night / 1.14:1 day on
        `--surface`, and 1.26:1 / 1.05:1 inside a dialog that paints `--bg`. That is §1.4.11
        again, arriving as a FILL rather than as a border token, which is exactly the shape
        tests/rqs01-tokens.test.js's control-edge check cannot see: it reads border and outline
        declarations. `.chip[aria-pressed="true"]` does get a 3:1 boundary from --accent-edge;
        the unselected state, which is most of them, gets none. Fixing it means deciding what a
        chip sits on across the whole app — a cascade question a static check cannot answer. */
  .chip{
    min-height:38px; padding:4px 14px; border:none; border-radius:999px;
    background:var(--sub); color:var(--fg); font-size:14px; font-weight:600; box-shadow:none;
  }
  .chip[aria-pressed="true"]{ background:var(--tint); color:var(--accent-ink); font-weight:800;
    box-shadow:inset 0 0 0 1.5px var(--accent-edge); }

  .seg{ display:flex; background:var(--sub); border-radius:var(--r-sm); padding:4px; gap:4px; }
  /* WKWebView (Safari) does not stretch <button> flex-items to fill a flex column the
     way Chrome does (inputs stretch, buttons don't) — force the full-width auth buttons. */
  #googleBtn, #authSubmit, #rsConfirm{ width:100%; }
  /* OAuth quick-sign-in: breathing room so it isn't cramped against the mode toggle / divider */
  #googleBtn{ margin-block:var(--sp-2); border-radius:var(--r-sm); }
  .seg button{
    flex:1; border:none; border-radius:9px; background:transparent; min-height:44px;
    box-shadow:none; font-weight:600; color:var(--muted);
  }
  .seg button[aria-pressed="true"]{
    background:var(--tint); color:var(--accent-ink); font-weight:800;
    box-shadow:inset 0 0 0 1.5px var(--accent-edge);
  }
  .seg.clay button[aria-pressed="true"]{
    background:var(--clay); color:var(--on-clay); font-weight:800;
    box-shadow:inset 0 0 0 1.5px var(--clay-edge);
  }
  .switchRow{ display:flex; align-items:center; justify-content:space-between; min-height:var(--tap-min); gap:var(--sp-3); }
  /* "צפה בסיור" must stay one line with its icon; let the description text flex/shrink */
  #tourReplay{ flex:0 0 auto; white-space:nowrap; }
  .switch{ position:relative; width:54px; height:32px; flex:none; border-radius:16px; border:none; background:var(--sub); padding:0; min-height:0; box-shadow:inset 0 1px 3px var(--track-shade); }
  .switch::after{
    content:""; position:absolute; top:3px; inset-inline-start:3px; width:26px; height:26px; border-radius:50%;
    background:var(--surface); box-shadow:0 1px 3px var(--shadow-control-color);
    transition:transform .2s var(--ease), background-color .2s;
  }
  .switch[aria-checked="true"]{ background:var(--sel-bg); }
  .switch[aria-checked="true"]::after{ background:var(--sel-fg); transform:translateX(-22px); }

  html, body{ overflow-x:hidden; }   /* only designated containers scroll sideways */
  /* sub-nav: the member panels of the active primary, a sticky-top segment */
  #subNav{ box-shadow:var(--shadow-sm); position:sticky; top:calc(env(safe-area-inset-top, 0px) + 2px); z-index:5; }
  #subNav button{ font-size:14.5px; padding:6px 4px; min-height:46px; }
  .tabPanel{ display:none; }
  /* pad the last row clear of the sticky bottom nav (nav is in-flow, so this is
     just breathing room — no content is ever hidden behind it at full scroll) */
  .tabPanel.active{ display:flex; flex-direction:column; gap:var(--sp-4); padding-bottom:var(--sp-4); }
  /* flex-column children default to min-width:auto and refuse to shrink below
     their content, so a wide inner scroller (the suggestions carousel, tables)
     overflows the card instead of scrolling. Let them shrink so the nested
     overflow-x:auto actually engages. <fieldset> is the sneaky one: it has a
     browser default of min-inline-size:min-content and will grow to fit the
     carousel unless we force min-width:0 on it too. */
  .tabPanel.active > *{ min-width:0; }
  fieldset{ min-width:0; }
  #matchSuggest, #matchSuggest fieldset, #matchSuggest .suggStrip{ min-width:0; max-width:100%; }

  /* sticky start CTA, always one thumb-tap away, even deep in league/history */
  #approvalsBox fieldset{
    border-inline-start:5px solid var(--accent-edge);
    background:linear-gradient(135deg, var(--tint), var(--surface) 70%);
  }
  #instantConfirm{ border-inline-start:5px solid var(--accent-edge); text-align:right; }

  /* The setup is a form, so its primary action stays in document flow. A
     floating CTA covered whichever Play control happened to sit behind it on
     small iPhones and made working choices appear dead. */
  #startBar{
    position:static; z-index:8;
    padding:var(--sp-2) 0;
    background:transparent;
  }
  #startBar button{ width:100%; font-size:20px; min-height:60px; }
  /* "The names are not filled in yet" — the state a first-ever launch opens in, so it is the
     first thing a new player sees, and the button is still LIVE (pressing it flashes the
     empty fields rather than doing nothing). At .55 the whole group — the lime surface AND
     the dark ink on it — faded into the page far enough to reach 2.66:1 in night, under the
     3:1 floor this text size has to clear. Nothing could see it: axe declines text over a
     gradient, and the gate counted "could not determine" as clean. Dimmed enough to read as
     not-ready, not so far that the label stops being readable. */
  #startBtn.dimmed{ opacity:.75; }
  @keyframes shake{ 25%{ transform:translateX(4px);} 75%{ transform:translateX(-4px);} }
  select.playerSelect.flash{ animation:shake .35s ease 2; border-color:var(--danger); }

  /* ================= game ================= */
  #game{ padding:env(safe-area-inset-top) 0 env(safe-area-inset-bottom); }
  #game .num{ direction:ltr; unicode-bidi:isolate; font-variant-numeric:tabular-nums; }
  /* Timer and serve context share ONE flow row. They used to be layered: the timer was
     absolutely positioned and the serve pill reserved space for it with a fixed
     `margin: 6px 70px 0`. A magic number cannot adapt — with long Hebrew names the pill
     wraps to a second line, outgrows the reservation and lands on top of the timer.
     A flex row with a gap keeps them apart at any content length or language. */
  #gameTopBar{
    display:flex; align-items:flex-start; justify-content:space-between;
    gap:var(--sp-2); padding:calc(env(safe-area-inset-top) + 10px) 12px 0;
    flex-wrap:nowrap;
  }
  #gameTopBar:has(#stage3LiveContext[hidden]){ justify-content:flex-start; }
  /* The one way out of a live match that keeps the match. It was a bare stroke icon in a
     corner and the owner could not find it at all — on this screen every other control is
     a scoring action, so the way back has to read as a control, not as decoration. Word
     plus arrow, a surface behind it, and it never shrinks when the serve context is long. */
  #leaveMatchBtn{
    display:flex; align-items:center; gap:6px; flex:0 0 auto;
    min-height:40px; padding:0 12px; font-size:13px; font-weight:800;
    border:1px solid var(--line-soft); border-radius:var(--r-sm);
    background:var(--surface); color:var(--fg); box-shadow:var(--shadow-sm);
  }
  #leaveMatchBtn .ic{ flex:0 0 auto; }
  #stage3LiveContext{
    flex:1 1 auto; min-width:0; display:flex; flex-wrap:wrap;
    justify-content:center; gap:5px 12px;
    padding:6px 12px; border-radius:999px; background:var(--scrim);
    border:1px solid var(--line-soft); color:var(--muted);
    font-size:12px; font-weight:750; text-align:center;
    backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
    overflow-wrap:anywhere;
  }
  /* A let replays the serve and changes no score, so without a visible marker the
     button looks like it did nothing — which is what made it feel "stuck". This states
     that a let happened, which serve is being replayed, and how many have run in a row.
     Consecutive lets are legal and unlimited under ITF rules; the count is information,
     never a penalty. */
  #stage3LetTag{
    display:inline-flex; align-items:center; gap:4px;
    padding:2px 10px; border-radius:999px;
    background:var(--tint);
    color:var(--accent-ink); font-weight:800; white-space:nowrap;
    box-shadow:inset 0 0 0 1.5px var(--accent-edge);
  }
  #stage3LetTag[hidden]{ display:none !important; }
  #stage3LiveContext[hidden],
  #stage3LiveActions[hidden]{ display:none !important; }
  #court{ flex:1; display:flex; flex-direction:column; gap:var(--sp-2); padding:var(--sp-2); min-height:0; }
  .panel{
    flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
    background:linear-gradient(180deg, var(--surface), color-mix(in srgb, var(--surface) 92%, var(--bg) 8%));
    border:1.5px solid var(--line-soft); border-radius:var(--r-lg);
    position:relative; min-height:0; cursor:pointer; overflow:hidden;
    box-shadow:var(--shadow-md);
    transition:transform .12s var(--ease), border-color .25s, box-shadow .25s;
  }
  .panel:active{ transform:scale(.985); }
  .panel.serving{ border-color:var(--accent-edge); box-shadow:var(--shadow-md), 0 0 0 3px color-mix(in srgb, var(--accent-edge) 38%, transparent); }
  .panel.armed{ border-color:var(--accent-edge); border-style:dashed; }
  .panel.armed::after{
    content:'נגיעה נוספת = נקודה'; position:absolute; bottom:12px; inset-inline:0;
    text-align:center; font-size:15px; font-weight:800; color:var(--accent-ink); animation:pulse 1s infinite;
  }
  .pname{ font-size:clamp(16px,4vw,24px); font-weight:800; display:flex; align-items:center; gap:var(--sp-2); letter-spacing:-.01em; max-width:94vw; }
  .pname .nameLabel{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .serveDot{
    width:16px; height:16px; border-radius:50%; display:none; flex:none;
    background:var(--serve-dot-grad);
    box-shadow:var(--serve-dot-shadow);
  }
  .panel.serving .serveDot{ display:inline-block; animation:bounceIn .35s var(--ease); }
  .serveSideTag{ display:none; font-size:12px; font-weight:700; color:var(--muted); background:var(--sub); border-radius:999px; padding:2px 10px; }
  .panel.serving .serveSideTag{ display:inline-block; }
  @keyframes bounceIn{ 0%{ transform:scale(.3); } 60%{ transform:scale(1.25); } 100%{ transform:scale(1); } }
  .ppoints{
    font-family:'Anton', -apple-system, "Segoe UI", sans-serif;
    font-size:clamp(64px, 24vh, 190px); font-weight:400; line-height:1;
    letter-spacing:.01em; color:var(--digit); text-shadow:var(--digit-glow);
  }
  .ppoints.pop{ animation:pop .3s var(--ease); }
  @keyframes pop{ 0%{ transform:scale(.8); opacity:.3; } 55%{ transform:scale(1.07); } 100%{ transform:scale(1); } }
  .pmeta{ display:flex; align-items:center; gap:var(--sp-3); font-size:clamp(13px,3.4vw,18px); color:var(--muted); font-weight:600; }
  .pgames{ font-family:'Anton', sans-serif; font-weight:400; color:var(--fg); font-size:1.3em; letter-spacing:.02em; }
  .setSquares{ display:flex; gap:5px; }
  .setSq{
    min-width:26px; height:26px; padding:0 5px; border-radius:8px;
    display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:800;
    background:var(--sub); color:var(--muted);
  }
  .setSq.won{ background:var(--accent); color:var(--on-accent); box-shadow:var(--shadow-sm); }

  #announceCard{
    display:flex; align-items:center; justify-content:center; gap:var(--sp-2);
    text-align:center; border:none; border-radius:999px;
    padding:var(--sp-2) var(--sp-4); font-weight:800; font-size:clamp(14px,4vw,20px);
    background:var(--surface); direction:ltr; box-shadow:var(--shadow-md);
    overflow:hidden; white-space:nowrap;
  }
  #announceText{ overflow:hidden; text-overflow:ellipsis; }
  #announceText.slideIn{ animation:slideUp .25s var(--ease); }
  @keyframes slideUp{ from{ opacity:0; transform:translateY(10px); } }
  #tbBadge{ display:none; font-size:12px; font-weight:800; letter-spacing:.08em; color:var(--on-accent); background:var(--accent); border-radius:999px; padding:3px 10px; flex:none; }
  #tbBadge.show{ display:inline-block; }

  #actions{ display:flex; gap:var(--sp-2); padding:var(--sp-2); }
  #actions button{ flex:1; padding:var(--sp-2); font-size:16px; border-radius:14px; }
  #undoBtn{ flex:1.6; font-size:14px; font-weight:700; white-space:nowrap; }
  #stage3LiveActions{
    display:grid; grid-template-columns:repeat(3, minmax(0, 1fr));
    gap:var(--sp-2); padding:0 var(--sp-2);
  }
  #stage3LiveActions button{
    min-height:48px; display:flex; align-items:center; justify-content:center;
    gap:6px; padding:7px 9px; border-radius:12px; box-shadow:none;
    background:var(--sub); color:var(--muted); font-size:13px; font-weight:750;
  }
  /* An author display:flex outranks the browser's own [hidden]{display:none}, exactly as
     the invite panel above records. #stage3AuthorityBtn (Stage 3.6 step 5 task 3) is
     hidden in every state except the two the SERVER says this caller may act in — a claim
     on the scoring role, or an answer to somebody else's — so without this line the rule
     above draws it in all of them, and a player is offered a button that can only fail. */
  #stage3LiveActions button[hidden]{ display:none; }
  #stage3LiveActions #pauseBtn[data-paused="true"]{
    color:var(--on-accent); background:var(--accent);
  }
  #lockBanner{
    display:none; text-align:center; background:var(--accent); color:var(--on-accent);
    font-weight:800; padding:6px; font-size:var(--fs-small); letter-spacing:.02em;
  }
  #lockBanner.show{ display:block; }
  #liveSportMismatch{
    display:none; text-align:center; background:var(--warn-surface); color:var(--warn-ink);
    font-weight:700; padding:6px 10px; font-size:var(--fs-small); line-height:1.4;
  }
  #liveSportMismatch.show{ display:block; }
  #matchTimer{
    /* in flow inside #gameTopBar — never overlapping the serve context */
    flex:0 0 auto; align-self:flex-start;
    background:var(--scrim); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
    border-radius:999px; padding:4px 14px; font-size:15px; font-weight:700;
    color:var(--muted); letter-spacing:.04em; box-shadow:var(--shadow-sm);
    white-space:nowrap;
  }
  #endDuration{ color:var(--muted); font-size:clamp(16px,4.5vw,20px); font-weight:700; }
  #endUndoBtn, #endCorrectionBtn{ border:none; background:transparent; box-shadow:none; color:var(--muted); font-size:14px; text-decoration:underline; }
  .stage3EndDetail{
    width:min(100%, 440px); color:var(--muted); font-size:14px;
    font-weight:700; line-height:1.45; overflow-wrap:anywhere;
  }
  .stage3EndSync{
    width:min(100%, 440px); padding:9px 12px; border-radius:var(--r-md);
    background:var(--tint); color:var(--accent-ink); font-size:13px;
    font-weight:800; line-height:1.4;
  }
  .stage3EndDetail[hidden], .stage3EndSync[hidden],
  #endCorrectionBtn[hidden]{ display:none; }

  .histRow{ display:flex; align-items:center; gap:var(--sp-2); padding:8px 0; border-bottom:1px dashed var(--line-soft); font-size:14px; }
  .histRow:last-child{ border-bottom:none; }
  .histMain{ flex:1; min-width:0; }
  .histMain b{ font-weight:800; }
  .histLine{ direction:ltr; unicode-bidi:isolate; font-variant-numeric:tabular-nums; font-weight:700; }
  .histMeta{ color:var(--muted); font-size:12px; }
  /* won–lost value: RTL-native, each number glued to its word so it can't read
     backwards the way a bare LTR "31-23" does under a Hebrew label */
  .wlVal{ direction:rtl; unicode-bidi:isolate; white-space:nowrap; font-weight:800; }
  .wlVal .num{ font-weight:800; }
  .wlVal .wlL{ color:var(--muted); font-weight:700; }
  .wlVal .wlSep{ color:var(--line-soft); margin:0 4px; }
  .histDel{ min-height:34px; width:34px; padding:0; border:none; background:var(--sub); box-shadow:none; border-radius:8px; color:var(--muted); flex:none; }

  /* private-invite cards (קהילה→משחקים): the match text used to be crowded by the
     action buttons on the same line. Now each invite is a vertical card — header
     (who + sport·format) → secondary line (when + court, free to wrap) → status
     chip → actions on their OWN row. Token-driven, both themes, 375-safe, no
     horizontal overflow (actions flex-wrap instead of pushing the card wide). */
  .invCard{ display:flex; flex-direction:column; gap:10px; padding:12px 14px; margin-bottom:10px;
    background:var(--surface-2); border:1px solid var(--line-soft); border-radius:var(--r-md); }
  .invCard:last-child{ margin-bottom:0; }
  .invCard .histMain{ display:flex; flex-direction:column; align-items:flex-start; gap:4px; }
  .invWho{ font-size:14px; line-height:1.35; overflow-wrap:anywhere; }
  .invWho b{ font-weight:800; }
  .invSport{ color:var(--muted); font-weight:600; }
  .invWhen{ font-size:12.5px; line-height:1.4; white-space:normal; overflow-wrap:anywhere; }
  .invChip{ display:inline-flex; align-items:center; gap:5px; margin-top:2px;
    font-size:11.5px; font-weight:800; padding:4px 10px; border-radius:999px;
    background:var(--sub); color:var(--muted); }
  .invChip .ic{ width:13px; height:13px; }
  .invChip.ok{ background:var(--tint); color:var(--accent-ink); }
  .invChip.wait{ background:var(--tint-clay); color:var(--clay-ink); }
  .invChip.bad{ background:var(--tint-danger); color:var(--danger); }
  .invChip.muted{ background:var(--sub); color:var(--muted); }
  .invActs{ display:flex; flex-wrap:wrap; gap:8px; }
  .invActs .invBtn{ min-height:40px; padding:0 16px; font-size:13px; font-weight:700; white-space:nowrap; flex:0 0 auto; }
  .invActs .invBtn.primary{ flex:1 1 auto; min-width:140px; }
  .invActs .invBtn.ghost{ background:transparent; border:none; box-shadow:none; color:var(--muted); padding:0 12px; }

  .stt{ width:100%; min-width:0; border-collapse:collapse; }
  .stt th{ font-size:11px; font-weight:800; color:var(--muted); padding:6px 3px; border-bottom:2px solid var(--line-soft); white-space:nowrap; }
  .stt td{ padding:8px 3px; text-align:center; border-bottom:1px dashed var(--line-soft); font-weight:700; font-size:13px; white-space:nowrap; }
  .stt td.pname-cell{ white-space:nowrap; max-width:40vw; overflow:hidden; text-overflow:ellipsis; }
  /* segments never wrap into uneven rows: keep labels on one line */
  .seg button{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  /* THE 320 FLOOR (2026-08-11). Samsung Screen-zoom one step above its default renders this
     app at 320 CSS px on the Galaxy A50 family (density 540; the default IS 360, one slider
     step away) — and a flex-equal share of a 320px .seg falls below Hebrew label width, so
     the nowrap+ellipsis rule above cut twelve labels on the device while every guard started
     at 360. Two named remedies, each a RULE FAMILY chosen per seg, never a per-label hack:
     .segFit  — buttons take their content width and split only the LEFTOVER equally
                (flex-basis auto keeps the row filled); for segs of short one-word labels,
                which cannot wrap because a single word has no break point;
     .segWrap — the .segTrack precedent (report dialog, "a clipped option is unreadable")
                generalised: a multi-word label wraps to a second line instead of clipping.
     Selection stays marked by the aria-pressed tint either way; min-height keeps the tap
     floor and a wrapped line only grows it. tests/no-truncated-labels.test.js holds 320 in
     WIDTHS from the same day. */
  /* padding-inline one token step under the button default (--sp-4): a content-sized button
     pays its padding per LABEL, so at 320 the default costs a four-button seg 2×16×4 = 128px
     of a ~296px row — the labels lost by 2-6px with the default and clear it at --sp-3.
     Width stays well above the tap target (the narrowest fitted button measures ~53px) and
     min-height keeps the 44px floor. */
  .seg.segFit button{ flex:1 1 auto; padding-inline:var(--sp-3); }
  .seg.segWrap button{ white-space:normal; overflow:visible; text-overflow:clip; line-height:1.15; }
  #segLeagueFormat button, #segMsMode button{ font-size:12px; letter-spacing:-.2px; }
  /* THREE MORE SEGS JOIN THE TWO FAMILIES (2026-08-12), and each is assigned by what its
     labels ARE, exactly as the two remedies above prescribe — never by which one was easier.
     They survived the 2026-08-11 sweep because no guard ever reached them: two sit behind an
     inline display:none the product clears on a state change, and one behind a session.
     Measured at 320 (and #segRegMethod is cut at 360 as well, which is a mainstream phone):
       #segRegMethod   "הרשמה עם טלפון" 150px needed in 121px  -> segWrap: three words, wraps
       #segJoinMode    "באישור מנהל"    122px needed in 104px  -> segWrap: two words, wraps
       #friendsSeg     "החברים שלי"     139px needed in 138px  -> segFit: one pixel over, and
                                                                  the label is two words that
                                                                  should not be split for it;
                                                                  the padding step alone clears
                                                                  it and the row stays one line.
     ASSIGNED HERE RATHER THAN AS A CLASS ON THE ELEMENT, which is where .segFit/.segWrap
     normally go: this change came from a lane that owns styles.css and not index.html, and the
     precedent for a per-seg ID rule in this block is the line directly above. Moving these
     three onto `class="segWrap"` / `class="segFit"` in the markup is a straight swap and is
     the tidier home — it is deliberately left as a swap rather than done half-way. */
  .seg#segRegMethod button, .seg#segJoinMode button{
    white-space:normal; overflow:visible; text-overflow:clip; line-height:1.15;
  }
  .seg#friendsSeg button{ flex:1 1 auto; padding-inline:var(--sp-3); }
  .msFilters{ flex-direction:column; }
  .stt tr:last-child td{ border-bottom:none; }
  .stt td.pname-cell{ text-align:right; }
  .stt tr.leader td{ color:var(--fg); }
  .stt tr.leader td.pname-cell{ font-weight:800; }

  #statsWrap{ display:none; position:fixed; inset:0; z-index:15; background:var(--scrim-overlay); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); align-items:center; justify-content:center; padding:var(--sp-4); }
  #statsWrap.show{ display:flex; }
  #statsCard{
    background:var(--bg); background-image:var(--bg-grad); border-radius:var(--r-lg);
    padding:var(--sp-4); width:100%; max-width:480px; max-height:85dvh; overflow-y:auto;
    box-shadow:var(--shadow-md); animation:sheetUp .25s var(--ease);
  }
  #statsCard h2{ font-size:20px; font-weight:900; display:flex; justify-content:space-between; align-items:center; margin-bottom:var(--sp-3); }
  /* The stats overlay shows EITHER the table or the "no point data" line, never both and
     never neither. Spelled out rather than left to the UA sheet: an author `display` rule
     of equal specificity beats `[hidden]{display:none}`, and this repository has already
     shipped a screen where `hidden` did not hide. */
  #statsTable[hidden], #statsEmpty[hidden]{ display:none; }
  #statsTable{ width:100%; border-collapse:collapse; }
  #statsTable th, #statsTable td{ padding:10px 6px; text-align:center; border-bottom:1px dashed var(--line-soft); }
  #statsTable tr:last-child td{ border-bottom:none; }
  #statsTable th{ font-weight:800; font-size:15px; }
  #statsTable td:first-child{ text-align:right; color:var(--muted); font-size:13px; font-weight:600; }
  #statsTable td{ font-weight:700; }

  #updateToast{
    position:fixed; bottom:calc(env(safe-area-inset-bottom) + 14px); inset-inline:14px; z-index:30;
    display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3);
    background:var(--sel-bg); color:var(--sel-fg); font-weight:700;
    border-radius:var(--r-md); padding:var(--sp-3) var(--sp-4); box-shadow:var(--shadow-md);
  }
  #updateToast button{ background:var(--sel-fg); color:var(--sel-bg); border:none; font-weight:800; min-height:40px; }
  #updateToast[hidden]{ display:none; }   /* author display:flex must not defeat the hidden attribute */

  /* The background-write notice. Sits above #bottomNav (z-index 7) and below #updateToast
     (30), which is a whole-app event and outranks one write.
     TWO TONES, and they are different on purpose: a REFUSAL is the warn pair — the same
     --warn-surface/--warn-ink #liveSportMismatch already ships, whose ink is a guaranteed
     on-colour for that surface in both themes rather than a hope. A deferral is the quiet
     --sub/--muted pair (measured 6.20:1 on day; both clear the 4.5:1 body floor), because a
     device that cannot authenticate is a fact to state, not an alarm. Mobile-first: it spans
     the viewport with a gutter and caps to the app column on wide screens. */
  #writeNotice{
    position:fixed; z-index:8;
    inset-inline:var(--sp-3); margin-inline:auto; max-width:var(--maxw-440px);
    bottom:calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--sp-3));
    background:var(--sub); color:var(--muted);
    border-radius:var(--r-md); box-shadow:var(--shadow-md);
    padding:var(--sp-3) var(--sp-4);
    font-size:var(--fs-small); font-weight:700; line-height:1.45; text-align:center;
  }
  #writeNotice[data-tone="refused"]{ background:var(--warn-surface); color:var(--warn-ink); }
  #writeNotice[hidden]{ display:none; }

  /* Step 6 viewer task 3: class-based (was #stakeBadge) — the badge exists once on the live
     court (id="stakeBadge" AND class="stakeBadge") and once, class-only, on the viewer
     screen; an id may not exist twice in the document, so styling keys off the class both
     places share. */
  .stakeBadge{
    align-self:center; border-radius:999px; padding:6px 18px;
    font-weight:900; font-size:clamp(14px,4vw,19px); letter-spacing:.02em;
    background:var(--accent); color:var(--on-accent); box-shadow:var(--shadow-md);
    animation:pulse 1.1s infinite;
  }
  .stakeBadge[hidden]{ display:none; }
  .streakTag{ color:var(--accent-ink); font-weight:800; }
  .faultBtn{
    display:none; min-height:34px; padding:2px 14px; border-radius:999px;
    background:var(--sub); border:1.5px dashed var(--line-soft); box-shadow:none;
    font-size:13px; font-weight:800; color:var(--muted); margin-top:4px;
  }
  .panel.serving .faultBtn{ display:inline-block; }
  .panel.serving.secondServe .faultBtn{ background:var(--accent); color:var(--on-accent); border-style:solid; }

  /* Step 6 viewer task 3 — the read-only screen. #viewerCourt mirrors #court's own layout
     rule (a second declaration rather than a shared class, since the two ids never coexist
     in one selector and this keeps each screen's rule self-contained and easy to find). */
  #viewerCourt{ flex:1; display:flex; flex-direction:column; gap:var(--sp-2); padding:var(--sp-2); min-height:0; }
  /* FIX ROUND 3, M4 — AND `hidden` HAS TO ACTUALLY HIDE IT.
     The author `display:flex` immediately above outranks the browser's own
     [hidden]{display:none}, exactly as this stylesheet already records for
     .stage3PresetCards, #stage3InviteSend, #stage3RulesModeField and #stage3LetTag. So every
     `$('viewerCourt').hidden = true` written since task 3 has been a no-op — including
     stage3CloseViewerIntoSummary's own, which meant a match that ended with no legal result
     showed "המשחק הסתיים בלי תוצאה סופית" UNDERNEATH the still-visible frozen board. Measured
     by tests/viewer-screen.test.js, which asserts the computed display, not the attribute. */
  #viewerCourt[hidden]{ display:none; }
  #viewerTopBar{
    display:flex; align-items:center; gap:var(--sp-2);
    padding:calc(env(safe-area-inset-top) + 10px) 12px 0;
  }
  #viewerBackBtn{
    display:flex; align-items:center; flex:0 0 auto;
    min-height:40px; padding:0 14px; font-size:13px; font-weight:800;
    border:1px solid var(--line-soft); border-radius:var(--r-sm);
    background:var(--surface); color:var(--fg); box-shadow:var(--shadow-sm);
  }
  #viewerScorerHeader{ flex:1 1 auto; min-width:0; text-align:center; }
  #viewerReadOnlyNotice, #viewerPausedNotice, #viewerEndedNotice{
    text-align:center; padding:0 var(--sp-3);
  }
  #viewerEndedNotice{ flex:1; display:flex; align-items:center; justify-content:center; }
  /* An author display:flex outranks the browser's own [hidden]{display:none} — the same
     trap #updateToast/#statsTable already name in this file. */
  #viewerEndedNotice[hidden]{ display:none; }
  #moreMenu{
    position:absolute; bottom:calc(env(safe-area-inset-bottom) + 68px); inset-inline-end:var(--sp-3);
    z-index:8; display:flex; flex-direction:column; gap:var(--sp-2);
    background:var(--surface); border-radius:var(--r-md); padding:var(--sp-3);
    box-shadow:var(--shadow-md); min-width:210px;
  }
  #moreMenu[hidden]{ display:none; }
  #moreMenu button{ text-align:right; }
  #moreMenu button.danger{ color:var(--danger); border-color:color-mix(in srgb, var(--danger) 45%, transparent); font-weight:800; }
  .stage3GameOverlay{
    position:fixed; inset:0; z-index:24; align-items:center; justify-content:center;
    padding:var(--sp-4); background:var(--scrim-match);
    backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  }
  .stage3GameOverlay.show{ display:flex; }
  .stage3Dialog{
    /* overflow-x is HIDDEN, deliberately, and deliberately not `clip`: `overflow:auto` on
       both axes meant any child that refused to shrink scrolled the DIALOG sideways — the
       owner's report — while html/body{overflow-x:hidden} kept the only overflow rail in
       the repo (tests/stage2-shell.test.js measures the document) structurally green.
       `hidden` keeps a scrollable overflow region, so scrollWidth still exceeds
       clientWidth and tests/report-score-grid.test.js can FAIL on a spill; `clip` would
       erase the region and blind the very guard written for it. A finger can no longer
       drag the dialog sideways either way. */
    width:min(100%, 480px); max-height:88dvh; overflow-y:auto; overflow-x:hidden;
    padding:var(--sp-4); border:1px solid var(--line);
    border-radius:var(--r-lg); background:var(--bg); background-image:var(--bg-grad);
    box-shadow:var(--shadow-md);
  }
  .stage3Dialog h2{ margin:0; font-size:21px; }
  .stage3Dialog > p{ margin-top:8px; color:var(--muted); }

  /* ============ THE ANDROID KEYBOARD FOLD (2026-08-12) ============
     On Android the keyboard covers the page and the page is never told. Measured live over
     CDP inside the real WebView with the IME up: innerHeight 630 before and after,
     visualViewport.height 630.2 before and after, offsetTop and pageTop 0, the injected
     --safe-area-inset-* variables absent, env(keyboard-inset-height) 0px, and
     navigator.virtualKeyboard present, accepting `overlaysContent = true` without error and
     reporting a 0x0 rectangle with no geometrychange — not one resize event of any kind.
     `dumpsys window` says why from the other side: with the IME visible the app window's
     frame is unchanged, because MainActivity declares adjustNothing. That attribute is the
     white-screen fix and it STAYS; removing it to buy a viewport signal trades a covered
     button for a dead app (tests/android-webview-background.test.js).

     So the dispute form's submit sat at y477..541 under a keyboard whose top edge is y309,
     and a tap on it did nothing at all. The signal used instead is `:focus-within` on a
     control that summons a keyboard — a real DOM state, no listener, no API, and on this
     shell an open editing session IS a raised keyboard. What it cannot say is how much is
     covered; nothing can, from inside the page. --kbd-reserve is that bound, argued at its
     declaration.

     WHAT THE RULES DO, and why it is the overlay's padding rather than the dialog's height:
     the overlay is the flex box that centres the dialog, so reserving the fold as padding on
     the overlay makes the dialog centre inside the region that is still VISIBLE, without any
     rule needing to know the dialog's own height. `max-height:100%` then keeps it inside that
     region instead of the 88dvh it may have anywhere else — a centred flex item taller than
     its line has its top clipped and is unreachable, which is the failure this pairing avoids.
     The action bar sticks to the dialog's bottom so the primary action is on screen at every
     scroll position rather than merely reachable.

     SCOPED TO ANDROID because iOS resizes its visual viewport for the keyboard and already
     has a real signal; app.js:571 writes the attribute and writes `ios` for everything that
     is not Android, which is also why every desktop suite — including the 320 burial proof in
     tests/report-score-grid.test.js — is untouched by all of this. Guarded by
     tests/android-keyboard-fold.test.js, which also forbids the two dead signals above,
     because they are the right answer everywhere except here and would review as a fix.

     The selector list is repeated rather than named: CSS has no way to name a selector, and
     the alternative is a class that some JS has to remember to add. It is every control that
     raises a keyboard and no control that does not — a checkbox, a radio or a submit taking
     focus must move nothing, which is half of what the guard asserts. */
  html[data-platform="android"] .stage3GameOverlay:has(:is(textarea,
      input:not([type="button"],[type="submit"],[type="reset"],[type="checkbox"],
        [type="radio"],[type="range"],[type="color"],[type="file"],[type="image"]),
      [contenteditable="true"]):focus){
    padding-block-end:calc(var(--sp-4) + var(--kbd-reserve));
  }
  html[data-platform="android"] .stage3GameOverlay:has(:is(textarea,
      input:not([type="button"],[type="submit"],[type="reset"],[type="checkbox"],
        [type="radio"],[type="range"],[type="color"],[type="file"],[type="image"]),
      [contenteditable="true"]):focus) .stage3Dialog{
    max-height:100%;
    /* so a focus scroll inside the dialog clears the bar that is now stuck to its bottom */
    scroll-padding-block-end:calc(var(--tap-min) + var(--sp-4));
  }
  html[data-platform="android"] .stage3GameOverlay:has(:is(textarea,
      input:not([type="button"],[type="submit"],[type="reset"],[type="checkbox"],
        [type="radio"],[type="range"],[type="color"],[type="file"],[type="image"]),
      [contenteditable="true"]):focus) .stage3Dialog .stage3DecisionActions{
    position:sticky; bottom:calc(var(--sp-4) * -1); z-index:1;
    background:var(--bg);
    padding-block:var(--sp-2) var(--sp-4);
    margin-block-end:calc(var(--sp-4) * -1);
  }
  /* A FORM IN THE PAGE FLOW IS THE WORSE CASE, not the milder one: the browser scrolls a
     focused control just inside the viewport it believes it has — the whole 630px — so the
     field itself lands under the keyboard and the player cannot see what they are typing
     (#loginUser focused, measured at 291..339 against a 309 fold). scroll-padding on the
     scrollport makes the browser's OWN focus scroll keep the control above the reserve; no
     listener and no scroll of ours. The action row of a long form is not promised here and
     deliberately so — a submit eight hundred pixels below the field is ordinary scrolling,
     and a rule that pretended to lift it would be a lie.

     ON `body`, NOT ON THE ROOT, and this was settled by measurement rather than by the spec
     sentence that scroll-padding on the root element applies to the viewport. `body` in this
     app carries `overflow-y:auto`, so IT is the scrollport: with the rule on `html` the
     computed padding read 378px, `document.scrollingElement` was documentElement, its
     maxScroll was 0, body's own scrollTop was 1300 — and the field stayed at 291..339,
     exactly where it had been. The rule was correct, applied, computed, and on the wrong box. */
  html[data-platform="android"]:has(:is(textarea,
      input:not([type="button"],[type="submit"],[type="reset"],[type="checkbox"],
        [type="radio"],[type="range"],[type="color"],[type="file"],[type="image"]),
      [contenteditable="true"]):focus) body{
    scroll-padding-block-end:var(--kbd-reserve);
  }
  html[data-platform="android"]:has(:is(textarea,
      input:not([type="button"],[type="submit"],[type="reset"],[type="checkbox"],
        [type="radio"],[type="range"],[type="color"],[type="file"],[type="image"]),
      [contenteditable="true"]):focus) .tabPanel.active{
    /* the reserve is only scrollable if there is something under the last field to scroll */
    padding-bottom:calc(var(--sp-4) + var(--kbd-reserve));
  }
  .stage3DecisionActions{
    display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-3); margin-top:var(--sp-4);
  }
  /* A ONE-BUTTON BAR TAKES THE WHOLE ROW. Owner, 2026-08-09, twice, on the device: the submit
     button on the report-result screen is badly placed, does not look right against the rest
     of the page, and "צמוד לימין".

     MEASURED, not guessed: this class was authored for the TWO-button decision dialogs
     (accept/reject), and a lone child fills column 1 only — 217px inside a 446px bar. The page
     is RTL, so column 1 is the right edge, which is exactly the words he used. It is not one
     screen: three bars in index.html hold a single button — #resultDisputeSubmit,
     #reportResultSubmit and #resultReviseSubmit — and all three were half-width. The second
     half of his complaint is the same fact from the other side: on that form the submit was
     the ONLY control not running edge to edge, so it read as a stray chip rather than the
     primary action.

     WHY :has(> :only-child) AND NOT grid-auto-flow:column. Both were measured in the live DOM
     under this stylesheet and both fix the one-child case to the full 446px with the TWO-child
     case byte-identical at [217, 217]. They differ on a shape that does not exist yet: with
     grid-auto-flow:column a future THREE-button bar becomes one row of 141px, and grid items
     are min-width:auto — they refuse to shrink below their own label, so the row spills at
     320px. #resumeBanner's C3 comment in this file records precisely that trap, already paid
     for once. This rule changes the broken case and nothing else; a three-button bar keeps
     today's two-row wrap, which is the behaviour that has always worked. :has() is settled
     precedent here (#startBar, line 890), not a bet on support. */
  .stage3DecisionActions:has(> :only-child){ grid-template-columns:1fr; }
  /* `min-width:0` — a grid item's default is min-width:auto, which refuses to shrink below its
     own label. FOUND 2026-08-09 by the new geometry guard, pre-existing and unrelated to the
     one-button fix above (proven by mutation: removing the :has rule leaves this failing and
     removing this leaves that failing): at 320px #rankedInviteAccept measured 130.4px and
     #rankedInviteDecline 112.9px inside a 254px bar, so the two-column row spilled by 1.3px
     instead of sharing the width. This is the trap #resumeBanner's C3 comment already names.
     The label wraps inside the 64px min-height instead, which is why that floor stays. */
  .stage3DecisionActions button{ min-height:64px; font-weight:850; min-width:0; }
  /* THE CLOSE CONTROL BELONGS ON THE TITLE ROW. Found while measuring the button above, and
     it is the same shape of defect one element over: .correctionDialog opts its header into
     the row below, .reportResultDialog reused the dialog pattern without opting in, so its
     computed display stayed `block` and the ✕ dropped to its own line under the intro text.
     Two dialogs that look like one component behaving differently is what makes a screen read
     as unfinished. */
  .reportResultDialog > header,
  .correctionDialog > header{
    display:flex; justify-content:space-between; align-items:flex-start;
    gap:var(--sp-3); margin-bottom:var(--sp-3);
  }
  .correctionDialog > header p{ margin-top:4px; }
  #correctionTimeline{
    display:flex; flex-direction:column; gap:7px; max-height:34dvh;
    overflow:auto; padding:2px; scroll-padding:2px;
  }
  #correctionTimeline button{
    width:100%; min-height:52px; display:grid;
    grid-template-columns:auto minmax(0, 1fr); gap:4px 10px;
    align-items:center; text-align:start; padding:8px 10px;
    background:var(--surface); box-shadow:none;
  }
  #correctionTimeline button::before{
    content:""; width:18px; height:18px; grid-row:1 / span 2;
    border:2px solid var(--line-interactive); border-radius:50%;
  }
  #correctionTimeline button[aria-selected="true"]{
    border-color:var(--accent); background:var(--tint);
  }
  #correctionTimeline button[aria-selected="true"]::before{
    border:5px solid var(--accent);
  }
  #correctionTimeline button[data-active="false"]{
    opacity:.62; cursor:default; text-decoration:line-through;
  }
  #correctionTimeline button b{ font-size:13px; }
  #correctionTimeline button small{ color:var(--muted); font-size:11.5px; }
  #correctionActions{
    margin-top:var(--sp-3); padding:var(--sp-3);
    border:1px solid var(--line-soft); border-radius:var(--r-md);
  }
  #correctionActions[disabled]{ opacity:.55; }
  #correctionActions legend{ padding:0 6px; font-size:13px; font-weight:800; }
  #correctionCancelBtn{ width:100%; color:var(--danger); font-weight:800; }
  .correctionReplace{
    display:grid; grid-template-columns:minmax(0,1fr) auto;
    gap:7px; align-items:end; margin-top:var(--sp-3);
  }
  .correctionReplace label{ grid-column:1 / -1; font-size:12px; color:var(--muted); }
  .correctionReplace select{
    min-height:48px; min-width:0; padding:7px 9px;
    border:1px solid var(--line-soft); border-radius:var(--r-sm);
    background-color:var(--surface); color:var(--fg); font:inherit;
  }
  .correctionReplace button{ min-height:48px; }
  #momentumWrap{ width:100%; max-width:440px; background:var(--sub); border-radius:var(--r-md); padding:12px; }
  #momentum{ width:100%; height:120px; display:block; }
  #endRivalry{ font-size:15px; font-weight:700; }

  /* landscape scoreboard, applies whenever the phone is rotated, any height */
  @media (orientation: landscape){
    #court{ flex-direction:row; }
    #announceCard{ position:absolute; inset-inline:28%; top:var(--sp-2); z-index:2; background:var(--scrim); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px); }
    .panel{ padding-bottom:52px; }
    /* corners hold ONLY the serve dot + name; everything else has its own layer */
    .pname{ position:absolute; top:10px; z-index:2; }
    /* Step 6 viewer task 3: class-based (were #panelA/#panelB) — both the live court and the
       viewer screen mirror left/right by class, never by an id that may not exist twice. */
    .panelA .pname{ right:14px; }
    .panelB .pname{ left:14px; }
    .serveSideTag{ display:none !important; }         /* the dot already marks the server */
    .panel{ padding-top:60px; }                       /* digits clear the top pills */
    .panel.serving .faultBtn{
      position:absolute; bottom:56px; margin:0; min-height:30px; font-size:12px; padding:2px 12px; z-index:2;
    }
    .panelA .faultBtn{ right:14px; }
    .panelB .faultBtn{ left:14px; }
    .stakeBadge{ position:absolute; top:54px; left:50%; transform:translateX(-50%); z-index:3; }
    .pmeta{
      position:absolute; bottom:58px; left:50%; transform:translateX(-50%);
      inset-inline:auto; justify-content:center; width:max-content; max-width:90%;
      background:var(--scrim); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
      border-radius:999px; padding:4px 14px; z-index:2;
    }
    .ppoints{ line-height:1; font-size:clamp(80px, 28vw, 54vh); }
    /* landscape reclaims vertical space: float the timer over the bottom edge */
    #matchTimer{ position:absolute; top:auto; inset-inline-start:auto;
      bottom:calc(env(safe-area-inset-bottom) + 58px); left:50%;
      transform:translateX(-50%); z-index:9; }
    #gameTopBar{ padding-top:env(safe-area-inset-top); }
    .ppoints{ font-size:clamp(80px, 34vw, 66vh); }
    #actions{ position:absolute; bottom:env(safe-area-inset-bottom); inset-inline:0; background:transparent; }
    #actions button{ min-height:40px; background:var(--scrim); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px); }

    /* setup: two columns so the whole form is reachable without endless scrolling
       (.active is required, a bare #setup rule would override .screen's display:none) */

  }
  /* very short landscape screens: compact the end screen so nothing scrolls */
  @media (orientation: landscape) and (max-height: 500px){
    #end{ gap:var(--sp-2); padding:var(--sp-3); }
    #end .trophy{ font-size:40px; }
    #endWinner{ font-size:clamp(24px,6vh,36px); }
    .setChip{ font-size:16px; padding:4px 12px; }
  }

  /* ================= end + shared summary ================= */
  #end, #summary{ align-items:center; justify-content:center; gap:var(--sp-4); padding:var(--sp-5); text-align:center; }
  #summary .row{ display:flex; width:100%; max-width:420px; }
  #summary .row button{ flex:1; }
  #sumWinner{ font-size:clamp(26px,7vw,42px); font-weight:900; color:var(--digit); text-shadow:var(--digit-glow); }
  #sumLine{ font-size:clamp(20px,6vw,30px); font-weight:800; }
  #sumMeta{ color:var(--muted); font-weight:700; }
  #sumStats{ border-collapse:collapse; }
  #sumStats th, #sumStats td{ padding:8px 6px; text-align:center; border-bottom:1px dashed var(--line-soft); }
  #sumStats td:first-child{ text-align:right; color:var(--muted); font-size:13px; font-weight:600; }
  #end .trophy, #summary .trophy{ font-size:64px; animation:trophyIn .6s var(--ease); }
  @keyframes trophyIn{ 0%{ transform:scale(.2) rotate(-15deg); opacity:0; } 60%{ transform:scale(1.15) rotate(4deg); } 100%{ transform:scale(1); } }
  #endWinner{ font-size:clamp(30px,8vw,50px); font-weight:900; letter-spacing:-.02em; color:var(--digit); text-shadow:var(--digit-glow); }
  #endSets{ display:flex; gap:var(--sp-2); justify-content:center; flex-wrap:wrap; }
  .setChip{
    background:var(--surface); border-radius:999px; padding:6px 16px;
    font-size:clamp(16px,4.5vw,22px); font-weight:800; box-shadow:var(--shadow-md);
  }
  #end .row{ display:flex; gap:var(--sp-3); width:100%; max-width:420px; margin-top:var(--sp-2); }
  #end .row button{ flex:1; }

  /* ================= sheet (clicker) ================= */
  #sheetWrap{ display:none; position:fixed; inset:0; z-index:10; background:var(--scrim-overlay); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); }
  #sheetWrap.show{ display:flex; align-items:flex-end; }
  #sheet{
    background:var(--bg); background-image:var(--bg-grad);
    border-radius:var(--r-lg) var(--r-lg) 0 0;
    width:100%; max-height:88dvh; overflow-y:auto; padding:var(--sp-3) var(--sp-4);
    display:flex; flex-direction:column; gap:var(--sp-3);
    padding-bottom:max(var(--sp-4), env(safe-area-inset-bottom));
    box-shadow:0 -12px 40px var(--shadow-sheet-color);
    animation:sheetUp .3s var(--ease);
  }
  @keyframes sheetUp{ from{ transform:translateY(40px); opacity:.5; } }
  .sheetHandle{ width:44px; height:5px; border-radius:3px; background:var(--line-soft); margin:0 auto; }
  #sheet h2{ font-size:20px; font-weight:900; display:flex; justify-content:space-between; align-items:center; }
  .controllerAlwaysOn{
    display:flex; align-items:center; gap:12px; min-height:64px;
    padding:12px; border:1px solid var(--line-soft); border-radius:var(--r-md);
    background:var(--tint);
  }
  .controllerAlwaysOn[hidden],
  #controllerDevicePicker[hidden]{ display:none; }
  .controllerAlwaysOn > span:last-child{
    display:flex; flex-direction:column; gap:3px; min-width:0;
  }
  .controllerAlwaysOn small{
    color:var(--muted); font-size:13px; line-height:1.45; font-weight:650;
  }
  #controllerDevicePicker{
    display:flex; flex-direction:column; gap:10px;
  }
  #controllerDevicePicker legend{ padding-inline:0; }
  .controllerPicker{ display:grid; gap:8px; }
  .controllerChoice{
    width:100%; min-height:64px; padding:10px 12px;
    display:flex; align-items:center; justify-content:space-between; gap:12px;
    text-align:start; background:var(--surface-2);
    border:1.5px solid var(--line-soft); border-radius:var(--r-md);
  }
  .controllerChoice > span:first-child{
    display:flex; flex-direction:column; gap:3px; min-width:0;
  }
  .controllerChoice b{ font-size:15px; color:var(--fg); }
  .controllerChoice small{
    color:var(--muted); font-size:12px; font-weight:650; line-height:1.35;
  }
  .controllerChoice[aria-pressed="true"]{
    border-color:var(--accent-edge); background:var(--tint);
    box-shadow:0 0 0 1px var(--accent-edge);
  }
  .controllerPickerState{
    flex:none; border-radius:999px; padding:5px 9px;
    background:var(--sub); color:var(--muted); font-size:11px; font-weight:850;
  }
  .controllerPickerState.connected,
  .controllerPickerState.tested{
    background:var(--tint); color:var(--accent-ink);
  }
  .controllerPickerState.unsupported{
    background:var(--tint-clay); color:var(--clay-ink);
  }
  .controllerSetupPanel[hidden]{ display:none; }
  .controllerBack{
    align-self:flex-start; min-height:48px; padding:8px 12px;
    background:var(--surface-2); color:var(--fg);
    border-color:var(--line-soft); font-weight:750;
  }
  .controllerDeviceGroup{
    display:flex; flex-direction:column; gap:var(--sp-3); min-width:0;
  }
  .controllerDeviceGroup[hidden]{ display:none; }
  .controllerDeviceGroup > button.primary{ width:100%; }
  #controllerTester[hidden],
  #controllerTechnicalLog[hidden]{ display:none; }
  #segProfile button{
    min-height:54px; padding:6px; white-space:normal;
    overflow:visible; text-overflow:clip; line-height:1.2; font-size:12px;
  }
  .controllerKind{
    align-self:flex-start; border-radius:999px; padding:4px 10px;
    background:var(--tint); color:var(--accent-ink);
    font-size:12px; font-weight:850;
  }
  .controllerNotice{
    margin:0; border-radius:var(--r-sm); padding:10px 12px;
    background:var(--sub); color:var(--fg);
    font-size:13px; line-height:1.5;
  }
  .controllerSteps{
    margin:0; padding-inline-start:22px; display:grid; gap:6px;
    color:var(--fg); font-size:13px; font-weight:650;
  }
  .controllerMap{
    display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px;
  }
  .controllerMap > span{
    min-width:0; border:1px solid var(--line-soft); border-radius:var(--r-sm);
    background:var(--surface-2); padding:10px; display:flex; flex-direction:column; gap:3px;
  }
  .controllerMap b{ direction:ltr; unicode-bidi:isolate; font-size:12px; }
  .controllerMap small{ color:var(--muted); font-size:12px; font-weight:700; }
  .controllerTestGrid{
    display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px;
  }
  .controllerTestGrid [data-controller-action]{
    min-height:48px; display:flex; align-items:center; justify-content:center;
    border:1.5px solid var(--line-soft); border-radius:var(--r-sm);
    background:var(--surface-2); color:var(--muted); font-size:13px; font-weight:850;
  }
  .controllerTestGrid [data-controller-action].done{
    border-color:var(--accent); background:var(--tint); color:var(--accent-ink);
  }
  #controllerTestStatus{
    min-height:46px; border-radius:var(--r-sm); padding:10px 12px;
    background:var(--sub); color:var(--fg); font-size:13px; font-weight:700;
    overflow-wrap:anywhere;
  }
  .controllerTestActions{
    display:grid; grid-template-columns:minmax(0, 1fr); gap:8px;
  }
  .controllerTestActions button{ min-width:0; }
  #stopControllerTest{ grid-column:1 / -1; }
  .keyChip{ direction:ltr; font-family:ui-monospace, Menlo, monospace; font-size:14px; border-radius:8px; padding:3px 10px; background:var(--sub); font-weight:700; }
  .learnRow{ display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2); }
  .learnRow button.learning{
    background:linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 84%, var(--mix-darken) 16%));
    color:var(--on-accent); border:1px solid var(--accent-edge); font-weight:800; animation:pulse 1s infinite;
  }
  @keyframes pulse{ 50%{ opacity:.6; } }
  #testerLog{
    direction:ltr; text-align:left; font-family:ui-monospace, Menlo, monospace; font-size:13px;
    background:var(--sub); border-radius:var(--r-sm);
    height:180px; overflow-y:auto; padding:var(--sp-2); user-select:text; -webkit-user-select:text;
  }
  #testerLog div{ padding:2px 0; border-bottom:1px dashed var(--line-soft); }
  .hint{ font-size:var(--fs-small); color:var(--muted); }
  /* ---- announcement voice picker (token-driven, both themes) ---- */
  #voiceOpts{ display:flex; flex-direction:column; gap:8px; }
  #voiceOpts .voice{ display:flex; align-items:center; gap:12px; padding:12px; border-radius:var(--r-md);
    border:1.5px solid var(--line-soft); background:var(--surface-2); cursor:pointer; transition:.15s var(--ease); }
  #voiceOpts .voice.sel{ border-color:var(--accent); background:var(--tint); }
  #voiceOpts .radio{ width:20px; height:20px; border-radius:50%; border:2px solid var(--line); flex:none; position:relative; }
  #voiceOpts .voice.sel .radio{ border-color:var(--accent); }
  #voiceOpts .voice.sel .radio::after{ content:''; position:absolute; inset:3px; border-radius:50%; background:var(--accent); }
  #voiceOpts .vmeta{ flex:1; min-width:0; }
  #voiceOpts .vname{ font-weight:800; font-size:15px; display:flex; align-items:center; gap:7px; }
  #voiceOpts .gtag{ font-size:11px; font-weight:800; padding:1px 7px; border-radius:999px; }
  #voiceOpts .gtag.m{ background:var(--tint); color:var(--accent-ink); }
  #voiceOpts .gtag.f{ background:var(--tint-clay); color:var(--clay-ink); }
  #voiceOpts .vdesc{ font-size:12px; color:var(--muted); margin-top:2px; }
  #voiceOpts .demoBtns{ display:flex; gap:6px; flex:none; }
  #voiceOpts .vdemo{ min-height:34px; padding:0 10px; border-radius:10px; border:1px solid var(--line);
    background:var(--surface); color:var(--fg); font-size:12px; font-weight:800; cursor:pointer; }
  #voiceFold .previewRow{ margin-top:16px; display:flex; flex-direction:column; gap:8px; }
  #voiceFold #anPreviewBtn{ min-height:var(--tap-min); border-radius:var(--r-sm); border:1px solid var(--line);
    background:var(--surface-2); color:var(--fg); font-weight:800; font-size:15px; }
  #voiceFold .lockNote{ border-radius:var(--r-md); border:1px dashed var(--line); background:var(--surface-2);
    padding:12px; font-size:13.5px; font-weight:700; text-align:center; }
  #voiceFold .lockNote b{ color:var(--accent-ink); }
  /* ---- My Matches calendar grid ---- */
  #mmGrid .mmRow{ display:grid; grid-template-columns:repeat(7,1fr); gap:4px; margin-bottom:4px; }
  .mmDow{ text-align:center; font-size:11px; color:var(--muted); font-weight:800; padding:2px 0; }
  .mmCell{ position:relative; aspect-ratio:1; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; border-radius:10px; color:var(--fg); }
  .mmCell.mmBlank{ visibility:hidden; }
  .mmCell.mmHas{ background:var(--surface-2); border:1px solid var(--line-soft); }
  .mmCell.mmToday{ outline:2px solid var(--accent-edge); outline-offset:-2px; }
  .mmDot{ position:absolute; bottom:5px; width:6px; height:6px; border-radius:50%; background:var(--clay-edge); }
  .mmDot.fut{ background:var(--accent-edge); }
  details.fold{
    background:var(--surface); border:1px solid var(--line-soft);
    border-radius:var(--r-md); box-shadow:var(--shadow-sm);
  }
  details.fold > summary{
    list-style:none; cursor:pointer; min-height:54px;
    display:flex; align-items:center; gap:10px; padding:0 var(--sp-4);
    font-weight:800; font-size:14px; color:var(--fg);
  }
  details.fold > summary::-webkit-details-marker{ display:none; }
  details.fold > summary::before{
    content:""; width:3px; height:16px; border-radius:2px; background:var(--accent); flex:none;
  }
  details.fold > summary::after{
    content:"‹"; direction:ltr; margin-inline-start:auto; color:var(--muted); font-size:18px; font-weight:700;
    transform:rotate(-90deg); transition:transform .2s var(--ease);
  }
  details.fold[open] > summary::after{ transform:rotate(90deg); }
  details.fold[open] > summary{ border-bottom:1px dashed var(--line-soft); }
  details.fold > .foldBody{ padding:var(--sp-4); display:flex; flex-direction:column; gap:var(--sp-3); }
  details.fold fieldset{ box-shadow:none; border:none; padding:0; background:transparent; }
  .foldCount{
    background:var(--tint); color:var(--accent-ink); border-radius:999px;
    padding:2px 10px; font-size:12px; font-weight:800;
  }
  input[type="range"]{ width:100%; accent-color:var(--sel-bg); }

  /* ================= player card ================= */
  #playerCardWrap{
    display:none; position:fixed; inset:0; z-index:14;
    background:var(--sheet-scrim); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
    align-items:flex-end; justify-content:center;
  }
  #playerCardWrap.show{ display:flex; }
  #playerCard{
    width:100%; max-width:430px; max-height:88dvh; overflow-y:auto;
    background:var(--bg); background-image:var(--bg-grad);
    border-radius:var(--r-lg) var(--r-lg) 0 0;
    padding:var(--sp-4) var(--sp-4) max(var(--sp-5), env(safe-area-inset-bottom));
    box-shadow:0 -12px 44px var(--sheet-shadow-color);
    animation:sheetUp .28s var(--ease);
    position:relative; overflow-x:hidden;
  }
  #playerCard::before{ /* court watermark, the brand stamp */
    content:""; position:absolute; inset:0; pointer-events:none; opacity:.5;
    background:
      linear-gradient(135deg, var(--tint) 0%, transparent 45%),
      linear-gradient(var(--line-soft) 0 0) top center / 64% 1.5px no-repeat,
      linear-gradient(var(--line-soft) 0 0) center / 1.5px 40% no-repeat;
  }
  .pcTop{ display:flex; align-items:center; gap:14px; position:relative; }
  .pcFace{
    width:74px; height:74px; border-radius:50%; flex:none;
    display:grid; place-items:center; font-weight:900; font-size:26px;
    background:var(--sub); border:2.5px solid var(--accent); box-shadow:var(--glow);
    overflow:hidden;
  }
  .pcFace img{ width:100%; height:100%; object-fit:cover; }
  .pcName{ flex:1; min-width:0; }
  .pcName b{ font-size:22px; font-weight:900; display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .pcName .hint{ font-size:12.5px; }
  .pcBrand{
    width:34px; height:34px; border-radius:10px; background:var(--accent); color:var(--on-accent);
    display:grid; place-items:center; flex:none; box-shadow:var(--glow);
  }
  .pcRating{
    display:flex; align-items:baseline; gap:10px; margin:14px 0 4px; position:relative;
  }
  .pcRating .num{
    font-family:'Anton', Impact, sans-serif; font-size:56px; line-height:1;
    color:var(--digit); text-shadow:var(--digit-glow);
  }
  .pcRating .lbl{ color:var(--muted); font-weight:700; font-size:13px; }
  .pcSection{ margin-top:14px; position:relative; }
  .pcSection .cardTitle{ margin-bottom:8px; }
  /* the safety row: block + report. Layout only — both controls inherit the base button
     pairing (--fg on --surface), which is the app's guaranteed-contrast pair in both themes,
     so nothing here names a colour. Wraps on a narrow phone rather than shrinking the tap
     target below --tap-min. */
  .pcSafety{
    display:flex; align-items:center; gap:8px; flex-wrap:wrap;
    margin-top:14px; position:relative;
  }
  .pcSafety button{ min-height:40px; padding:0 16px; font-size:13px; font-weight:800; }
  .pcSafety .hint{ flex:1 0 100%; margin:0; }
  .pcVs{
    border:1px solid var(--line-soft); border-radius:var(--r-md); padding:12px 14px;
    display:flex; align-items:center; gap:10px; background:var(--surface);
  }
  .pcVs .num{ font-family:'Anton', Impact, sans-serif; font-size:26px; }
  .pcClose{ position:absolute; top:10px; inset-inline-end:12px; z-index:2; min-height:36px; width:36px; padding:0; border-radius:10px; }

  /* ================= CENTER COURT layer ================= */
  /* RQS-01: the FAMILY comes from the token like every other design decision. Until
     2026-08-08 this rule named the stack literally and put "Heebo" FOURTH — behind three
     system faces that always win — so pointing --font-body at Heebo changed nothing that
     paints. Declared-but-not-used is the same failure the token guards exist to catch,
     and it was caught here by reading document.body's COMPUTED family in a browser
     rather than by reading the token. */
  body{ font-family:var(--font-body); }
  body::before{ /* iOS status-bar scrim: the clock never sits on app content */
    content:""; position:fixed; top:0; left:0; right:0; z-index:60;
    height:env(safe-area-inset-top, 0px); pointer-events:none;
    background:color-mix(in srgb, var(--bg) 80%, transparent);
    backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  }
  body::after{ /* broadcast film grain */
    content:""; position:fixed; inset:0; z-index:99; pointer-events:none; opacity:var(--grain-op, .05);
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  }
  .courtBg{ /* court geometry behind every screen */
    position:fixed; bottom:-3vh; left:50%; transform:translateX(-50%);
    width:min(620px, 130vw); height:auto; color:var(--fg); opacity:.055;
    z-index:0; pointer-events:none;
  }
  .screen{ position:relative; z-index:1; }

  /* every dropdown must LOOK like a dropdown: chalk border + permanent chevron
     (appearance:none removed the native arrow; this puts one back, theme-aware) */
  select.playerSelect, select.groupSelect{
    border:1.5px solid var(--line-soft);
    background-repeat:no-repeat;
    background-position:left 14px center;      /* RTL: the arrow sits at the inline end */
    background-size:17px;
    padding-inline-end:42px;                   /* text never runs under the arrow */
  }
  /* per-theme chevron duplicates removed: the `select` rule above uses
     --select-chevron, which each theme defines once. */

  /* ---- in-app notifications ----
     Shown when the app is OPEN, where a system notification is the wrong surface.
     Sits below the safe area and above everything except a modal, and never covers the
     bottom navigation or the live score. Token-driven so it re-skins with the theme. */
  #raqioNotifyHost{
    position:fixed; z-index:60;
    top:calc(env(safe-area-inset-top) + 8px);
    inset-inline:8px;
    display:flex; flex-direction:column; gap:8px;
    pointer-events:none;              /* the app stays usable behind the stack */
  }
  .raqioNotify{
    pointer-events:auto;
    display:flex; align-items:flex-start; gap:10px;
    padding:12px 14px; border-radius:var(--r-md, 14px);
    background:var(--surface); color:var(--fg);
    border:1px solid var(--line-soft);
    box-shadow:var(--shadow-md);
    opacity:0; transform:translateY(-10px);
    transition:opacity .22s var(--ease, ease), transform .22s var(--ease, ease);
    max-width:min(100%, 560px); margin-inline:auto; width:100%;
  }
  .raqioNotify.in{ opacity:1; transform:translateY(0); }
  .raqioNotify.out{ opacity:0; transform:translateY(-10px); }
  .raqioNotify.tappable{ cursor:pointer; }
  .raqioNotify.tappable:focus-visible{ outline:none; border-color:var(--ring); }
  .raqioNotify[data-kind="invite"]{
    border-inline-start:4px solid var(--accent);
  }
  .raqioNotifyText{ flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
  .raqioNotifyText b{ font-weight:800; font-size:14px; }
  /* A notification is other people's words: it wraps rather than truncating, because
     there is no shorter meaningful form of someone's message. */
  .raqioNotifyText span{
    font-size:13px; color:var(--muted); overflow-wrap:anywhere;
    display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical;
    overflow:hidden;
  }
  .raqioNotifyClose{
    flex:none; min-height:32px; min-width:32px; padding:0;
    border:none; background:transparent; color:var(--muted); cursor:pointer;
    box-shadow:none;
  }
  @media (prefers-reduced-motion: reduce){
    .raqioNotify{ transition:none; }
  }

  .ic{ width:22px; height:22px; stroke:currentColor; fill:none; stroke-width:1.8;
       stroke-linecap:round; stroke-linejoin:round; flex:none; }
  .ic.sm{ width:17px; height:17px; }

  /* Raqio wordmark: theme-aware pair, toggled purely by [data-theme] (no JS needed).
     "-dark" = the white logo file, shown ON dark ("night") backgrounds.
     "-light" = the navy logo file, shown ON light ("day") backgrounds. */
  .wordmark-dark, .wordmark-light{ display:none; }
  [data-theme="night"] .wordmark-dark{ display:block; }
  [data-theme="day"] .wordmark-light{ display:block; }

  .brandMark{ flex:none; display:flex; align-items:center; height:28px; }
  /* the horizontal "Raqio" wordmark (802x197), sized for the compact top bar.
     Which colour shows is handled by the .wordmark-dark/.wordmark-light theme toggle. */
  .brandMark img{ height:26px; width:auto; max-width:none; }
  /* full lockup (symbol + wordmark), uncropped: the login/welcome splash has the
     vertical room the compact header chip doesn't */
  .splashLogo{ height:88px; width:auto; margin:0 auto 2px; }
  .hero{ gap:var(--sp-3); justify-content:flex-start; }
  .hero .brandText{ flex:1; min-width:0; }
  .hero h1{ font-family:'Anton', Impact, sans-serif; font-size:23px; font-weight:400; letter-spacing:.06em; direction:ltr; text-align:right; line-height:1; }
  .themeBtn{ display:flex; align-items:center; justify-content:center; gap:8px; }

  /* ===== premium 4-tab bottom nav ===== */
  #bottomNav{
    /* FIXED to the visual-viewport bottom so it never detaches on scroll (the old
       sticky+100dvh combo left a gap when the mobile toolbar showed/hid). Capped to
       the #setup column width + centered so it stays full-bleed on phones and aligned
       on wide screens. screenIn's transform is only in the `from` keyframe, so at rest
       #setup has no transform and this stays viewport-relative. */
    position:fixed; inset-inline:0; bottom:0; z-index:7;
    max-width:520px; margin-inline:auto;
    display:flex; gap:4px;
    padding:6px var(--sp-2) calc(6px + env(safe-area-inset-bottom));
    background:color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
    border-top:1px solid var(--line-soft);
  }
  #bottomNav button{
    flex:1; position:relative;
    font-size:10.5px; font-weight:800; padding:7px 1px 6px; min-height:52px;
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
    color:var(--muted); background:transparent; border:none; box-shadow:none; border-radius:var(--r-sm);
  }
  #bottomNav button[aria-pressed="true"]{ background:var(--tint); color:var(--accent-ink); box-shadow:none; }
  #bottomNav button.hasBadge::after{
    content:""; position:absolute; top:8px; inset-inline-end:calc(50% - 20px);
    width:9px; height:9px; border-radius:50%; background:var(--danger);
    box-shadow:0 0 0 2px var(--bg);
  }

  /* ===== pull-to-refresh indicator (pull-refresh.js) =====
     Sits ABOVE the sticky #subNav (z-index 5) and BELOW the fixed #bottomNav (z-index 7), or
     it renders behind the sub-nav on community/profile. The spinner is drawn on its own
     --surface scrim rather than directly on the page background, so its ring colour
     (--sport-accent-ink, the same guaranteed-contrast pairing used everywhere else on --surface
     and --tint) always clears 3:1 regardless of what is scrolled underneath it. */
  #pullRefresh{
    position:fixed; inset-inline:0; top:calc(env(safe-area-inset-top, 0px));
    display:flex; justify-content:center; pointer-events:none;
    z-index:6;
    transform:translateY(-56px); opacity:0;
    transition:opacity .15s var(--ease);
  }
  #pullRefresh.active{ opacity:1; }
  .pullRefreshSpinner{
    --p:0;
    width:34px; height:34px; margin-top:10px; border-radius:50%;
    background:var(--surface); box-shadow:var(--shadow-sm);
    border:2.5px solid var(--line);
    transform:scale(.72);
  }
  /* Owner ruling 2026-08-07: nothing Apple appears in the Android app. Not the Sign in with
     Apple button, not the account-linking field, not the Apple Watch controller or its
     settings, not the iOS add-to-home guide, and not the one sentence inside the push hint
     that explains pinning to the home screen on an iPhone — that sentence is carved into its
     own span so the rest of the hint, which is true everywhere, survives.

     `!important` is not decoration. Three of these carry an author `display` rule of their own
     (.controllerChoice is a grid, #appleBtn an inline-flex), and this file already documents
     twice that an author display outranks the browser's [hidden]{display:none} — which is how
     "hidden" has silently failed here before. tests/android-no-apple.test.js reads
     getComputedStyle in a real browser rather than trusting the class, and its census fails the
     day an eighth Apple surface appears unmarked.

     The platform attribute is written once in app.js and is 'android' or 'ios', so the web —
     where Sign in with Apple genuinely works — falls on the showing side. */
  html[data-platform="android"] [data-apple-only]{ display:none !important; }
  html[data-platform="ios"] .pullRefreshSpinner{
    /* an arc that fills with pull travel: a conic-gradient ring clipped to a border, painted
       over the same neutral --surface scrim so the ring colour never sits directly on a
       variable band colour. */
    border-color:transparent;
    background:
      linear-gradient(var(--surface), var(--surface)) content-box,
      conic-gradient(var(--sport-accent-ink, var(--accent-ink)) calc(var(--p, 0) * 360deg), var(--line) 0) border-box;
  }
  html[data-platform="android"] .pullRefreshSpinner{
    /* Material circular indicator: a plain bordered circle, no travel-fill — it only spins,
       on release, via #pullRefresh.busy below. */
    border-top-color:var(--sport-accent-ink, var(--accent-ink));
    border-inline-end-color:var(--sport-accent-ink, var(--accent-ink));
  }
  #pullRefresh.busy .pullRefreshSpinner{
    transform:scale(1);
    animation:pullRefreshSpin .8s linear infinite;
  }
  @keyframes pullRefreshSpin{ to{ transform:rotate(360deg); } }

  @media (min-width:760px){
    #setup.appShell.screen.active{
      display:grid;
      grid-template-columns:112px minmax(0, 760px);
      grid-template-areas:
        "nav hero"
        "nav sport"
        "nav main"
        "nav start";
      align-content:start; align-items:start; justify-content:center;
      column-gap:var(--sp-6); row-gap:var(--sp-4);
      max-width:1040px; min-height:100dvh;
      padding:max(var(--sp-6), env(safe-area-inset-top)) var(--sp-6) var(--sp-6);
    }
    #setup > .hero{ grid-area:hero; width:100%; }
    #setup > #sportContextSwitch{ grid-area:sport; width:100%; }
    #setup > #appMain{ grid-area:main; width:100%; max-width:760px; }
    #setup > #startBar{ grid-area:start; width:100%; bottom:var(--sp-3); }
    #bottomNav.appNav{
      grid-area:nav; position:sticky; top:max(var(--sp-6), env(safe-area-inset-top));
      inset-inline:auto; bottom:auto; z-index:7; align-self:start;
      width:112px; max-width:none; margin:0; padding:var(--sp-2);
      display:flex; flex-direction:column; gap:var(--sp-1);
      background:color-mix(in srgb, var(--surface) 88%, transparent);
      border:1px solid var(--line-soft); border-radius:var(--r-md);
      box-shadow:var(--shadow-sm);
    }
    #bottomNav.appNav button{
      flex:none; width:100%; min-height:52px; padding:var(--sp-2);
      flex-direction:row; justify-content:flex-start; gap:var(--sp-2);
      font-size:13px; text-align:start;
    }
    #bottomNav.appNav button.hasBadge::after{
      top:7px; inset-inline-end:7px;
    }
    #subNav.appSubnav{ top:var(--sp-2); }
  }
  /* sub-nav dot: a lit primary tab points to the exact sub-tab that owns the update */
  #subNav button{ position:relative; }
  #subNav button.hasBadge::after{
    content:""; position:absolute; top:5px; inset-inline-end:7px;
    width:8px; height:8px; border-radius:50%; background:var(--danger);
    box-shadow:0 0 0 2px var(--sub);
  }
  /* labels fit down to 320px; only on ultra-narrow screens do we drop to
     icon-only so nothing clips (the icon keeps its own explicit size) */
  @media (max-width: 319px){
    #bottomNav button{ font-size:0; gap:0; padding:10px 1px; }
    #bottomNav button .ic{ width:23px; height:23px; }
  }

  /* ---- guided first-run tour: coachmarks over the bottom nav -------------
     Scrim + a spotlight ring cut over the active tab + an explainer card.
     Both themes are token-driven; card text sits on the opaque --surface so
     contrast never depends on the scrim underneath. Safe-area aware. */
  #tourWrap{ position:fixed; inset:0; z-index:70; display:block; }
  #tourWrap[hidden]{ display:none; }
  #tourSpot{
    position:fixed; border-radius:var(--r-md); pointer-events:none;
    /* the huge outward shadow IS the scrim — a real cut-out around the tab;
       the tight accent ring (decorative) hugs the spotlighted button */
    box-shadow: 0 0 0 3px var(--accent), 0 0 0 6px var(--surface), 0 0 0 9999px var(--scrim);
    transition: top .28s var(--ease), left .28s var(--ease), width .28s var(--ease), height .28s var(--ease);
  }
  #tourCard{
    position:fixed; z-index:71;
    left:max(var(--sp-4), env(safe-area-inset-left)); right:max(var(--sp-4), env(safe-area-inset-right));
    bottom:calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--sp-4));
    max-width:440px; margin-inline:auto;
    background:var(--surface); border:1px solid var(--line); border-radius:var(--r-lg);
    box-shadow:var(--shadow-md); padding:var(--sp-4);
    animation:sheetUp .28s var(--ease);
  }
  .tourTop{ display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3); margin-bottom:var(--sp-2); }
  .tourStepNum{ font-size:13px; font-weight:800; color:var(--muted); letter-spacing:.04em; }
  #tourSkip{ min-height:34px; padding:4px 14px; font-size:13px; font-weight:700; color:var(--muted); background:transparent; border:1px solid var(--line); border-radius:999px; box-shadow:none; }
  #tourText{ font-size:15px; font-weight:600; line-height:1.5; color:var(--fg); margin:0; }
  .tourDots{ display:flex; gap:6px; justify-content:center; margin:var(--sp-3) 0; }
  .tourDot{ width:7px; height:7px; border-radius:50%; background:var(--line); transition:width .2s var(--ease), background-color .2s var(--ease); }
  .tourDot.on{ width:20px; border-radius:999px; background:var(--accent-ink); }
  #tourNext{ width:100%; min-height:46px; }

  /* The first-match guide consumes the global sport choice and explains only
     the valid setup for that sport. Mobile uses a calm bottom sheet; desktop
     centers the same component without a second DOM or interaction model. */
  #firstMatchWrap{
    position:fixed; inset:0; z-index:72; display:flex; align-items:flex-end;
    justify-content:center; padding:
      var(--sp-4)
      max(var(--sp-4), env(safe-area-inset-right))
      max(var(--sp-4), env(safe-area-inset-bottom))
      max(var(--sp-4), env(safe-area-inset-left));
    background:var(--scrim);
  }
  #firstMatchWrap[hidden]{ display:none; }
  .firstMatchCard{
    width:min(100%, 440px); padding:var(--sp-4);
    background:var(--surface); color:var(--fg);
    border:1px solid var(--line); border-radius:var(--r-lg);
    box-shadow:var(--shadow-md); animation:sheetUp .28s var(--ease);
  }
  .firstMatchTop{
    display:flex; align-items:center; justify-content:space-between;
    gap:var(--sp-3); margin-bottom:var(--sp-3);
  }
  #firstMatchSkip{
    min-height:36px; padding:4px 14px; border-radius:999px;
    color:var(--muted); background:transparent; border:1px solid var(--line);
    box-shadow:none; font-size:13px; font-weight:700;
  }
  .firstMatchSport{
    width:max-content; display:inline-flex; align-items:center; gap:6px;
    color:var(--sport-accent-ink); background:var(--sport-tint);
    border-color:var(--sport-accent);
  }
  #firstMatchTitle{ margin:var(--sp-3) 0 var(--sp-2); font-size:22px; line-height:1.25; }
  #firstMatchBody{ min-height:4.5em; margin:0; color:var(--muted); font-size:15px; line-height:1.55; }
  .firstMatchProgress{
    display:flex; justify-content:center; gap:6px; margin:var(--sp-4) 0 var(--sp-3);
  }
  .firstMatchDot{
    width:7px; height:7px; border-radius:50%; background:var(--line);
    transition:width .2s var(--ease), background-color .2s var(--ease);
  }
  .firstMatchDot.on{ width:20px; border-radius:999px; background:var(--sport-accent-ink); }
  #firstMatchNext{ width:100%; min-height:48px; }
  @media (min-width:760px){
    #firstMatchWrap{ align-items:center; }
  }

  fieldset{ border:1px solid var(--line-soft); }
  button.primary, .learnRow button.learning{
    background:var(--acc-grad); color:var(--on-accent); border-color:transparent; font-weight:800;
    box-shadow:var(--shadow-md), var(--acc-glow), inset 0 1px 0 var(--bevel-hi);
  }
  button.primary:active, .learnRow button.learning:active{ box-shadow:var(--shadow-sm), inset 0 1px 0 var(--bevel-hi-pressed); }
  .icChip{
    width:36px; height:36px; border-radius:11px; background:var(--tint); color:var(--accent-ink);
    display:grid; place-items:center; flex:none;
  }
  .icChip .ic{ width:20px; height:20px; }
  .tiles{ display:grid; grid-template-columns:repeat(3, 1fr); gap:var(--sp-3); }
  .tile{
    background:var(--surface); border:1px solid var(--line-soft); border-radius:var(--r-md);
    padding:16px 10px; text-align:center; box-shadow:var(--shadow-sm);
  }
  .tile .v{
    font-family:'Anton', Impact, sans-serif; font-size:29px; line-height:1.05;
    font-variant-numeric:tabular-nums; white-space:nowrap;
  }
  .tile .k{ font-size:11.5px; color:var(--muted); font-weight:700; margin-top:4px; }

  .panel{
    background:linear-gradient(175deg, var(--surface-2, var(--surface)), var(--surface));
    border:1px solid var(--line-soft);
  }
  .panel.serving{ border-color:var(--accent); }
  .serveDot{ animation:servePulse 2s ease-in-out infinite; }
  @keyframes servePulse{ 0%,100%{ transform:scale(1); opacity:1; } 50%{ transform:scale(1.18); opacity:.8; } }
  #announceCard{ border:1px solid var(--line-soft); }

  .rankChip{
    display:inline-grid; place-items:center; width:26px; height:26px; border-radius:8px;
    background:var(--sub); color:var(--muted); font-weight:800; font-size:13px;
  }
  .rankChip.gold{ background:var(--accent); color:var(--on-accent);
    box-shadow:var(--glow), inset 0 0 0 1px var(--accent-edge); }
  .stt td.winCol{ color:var(--accent-ink); }
  .stt td.lossCol{ color:var(--muted); }
  /* recent form, newest on the right (last five results) */
  .formDots{ display:inline-flex; gap:3px; direction:ltr; }
  .formDots i{ width:8px; height:8px; border-radius:50%; background:var(--sub); }
  .formDots i.w{ background:var(--accent-edge); }
  .formDots i.l{ background:transparent; box-shadow:inset 0 0 0 1.5px var(--muted); }
  /* your own row, called out so you find yourself at a glance */
  .stt tr.me td{ background:var(--tint); }
  .stt tr.me td.pname-cell{ font-weight:900; }
  .stt tr.me td:first-child{ border-start-start-radius:8px; border-end-start-radius:8px; }
  .stt tr.me td:last-child{ border-start-end-radius:8px; border-end-end-radius:8px; }
  .meTag{ color:var(--accent-ink); font-weight:800; font-size:11px; margin-inline-start:5px; }

  /* followers / following / kudos list sheet (opens over the player card) */
  #listSheetWrap{ display:none; position:fixed; inset:0; z-index:16; background:var(--sheet-scrim);
    backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); align-items:flex-end; justify-content:center; }
  #listSheetWrap.show{ display:flex; }
  #listSheet{ width:100%; max-width:430px; max-height:82dvh; overflow-y:auto; position:relative;
    background:var(--bg); background-image:var(--bg-grad); border-radius:var(--r-lg) var(--r-lg) 0 0;
    padding:var(--sp-4) var(--sp-4) max(var(--sp-5), env(safe-area-inset-bottom));
    box-shadow:0 -12px 44px var(--sheet-shadow-color); animation:sheetUp .28s var(--ease); }
  .listRow{ display:flex; align-items:center; gap:10px; padding:10px 0; border-bottom:1px dashed var(--line-soft); }
  .listRow .who{ flex:1; min-width:0; }
  .listRow .who b{ font-weight:800; display:block; }
  .listRow .who .hint{ font-size:12px; }
  .listRow button{ min-height:34px; padding:0 14px; font-size:12.5px; flex:none; }

  /* ===== chat ===== */
  .chatRow{ display:flex; align-items:center; gap:10px; padding:10px 12px; background:var(--surface); border:1px solid var(--line-soft); border-radius:var(--r-md); box-shadow:var(--shadow-sm); cursor:pointer; }
  .chatRow:active{ transform:scale(.99); }
  .chatRow .avatarDot{ width:42px; height:42px; }
  .chatRow .cmeta{ flex:1; min-width:0; }
  .chatRow .cnm{ font-weight:800; font-size:14px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .chatRow .clast{ font-size:12.5px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .chatRow .cwhen{ font-size:11px; color:var(--muted); flex:none; display:flex; flex-direction:column; align-items:flex-end; gap:5px; }
  /* per-thread unread pip — the same danger dot as the nav badges, ringed on the
     opaque row surface so it clears contrast in both themes */
  .chatRow .cunread{ width:9px; height:9px; border-radius:50%; background:var(--danger); box-shadow:0 0 0 2px var(--surface); }
  .chatRow.hasUnread .cnm{ font-weight:900; }
  /* "load earlier" control, centered at the top of the thread */
  #chatMessages .chatMore{ align-self:center; margin:2px auto 6px; padding:7px 16px; font-size:12.5px; font-weight:800;
    color:var(--accent-ink); background:var(--sub); border:1px solid var(--line-soft); border-radius:999px; box-shadow:none; }
  #chatMessages .chatMore:disabled{ opacity:.6; }
  .chatKebab{ flex:none; min-height:34px; min-width:34px; padding:0; border-radius:9px; background:transparent; border:none; color:var(--muted); font-size:20px; line-height:1; }
  .chatKebab:active{ background:var(--sub); }
  .rowActions{ display:flex; gap:6px; flex:none; }
  .rowActions button{ min-height:34px; padding:0 12px; font-size:12.5px; font-weight:800; border-radius:9px; background:var(--sub); border:1px solid var(--line-soft); color:var(--fg); }
  .rowActions button.del{ color:var(--danger); border-color:color-mix(in srgb, var(--danger) 40%, transparent); }
  #chatInput{
    flex:1; min-width:0; font:inherit; font-size:15px; color:var(--fg); background:var(--sub);
    border:1.5px solid transparent; border-radius:var(--r-sm); resize:none;
    min-height:48px; max-height:132px; padding:12px 14px; line-height:1.4;
    user-select:text; -webkit-user-select:text; transition:border-color .2s, background-color .2s;
  }
  #chatInput:focus-visible{ outline:none; border-color:var(--ring); background:var(--surface); }
  .chatMsg{ max-width:82%; padding:9px 13px; border-radius:15px; font-size:15px; line-height:1.45; word-break:break-word; white-space:pre-wrap; }
  .chatMsg.mine{ align-self:flex-end; background:var(--accent); color:var(--on-accent); border-end-end-radius:5px; }
  .chatMsg.theirs{ align-self:flex-start; background:var(--sub); color:var(--fg); border-end-start-radius:5px; }
  .chatMsg .who{ font-size:11px; font-weight:700; opacity:.8; margin-bottom:2px; }
  .chatMsg .cwhen{ font-size:10px; opacity:.65; margin-top:3px; text-align:end; }
  /* report a message (App Store Review Guideline 1.2). It sits INSIDE a .theirs bubble, whose
     surface is --sub, and it draws in --muted: re-measured under RQS-01 at 5.28:1 on day and
     5.62:1 on night — lower than the 6.20/8.11 the old palette gave, and still clear of AA for
     both text and graphics in both
     themes, which is why no opacity is applied to quiet it down. It is made unobtrusive by
     size and placement instead. tests/block-report-ui.test.js re-measures this against the
     LIVE computed style in both themes, so a token change cannot loosen it silently. */
  .chatMsg .chatReport{
    display:block; margin-top:2px; margin-inline-start:auto;
    min-height:28px; width:28px; padding:0;
    background:transparent; border:none; box-shadow:none; color:var(--muted);
  }
  .chatMsg .chatReport:hover, .chatMsg .chatReport:focus-visible{ color:var(--fg); }

  /* ===== shared avatar dot (photo or initials), used across board/groups/notifs ===== */
  .avatarDot{ width:34px; height:34px; border-radius:50%; flex:none; display:grid; place-items:center;
    background:var(--tint); color:var(--accent-ink); font-weight:900; font-size:13px;
    border:1.5px solid var(--accent); overflow:hidden; }
  .avatarDot.sm{ width:26px; height:26px; font-size:11px; border-width:1px; }
  .avatarDot img{ width:100%; height:100%; object-fit:cover; }

  /* ===== board game cards ===== */
  #gamesList fieldset.gameCard{ gap:10px; margin-bottom:12px; }
  .gcHead{ display:flex; align-items:center; gap:10px; }
  .gcHead .gcTitle{ font-weight:900; font-size:15px; flex:1; }
  .datePill{ background:var(--sub); color:var(--fg); border-radius:999px; padding:4px 12px; font-size:12.5px; font-weight:800; white-space:nowrap; }
  .datePill.soon{ background:var(--tint); color:var(--accent-ink); }
  .venueLine{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; font-size:13.5px; }
  .venueLine .city{ color:var(--muted); font-weight:600; }
  .stateChip{ font-size:11px; font-weight:800; border-radius:999px; padding:3px 10px; margin-inline-start:auto; white-space:nowrap; }
  .stateChip.ok{ background:var(--tint); color:var(--accent-ink); }
  .stateChip.no{ background:var(--tint-clay); color:var(--clay-ink); }
  .creatorRow{ display:flex; align-items:center; gap:8px; }
  .creatorRow .who{ flex:1; font-size:13px; min-width:0; }
  .creatorRow .who b{ font-weight:800; }
  .creatorRow .cap{ font-weight:900; font-size:15px; }
  .creatorRow .cap span.k{ font-size:11px; color:var(--muted); font-weight:700; margin-inline-start:3px; }
  .playerChips{ display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
  .pChip{ display:flex; align-items:center; gap:6px; background:var(--sub); border-radius:999px; padding:3px 12px 3px 5px; font-size:13px; font-weight:700; cursor:pointer; }
  .pChip .rt{ color:var(--muted); font-weight:800; }
  .slotChip{ display:flex; align-items:center; gap:6px; border:1.5px dashed var(--line-soft); border-radius:999px; padding:4px 12px; font-size:12.5px; font-weight:700; color:var(--muted); }
  .tag{ font-size:10.5px; font-weight:700; padding:3px 9px 3px 8px; border-radius:99px; white-space:nowrap; display:inline-flex; align-items:center; gap:4px; }
  .tag.tennis{ background:var(--tint); color:var(--accent-ink); }
  .tag.padel{ background:var(--tint-clay); color:var(--clay-ink); }
  .badge{ font-size:10px; font-weight:800; padding:4px 9px; border-radius:99px; letter-spacing:.03em; display:inline-flex; align-items:center; gap:5px; }
  .badge.tennis{ background:var(--accent); color:var(--on-accent); }
  .badge.padel{ background:var(--clay); color:var(--on-clay); }
  .alertStrip{ background:var(--tint); color:var(--accent-ink); border-radius:var(--r-sm); padding:8px 12px; font-weight:800; font-size:13.5px; display:flex; align-items:center; gap:8px; }
  .alertStrip .ic{ width:17px; height:17px; }
  .gcFoot{ display:flex; align-items:center; gap:12px; flex-wrap:wrap; border-top:1px dashed var(--line-soft); padding-top:9px; font-size:12.5px; color:var(--muted); font-weight:700; }
  .gcFoot .fi{ display:flex; align-items:center; gap:5px; }
  .gcFoot .ic{ width:15px; height:15px; }

  /* ===== my-stats greeting + highlighted hero tile ===== */
  .greet{ display:flex; align-items:center; gap:12px; }
  .greet .g-name{ font-size:18px; font-weight:900; line-height:1.1; }
  .greet .g-sub{ font-size:13px; color:var(--muted); font-weight:600; margin-top:2px; }
  .tile.hl{ background:linear-gradient(160deg, var(--tint), var(--surface));
    border-color:var(--accent-edge); }
  .tile.hl .v{ color:var(--digit); text-shadow:var(--digit-glow); }

  /* ===== premium profile hero (Task 3): context badge · big Elo · 30-day delta · chart ===== */
  .pHero{ position:relative; overflow:hidden; background:linear-gradient(160deg, var(--surface-2), var(--surface));
    border:1px solid var(--line); border-radius:var(--r-lg); padding:16px 16px 10px; box-shadow:var(--glow); }
  .pBadge{ display:inline-flex; align-items:center; gap:6px; background:var(--tint); color:var(--accent-ink);
    font-size:11.5px; font-weight:800; padding:5px 11px; border-radius:999px; }
  .pBadge .ic{ width:14px; height:14px; }
  .pNum{ font-size:52px; font-weight:900; line-height:1; letter-spacing:-.02em; color:var(--fg); margin:10px 0 3px; }
  .pDeltaLine{ display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
  .pDelta{ font-weight:800; font-size:14px; font-variant-numeric:tabular-nums; direction:ltr; unicode-bidi:isolate; }
  .pSub{ color:var(--faint); font-size:12px; }
  .pChart{ margin-top:8px; }
  /* stat trio: win% ring · matches · streak */
  .pTrio{ display:flex; gap:9px; }
  .pStat{ flex:1; min-width:0; background:var(--surface-2); border:1px solid var(--line-soft); border-radius:var(--r-md);
    padding:12px 8px; text-align:center;
    display:flex; flex-direction:column; align-items:center; justify-content:center; }
  .pStatK{ font-size:10.5px; color:var(--faint); font-weight:700; margin-top:5px; }
  .pStatV{ font-size:22px; font-weight:900; color:var(--fg); }
  .pRing{ width:52px; height:52px; display:block; margin:0 auto; }
  .pStreak{ display:inline-flex; align-items:center; gap:5px; justify-content:center;
    color:var(--accent-ink); font-weight:900; font-size:20px; line-height:1; }
  .pStreak .ic{ width:20px; height:20px; }
  /* recent form: last 5 W/L dots (win --graph bg, loss --clay border) */
  .pFormWrap{ display:flex; flex-direction:column; gap:8px; }
  .pLbl{ font-size:11px; color:var(--faint); font-weight:700; text-align:center; }
  .pForm{ display:flex; align-items:center; gap:6px; justify-content:center; direction:ltr; }
  .pDot{ width:26px; height:26px; border-radius:var(--r-xs); display:grid; place-items:center; font-size:11px; font-weight:900; }
  .pDot.w{ background:var(--graph); color:var(--on-accent); }
  .pDot.l{ background:var(--tint-clay); color:var(--clay-ink); border:1px solid var(--clay-edge); }
  /* ===== per-combo cards (Task 4): the OTHER combos below the hero ===== */
  .combos{ display:flex; flex-direction:column; gap:9px; }
  .combo{ display:flex; align-items:center; gap:11px; background:var(--surface-2);
    border:1px solid var(--line-soft); border-radius:var(--r-md); padding:11px 13px; }
  .combo.padel{ border-color:var(--tint-clay); }
  .combo .cc{ width:34px; height:34px; border-radius:var(--r-sm); display:grid; place-items:center; flex-shrink:0; }
  .combo .cc .ic{ width:19px; height:19px; }
  .combo.tennis .cc{ background:var(--tint); color:var(--accent-ink); }
  .combo.padel .cc{ background:var(--tint-clay); color:var(--clay-ink); }
  .combo .ci{ flex:1; min-width:0; }
  .combo .ci b{ font-size:13.5px; font-weight:800; display:block; color:var(--fg); }
  .combo .cMeta{ display:block; font-size:11.5px; color:var(--muted); margin-top:2px; }
  .combo .cSpark{ flex-shrink:0; width:58px; height:22px; display:block; }
  .combo .cv{ font-size:19px; font-weight:900; flex-shrink:0; }
  .combo.tennis .cv{ color:var(--accent-ink); }
  .combo.padel .cv{ color:var(--clay-ink); }
  /* ===== profile-completion card (Task 5): canonical 3-row "N מתוך 3" ===== */
  .completion{ background:linear-gradient(160deg, var(--tint), transparent 70%), var(--surface-2);
    border:1px solid var(--line); border-radius:var(--r-md); padding:12px 13px; }
  .completion .ch{ display:flex; justify-content:space-between; align-items:center;
    font-size:12.5px; font-weight:800; color:var(--fg); margin-bottom:8px; }
  .completion .ch b{ color:var(--accent-ink); font-variant-numeric:tabular-nums; }
  .completion .cbar{ height:7px; border-radius:99px; background:var(--sub); overflow:hidden; margin-bottom:10px; }
  .completion .cbar > i{ display:block; height:100%; border-radius:99px;
    background:linear-gradient(90deg, var(--graph), var(--accent-ink)); }
  .completion .citem{ display:flex; align-items:center; gap:9px; padding:6px 0; font-size:13px; color:var(--fg); }
  .completion .citem.ok{ color:var(--faint); }
  .completion .cdot{ width:9px; height:9px; border-radius:50%; flex-shrink:0; }
  .completion .cdot.done{ background:var(--graph); }
  .completion .cdot.todo{ background:var(--danger); box-shadow:0 0 0 3px color-mix(in srgb, var(--danger) 16%, transparent); }
  .completion .go{ margin-inline-start:auto; padding:0; border:0; background:none; cursor:pointer;
    box-shadow:none; min-height:0; line-height:1;
    color:var(--accent-ink); font-weight:800; font-size:12px; font-family:inherit; }
  /* ===== premium league podium (Task 6): champion-lime / silver / bronze top-3 ===== */
  /* champion is fully tokenised (lime is a theme token); silver + bronze use metallic
     literals — allowed decorative FILLS only, never on legible text. The name, rating
     and the big rank numeral are guaranteed-contrast --fg, AA in both themes. */
  .podium{ display:flex; align-items:flex-end; justify-content:center; gap:5px; margin:8px 0 16px; }
  .pcol{ display:flex; flex-direction:column; align-items:center; gap:7px; flex:1; min-width:0; }
  .pcol .pav{ width:52px; height:52px; border-radius:50%; background:var(--sub); display:grid; place-items:center;
    font-weight:900; font-size:18px; color:var(--fg); border:2.5px solid var(--line); position:relative; z-index:1; }
  .pcol .pav img{ width:100%; height:100%; border-radius:50%; object-fit:cover; }
  .pcol.first .pav{ width:66px; height:66px; box-shadow:var(--glow); }
  .pcol.first .pbar{ box-shadow:0 -6px 26px -12px var(--accent); }
  /* A PERSON'S NAME IS NOT CHROME, AND ON THE PODIUM IT IS THE WHOLE COLUMN.
     This rule was nowrap+ellipsis and rendered "אלכסנדר רוזנ…" for the player in FIRST PLACE
     on the owner's own geometry (320 CSS px; the name needed 88px in an 81px column, and
     "מיכאלה בן-ארצי" 85 in 81). tests/no-truncated-labels.test.js deliberately lets a player's
     name ellipsize elsewhere — in a table row or a match line a name sits beside columns that
     have a claim on the width, and its first word still identifies the person. The podium is
     the opposite case: three fixed columns whose entire content IS one name, one rating and a
     rank. So it WRAPS. There is room to — a 320px column is ~103px and both words fit on their
     own line — and `overflow-wrap:anywhere` means even a single unbreakable word stays inside
     the column instead of spilling out of it. `.podium{align-items:flex-end}` already aligns
     the columns by their bars, so a two-line name grows the column upward and the three bars
     stay level. */
  .pcol .pn{ font-size:12px; font-weight:800; color:var(--fg); max-width:100%;
    white-space:normal; overflow-wrap:anywhere; text-overflow:clip; overflow:visible;
    line-height:1.35; text-align:center; }
  .pcol .pr{ font-size:14px; font-weight:900; color:var(--fg); }
  .pbar{ width:100%; border-radius:13px 13px 0 0; border:1px solid var(--line); border-bottom:0;
    display:grid; place-items:center; font-weight:900; position:relative; overflow:hidden; }
  .pbar::before{ content:''; position:absolute; inset:0 0 auto 0; height:3px; background:var(--pc);
    border-radius:13px 13px 0 0; box-shadow:0 0 10px var(--pc); }
  .prank{ font-size:34px; font-weight:900; line-height:1; font-variant-numeric:tabular-nums;
    color:var(--fg); opacity:.92; }
  .medal{ position:absolute; bottom:-6px; left:50%; transform:translateX(-50%); width:23px; height:23px;
    border-radius:50%; display:grid; place-items:center; font-size:11px; font-weight:900;
    box-shadow:0 2px 7px var(--shadow-badge-color); border:1.5px solid var(--bevel-hi-strong); }
  /* secondary period control: a smaller, quieter segment below the podium/table */
  .segLabel{ font-size:11px; color:var(--faint); font-weight:700; margin:12px 2px 6px; }
  .seg.segSm{ margin-bottom:6px; }
  .seg.segSm button{ min-height:38px; font-size:12px; }
  /* full-statistics disclosure — demoted legacy sections, below the premium fold */
  .fullStats{ border:1px solid var(--line-soft); border-radius:var(--r-md); background:var(--surface); padding:0 14px; }
  .fullStats > summary{ list-style:none; cursor:pointer; padding:13px 2px; font-weight:800; font-size:14px; color:var(--fg);
    display:flex; align-items:center; justify-content:space-between; }
  .fullStats > summary::-webkit-details-marker{ display:none; }
  .fullStats > summary::after{ content:'\25BE'; color:var(--faint); font-size:12px; transition:transform .2s; }
  .fullStats[open] > summary::after{ transform:rotate(180deg); }
  .fullStats[open] > summary{ border-bottom:1px solid var(--line-soft); margin-bottom:12px; }
  .fullStats fieldset{ border:none; box-shadow:none; background:transparent; padding:6px 0; }
  .fullStats > *:last-child{ margin-bottom:12px; }

  /* ===== matchmaking suggestions (level-matched opponents) ===== */
  .suggStrip{ display:flex; gap:10px; overflow-x:auto; padding:2px 2px 8px; scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
  .suggCard{ flex:none; width:148px; scroll-snap-align:start; background:var(--surface); border:1px solid var(--line-soft);
    border-radius:var(--r-md); padding:14px 12px; text-align:center; box-shadow:var(--shadow-sm); cursor:pointer;
    display:flex; flex-direction:column; align-items:center; gap:6px; }
  .suggCard .avatarDot{ width:48px; height:48px; font-size:17px; }
  .suggCard .snm{ font-weight:800; font-size:14px; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .suggCard .srate{ font-family:'Anton', Impact, sans-serif; font-size:23px; line-height:1; color:var(--digit); text-shadow:var(--digit-glow); }
  .fitPill{ font-size:11px; font-weight:800; border-radius:999px; padding:2px 10px; }
  .fitPill.great{ background:var(--tint); color:var(--accent-ink); }
  .fitPill.near{ background:var(--sub); color:var(--muted); }
  .fitPill.chal{ background:var(--tint-clay); color:var(--clay-ink); }
  .modeChip{
    display:inline-flex; align-items:center; gap:8px; background:var(--tint); color:var(--accent-ink);
    border-radius:999px; padding:8px 15px; font-size:13px; font-weight:700;
  }
  .modeChip.clay{ background:var(--tint-clay); color:var(--clay-ink); }
  .formatLock{
    display:flex; align-items:center; gap:8px; font-size:12px; color:var(--clay-ink);
    background:var(--tint-clay); border-radius:var(--r-sm); padding:9px 11px; margin-top:9px; line-height:1.4;
  }
  .suggCard .sgroup{ font-size:11px; color:var(--muted); font-weight:700; }

  .trophyChip{
    width:84px; height:84px; border-radius:50%; background:var(--tint); color:var(--accent-ink);
    display:grid; place-items:center;
  }
  .trophyChip .ic{ width:44px; height:44px; }
  #startBtn{ display:flex; align-items:center; justify-content:center; gap:10px; }
  #actions .ic{ width:21px; height:21px; }
  #actions button, #moreMenu button{ display:flex; align-items:center; justify-content:center; gap:8px; }
  #moreMenu button{ justify-content:flex-start; }

/* ── phone verification (019 OTP) ─────────────────────────────────────────
   token-only; the verified badge uses --accent-ink, which is guaranteed
   legible on the form surface in BOTH themes (dark-olive / bright volt). */
.phoneVerifyRow{ display:flex; flex-wrap:wrap; gap:var(--sp-2); align-items:center; margin-top:var(--sp-2); }
.pfPhoneState{ margin:0; }
.pfPhoneState.ok{ color:var(--accent-ink); font-weight:800; }
.pfPhoneState.warn{ color:var(--muted); }
.otpRow{ margin-top:var(--sp-3); }
.otpCode{ letter-spacing:.35em; text-align:center; font-variant-numeric:tabular-nums;
  font-size:1.2rem; font-weight:800; }
#pfOtpMsg{ margin-top:var(--sp-2); }
#pfOtpMsg.err{ color:var(--danger); }
#pfOtpMsg.ok{ color:var(--accent-ink); font-weight:800; }

/* ── tab / field "needs completing" dots ─────────────────────────────────
   Tab dots reuse #bottomNav .hasBadge::after. A missing profile field gets a small
   red dot after its label (both <label> and the gender <span class=flabel>). */
.field.needsFill > label::after,
.field.needsFill > .flabel::after{
  content:""; display:inline-block; width:7px; height:7px; margin-inline-start:6px;
  border-radius:50%; background:var(--danger); vertical-align:middle;
}

/* kudos = a warm flame that reads as "on fire", not the accent green */
#kudosBtn .ic{ color:var(--kudos-flame); }

/* ── league hero: your rating, rank, streak (energetic accent on the calm base) ── */
.lgHero{ border-radius:var(--r-md); padding:16px; position:relative; overflow:hidden;
  background:linear-gradient(150deg, var(--tint), var(--surface) 72%);
  box-shadow:inset 0 0 0 1px var(--line), inset 0 1px 0 var(--hair); }
.lgHero::after{ content:""; position:absolute; inset:0; pointer-events:none; background:var(--glass); }
.lgHero .hRow{ display:flex; align-items:center; justify-content:space-between; position:relative; }
.lgHero .hLbl{ font-size:12px; color:var(--muted); }
.lgHero .hRate{ font-size:26px; font-weight:900; letter-spacing:-.02em; color:var(--fg); display:flex; align-items:center; gap:9px; }
.lgHero .hRank{ font-family:'Anton'; font-size:30px; color:var(--accent-ink); line-height:1; }
.lgHero .hChips{ display:flex; gap:8px; margin-top:12px; flex-wrap:wrap; position:relative; }
.lgHero .hChip{ font-size:11.5px; font-weight:800; padding:6px 12px; border-radius:999px; display:inline-flex; align-items:center; gap:6px; }
.lgHero .hChip .ic{ width:1em; height:1em; font-size:13.5px; }
.lgHero .hChip.hot{ background:var(--acc-grad); color:var(--on-accent); box-shadow:var(--acc-glow), inset 0 1px 0 var(--bevel-hi-soft); }
.lgHero .hChip.hot .ic.flame{ color:var(--kudos-flame); }
.lgHero .hChip.ghost{ background:var(--sub); color:var(--accent-ink); box-shadow:inset 0 0 0 1px var(--line-soft); }
.lgHero .trendUp{ color:var(--accent-ink); display:inline-flex; } .lgHero .trendDn{ color:var(--clay-ink); display:inline-flex; }
.lgHero .trendUp .ic, .lgHero .trendDn .ic{ width:.9em; height:.9em; }

/* ================= STEP 8 — the result claim screen (RQS-06) =================
 * Built in the RQS-01 tokens on the first pass, which is the entire reason owner ruling
 * 2026-08-08 placed the token file BEFORE this step: a screen painted in the old ones joins
 * the repainting debt on the day it ships.
 *
 * THE STATUS TRIPLE IS THE POINT. RQS-01 gives each of the five states a dot colour (a 3:1
 * graphical boundary), an OPAQUE fill, and the 4.5:1 ink that fill is solved against — the
 * Material-3 pairing this project treats as a house rule. So `.statusChip` never picks its
 * own ink: `--status-<tone>-on` is a guaranteed-contrast partner for `--status-<tone>-fill`
 * in BOTH themes, and the chip re-values by swapping one attribute. Nothing here names a
 * colour; tests/token-only-colour.test.js holds that at zero.
 *
 * The ring is a conic gradient over the same two tokens: a colour a player can read at a
 * glance is not a substitute for the digits inside it, which is why the digits are the
 * element the countdown writes to and the ring is decoration around them. */
  .statusChip{
    display:inline-flex; align-items:center; gap:var(--space-4);
    border-radius:var(--r-999px); padding:var(--sp-1) var(--sp-3);
    font-size:var(--fs-12-5px); font-weight:800; white-space:nowrap;
    background:var(--status-pending-fill); color:var(--status-pending-on);
    box-shadow:inset 0 0 0 1px var(--status-pending);
  }
  .statusChip[data-tone="approved"]{ background:var(--status-approved-fill); color:var(--status-approved-on); box-shadow:inset 0 0 0 1px var(--status-approved); }
  .statusChip[data-tone="pending"]{ background:var(--status-pending-fill); color:var(--status-pending-on); box-shadow:inset 0 0 0 1px var(--status-pending); }
  .statusChip[data-tone="disputed"]{ background:var(--status-disputed-fill); color:var(--status-disputed-on); box-shadow:inset 0 0 0 1px var(--status-disputed); }
  .statusChip[data-tone="expired"]{ background:var(--status-expired-fill); color:var(--status-expired-on); box-shadow:inset 0 0 0 1px var(--status-expired); }
  .statusChip[data-tone="cancelled"]{ background:var(--status-cancelled-fill); color:var(--status-cancelled-on); box-shadow:inset 0 0 0 1px var(--status-cancelled); }

  #resultClaimTopBar{
    display:flex; align-items:center; gap:var(--sp-2);
    padding:calc(env(safe-area-inset-top) + 10px) var(--sp-3) 0;
  }
  #resultClaimBackBtn{
    display:flex; align-items:center; flex:0 0 auto;
    min-height:var(--size-40px); padding:0 var(--sp-14px);
    font-size:var(--fs-small); font-weight:800;
    border:1px solid var(--line-interactive); border-radius:var(--r-sm);
    background:var(--surface); color:var(--fg); box-shadow:var(--shadow-sm);
  }
  #resultClaimHeader{ flex:1 1 auto; min-width:0; text-align:center; }
  /* Mobile first, and it stays a single readable column on a tablet rather than stretching
     a card the eye has to track across. */
  #resultClaimBody{
    display:flex; flex-direction:column; gap:var(--sp-3);
    width:100%; max-width:var(--maxw-440px); margin:0 auto;
    padding:var(--sp-3) var(--sp-3) var(--space-40);
  }
  #resultClaimStatus{ text-align:center; padding:0 var(--sp-3) var(--space-24); }

  .rcPositions{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcPositionRow{ display:flex; align-items:baseline; gap:var(--sp-2); }
  .rcPositionSide{ color:var(--muted); font-size:var(--fs-12-5px); font-weight:800; flex:0 0 auto; }
  .rcPositionNames{ font-weight:750; min-width:0; }

  .rcScore{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcWinner{ font-weight:900; font-size:var(--fs-body-18); color:var(--accent-ink); margin:0; }
  .rcSetChips{ display:flex; flex-wrap:wrap; gap:var(--sp-2); }
  .rcSetChip{
    display:inline-flex; align-items:center; gap:var(--space-8);
    background:var(--sub); border-radius:var(--r-999px);
    padding:var(--sp-1) var(--sp-3); font-size:var(--fs-12-5px); font-weight:700;
  }
  .rcSetLabel{ color:var(--muted); }
  .rcSetScore{ font-variant-numeric:tabular-nums; font-weight:900; }

  .rcWindow{ display:flex; flex-direction:column; gap:var(--sp-3); align-items:stretch; }
  .rcWindowHead{ display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap; }
  .rcModeChip{
    display:inline-flex; align-items:center; border-radius:var(--r-999px);
    padding:var(--sp-1) var(--sp-3); font-size:var(--fs-12-5px); font-weight:800;
    background:var(--tint); color:var(--accent-ink);
    box-shadow:inset 0 0 0 1px var(--accent-edge);
  }
  .rcModeChip[data-mode="friendly"]{
    background:var(--sub); color:var(--muted); box-shadow:inset 0 0 0 1px var(--line-interactive);
  }
  /* The ring's own ink never touches text: the digits sit on --surface inside it, so the
     conic gradient can be the brand colour without putting text on a variable band. */
  .rcRing{
    --rc-ring-fraction:1;
    align-self:center; position:relative;
    width:var(--size-130px); height:var(--size-130px); border-radius:50%;
    background:conic-gradient(var(--status-pending) calc(var(--rc-ring-fraction) * 360deg), var(--sub) 0);
    display:flex; align-items:center; justify-content:center;
  }
  .rcRingInner{
    position:absolute; inset:var(--space-8); border-radius:50%;
    background:var(--surface);
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:var(--space-4);
    text-align:center; padding:var(--space-8);
  }
  .rcCountdownLabel{ color:var(--muted); font-size:var(--fs-12-5px); font-weight:700; }
  .rcCountdown{
    font-size:var(--fs-num-20); font-weight:900; color:var(--fg);
    font-variant-numeric:tabular-nums; direction:ltr; unicode-bidi:isolate; white-space:nowrap;
  }
  .rcReminder{
    margin:0; padding:var(--sp-2) var(--sp-3); border-radius:var(--r-sm);
    background:var(--status-pending-fill); color:var(--status-pending-on);
    box-shadow:inset 0 0 0 1px var(--status-pending);
    font-size:var(--fs-small); font-weight:700;
  }
  .rcSettlement{ margin:0; color:var(--fg); font-size:var(--fs-small); line-height:1.55; }
  /* A superseded original stays reachable BY DESIGN — the revision model is append-only — so
     this has to read as "not the one that counts", not as an error. The cancelled triple, and
     its own paired ink, rather than a dimmed --muted on an unknown surface. */
  .rcSuperseded{
    margin:0; padding:var(--sp-2) var(--sp-3); border-radius:var(--r-sm);
    background:var(--status-cancelled-fill); color:var(--status-cancelled-on);
    box-shadow:inset 0 0 0 1px var(--status-cancelled);
    font-size:var(--fs-small); font-weight:700;
  }
  .rcOpenReplacement{ width:100%; }
  .rcProvenance{ margin:0; display:flex; gap:var(--space-8); align-items:baseline; flex-wrap:wrap; }
  .rcProvenanceLabel{ color:var(--muted); font-size:var(--fs-12-5px); font-weight:700; }
  .rcProvenanceValue{
    background:var(--sub); color:var(--fg); border-radius:var(--r-999px);
    padding:var(--sp-1) var(--sp-3); font-size:var(--fs-12-5px); font-weight:800;
  }

  .rcActionsCard{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcActions{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcActions button{ width:100%; }

  .rcTimelineCard{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcTimeline{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--sp-3); }
  .rcTimelineRow{ display:flex; gap:var(--sp-2); align-items:flex-start; }
  .rcTimelineDot{
    flex:0 0 auto; margin-top:var(--space-4);
    width:var(--space-8); height:var(--space-8); border-radius:50%;
    background:var(--line-interactive);
  }
  .rcTimelineBody{ display:flex; flex-direction:column; gap:var(--space-4); min-width:0; }
  .rcTimelineText{ font-weight:750; font-size:var(--fs-small); }
  .rcTimelineWhen{ font-size:var(--fs-12-5px); }
  .rcTimelineReason{
    margin:0; padding:var(--sp-2); border-radius:var(--r-sm);
    background:var(--sub); color:var(--fg); font-size:var(--fs-12-5px); line-height:1.5;
  }

  .resultDisputeDialog{ display:flex; flex-direction:column; gap:var(--sp-3); }
  .resultDisputeField{ display:flex; flex-direction:column; gap:var(--space-4); }
  .resultDisputeField label{ color:var(--muted); font-size:var(--fs-12-5px); font-weight:800; }
  .resultDisputeField textarea{
    font:inherit; color:var(--fg); background:var(--surface);
    border:1.5px solid var(--line-interactive); border-radius:var(--r-sm);
    padding:var(--sp-2) var(--sp-3); resize:vertical; min-height:var(--size-96px);
  }

/* ================= STEP 8 — THE CREATING HALF (report · pending · correction) =================
 * The three surfaces step 8 shipped without. Built in the RQS-01 tokens on the first pass, per
 * owner ruling 2026-08-08 — *"אסור יותר לבנות שום מסך חדש בטוקנים הישנים"* — so nothing here
 * names a colour, a spacing value or a radius directly. tests/token-only-colour.test.js holds
 * the literal-colour count at zero for the whole file and would fail on one hex.
 *
 * IT REUSES THE VOCABULARY ABOVE RATHER THAN INVENTING A SECOND ONE. `.statusChip` carries the
 * pending rows' role, `.appCard` is the container, `.appButton` is every control, and the
 * countdown digits use the same tabular-numeral treatment as `.rcCountdown`. A form that looked
 * like a different product from the card it creates would be the duplicate-surface failure in
 * visual form.
 *
 * THE ONE STRUCTURAL MOVE IS THE DISABLED SCORE FIELD. Positions before score is enforced by the
 * form (result-claim.js enables #reportResultScore only once every seat has a name), so the
 * disabled state has to READ as "not yet", not as "broken": it keeps its label, keeps its
 * describedby hint, and the hint says which step is outstanding. A control disabled with no
 * label change and no reason on screen is the precedent at app.js:4381 this step exists to stop
 * repeating. */
  .reportEntryCard{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .reportEntryCard .appButton{ width:100%; }

  .reportResultDialog{ display:flex; flex-direction:column; gap:var(--sp-4); }
  .rcFormStep{ display:flex; flex-direction:column; gap:var(--sp-2); }
  /* The step heading is a heading, not a card title: these are sections of one task, and
     giving each the weight of a card would make a four-field form read as four screens. */
  .rcFormStepTitle{
    margin:0; font-size:var(--fs-body-16); font-weight:900; color:var(--fg);
    padding-bottom:var(--space-4); border-bottom:1px solid var(--line);
  }
  .rcFormField{ display:flex; flex-direction:column; gap:var(--space-4); }
  /* `.rcFieldLabel` is the same label, for a group of buttons rather than a form control —
     a <label for> may only point at the latter, and the track picker is a segmented control. */
  .rcFormField label, .rcFormField .rcFieldLabel{
    color:var(--muted); font-size:var(--fs-12-5px); font-weight:800;
  }
  /* THE TAP FLOOR, scoped rather than global. `.seg button` is 44px everywhere and this
     project's floor is --tap-min (48px); raising it globally would move nineteen other
     controls on screens nobody is accepting today, which is a spacing change no suite can
     see. Both segmented controls in THIS dialog get the floor. */
  .reportResultDialog .seg button{ min-height:var(--tap-min); }
  /* The three track labels are full phrases ("טניס יחידים"), and `.seg button` ellipsises by
     default. A clipped option is unreadable, so this one wraps to a second line instead. */
  .segTrack button{
    white-space:normal; overflow:visible; text-overflow:clip;
    line-height:1.15; padding-inline:var(--space-4);
  }
  /* ---- THE FORMAT, as a disclosure ---------------------------------------------------------
     One field of the form that opens in place: a button carrying aria-expanded and a
     radiogroup carrying `hidden`. Not a second panel and not a second overlay — a second
     .appOverlay has to be hand-added to scripts/a11y.js's OVERLAYS table or the gate fails at
     run time, and a second panel of #reportResultForm is never axe-scanned at all.

     The toggle borrows .rcSeatPick's box on purpose, so the two questions above the score
     read as one ladder: --line-interactive and not --line or --line-soft, because this is the
     sole boundary of a CONTROL and those two are 1.27:1 and 1.26:1 by design — legal as
     decorative rules, forbidden as the edge of something tappable (§8.4). */
  .rcRulesetToggle, .rcRulesetOption{
    display:flex; align-items:center; gap:var(--sp-2); width:100%; text-align:start;
    min-height:var(--tap-min); padding:var(--sp-2) var(--sp-3); font:inherit;
    border-radius:var(--r-sm); background:var(--surface); color:var(--fg);
    border:1.5px solid var(--line-interactive); cursor:pointer;
  }
  .rcRulesetToggle:hover, .rcRulesetOption:hover{ background:var(--surface-2); }
  .rcRulesetText{ display:flex; flex-direction:column; gap:var(--sp-2px); min-width:0; }
  .rcRulesetName{ font-weight:800; overflow-wrap:anywhere; }
  /* The shape of the format in one line ("6 משחקונים · 2 סטים · יתרון"). --muted is the
     dimmest ink that still clears AA on every surface this row can land on. */
  .rcRulesetDetail{ color:var(--muted); font-size:var(--fs-small); }
  .rcRulesetList{ display:flex; flex-direction:column; gap:var(--space-4); }
  /* An author `display` outranks the UA's [hidden]{display:none}. The live screen's preset
     cards shipped exactly that bug — chosen "custom" hid nothing — and
     tests/hidden-means-hidden.test.js walks the real page for it. */
  .rcRulesetList[hidden]{ display:none; }
  .rcRulesetCheck{
    margin-inline-start:auto; width:24px; height:24px; display:grid; place-items:center;
    border-radius:50%; border:1.5px solid var(--line-interactive); color:transparent;
  }
  .rcRulesetOption[aria-checked="true"]{ border-color:var(--accent-ink); background:var(--tint); }
  .rcRulesetOption[aria-checked="true"] .rcRulesetCheck{
    color:var(--on-accent); background:var(--accent); border-color:var(--accent);
  }
  /* THE RATED-FLOOR VERDICT. The box is local; the two INKS are the setup screen's own rule
     (search #stage3RulesRating), extended rather than copied, because it is the same sentence
     about the same rule and a second colour pair is a second thing to keep at AA. */
  .rcRulesetRating{
    margin:0; padding:var(--sp-2) var(--sp-3); border-radius:var(--r-sm);
    background:var(--sub); line-height:1.5;
  }
  .rcSeatInput, .reportResultDialog input[type="date"]{
    font:inherit; color:var(--fg); background:var(--surface);
    border:1.5px solid var(--line-interactive); border-radius:var(--r-sm);
    padding:var(--sp-2) var(--sp-3); min-height:var(--tap-min); width:100%;
    /* the owner's iPhone finding, 2026-08-10: "מתי שיחקתם" cut inside the date window.
       When iOS raises the date picker the WebView's focus scrolling positions the INPUT at
       the scroller's edge and the label above it leaves the visible box. The margin makes
       any UA or scripted scroll-into-view keep one label's height of room above — the JS
       half (centring the whole .rcFormField on focus) lives in result-claim.js. */
    scroll-margin-block:calc(var(--sp-5) + var(--sp-3)) var(--sp-5);
  }
  .rcSeatInput:disabled{
    background:var(--sub); color:var(--muted); border-style:dashed;
  }
  /* THE DATE, DRAWN BY THE PAGE (owner canary finding, 2026-08-10: "Aug 2026 10").
     WKWebView paints a date input's own glyphs per the NATIVE app's localization, never the
     page's lang/dir — the iOS shell declares no Hebrew, so the glyphs came out "10 Aug 2026"
     and the RTL dialog visually reordered them. Info.plist is out of OTA's reach, so the page
     hides the input's GLYPHS ONLY — the input itself stays visible, focusable and tappable;
     an opacity-0 input is the hidden-means-hidden defect wearing a costume — and draws the
     chosen date itself in .rcDateDisplay (result-claim.js, toLocaleDateString('he-IL')).
     BOTH hiding routes ship deliberately: WKWebView's respect for ::-webkit-datetime-edit is
     exactly the thing being worked around, and a transparent `color` is the belt to that
     brace. The overlay mirrors the input's border and padding so the Hebrew text sits where
     the glyphs did, takes the input's own ink token, and passes every tap through. */
  /* THE WIDTH, CLAMPED (owner canary finding, 2026-08-10, second photo, STILL overflowing on
     bundle 6 after two Chromium-verified fixes — measured on iOS WebKit 2026-08-10, sim
     iPhone 17). The first two diagnoses were wrong because desktop Chromium cannot show the
     mechanism: a NATIVELY-THEMED iOS date input takes UA rules the author cascade cannot
     beat — computed box-sizing stayed content-box against the global border-box reset, and
     computed min-width stayed 93px against the min-width:0 below. width:100% (336px) then
     ADDED its 24px padding and 3px border OUTSIDE the box: 362.7px in a 336px column,
     poking 26.7px through the dialog's inline-end wall (RTL: leftwards), through the
     dialog's own border. appearance:none is the fix because those UA rules are conditioned
     on the native appearance — with it gone, the global border-box and the width clamp
     below actually apply. Drawing only by spec: value/focus/change are untouched and the
     native picker opening on tap is NOT sim-verifiable (no tool here can land a real tap;
     WebDriver's synthetic ones never reach the page) — it is owed to the bundle-7 device
     pass, and the picker already survived this input's glyph-hiding on the owner's phone.
     min-width:0 + max-width:100% stay
     as the cross-engine clamp, and .rcDateWrap keeps min-width:0 so no flex ancestor
     re-inflates from the intrinsic floor. */
  .rcDateWrap{ position:relative; display:block; min-width:0; max-width:100%; }
  .reportResultDialog .rcDateWrap input[type="date"]{
    -webkit-appearance:none; appearance:none;
    color:transparent; min-width:0; max-width:100%;
  }
  .reportResultDialog .rcDateWrap input[type="date"]::-webkit-datetime-edit{ opacity:0; }
  .rcDateDisplay{
    position:absolute; inset:0; display:flex; align-items:center;
    padding:var(--sp-2) var(--sp-3); border:1.5px solid transparent;
    color:var(--fg); pointer-events:none;
  }
  /* THE SCORE-ENTRY GRID (2026-08-10, replacing .rcScoreInput — the owner's own sketch:
     a cell per set, a row per side, the tiebreak small, no surplus boxes).
     A real <table>: the RTL page draws the first column rightmost, so set 1 sits at the
     right edge exactly as the live board's .setSquares does. table-layout:fixed with an
     explicit width on every set <col> (major 7: fixed layout otherwise splits columns
     EQUALLY and the name lands at 53.5px) — the name column takes the remainder and may
     wrap; a number may never. The cells' digits keep the .rcScoreInput treatment: tabular
     figures, an LTR run isolated inside the RTL page.
     THE GRID NEVER WRAPS AND NEVER SCROLLS; the PAD wraps. A sideways-scrolling entry
     surface is the owner's complaint wearing new clothes. */
  .rcSetGrid{
    width:100%; table-layout:fixed;
    border-collapse:separate; border-spacing:var(--sp-1);
  }
  .rcSetGrid col.rcSetCol{ width:calc(var(--tap-min) + var(--sp-1)); }
  .rcSetGridHead{
    font-size:var(--fs-12-5px); font-weight:600; color:var(--muted);
    text-align:center; padding:0;
  }
  .rcSetGridCorner{ padding:0; }
  .rcSetGridName{
    text-align:start; font-size:var(--fs-small); font-weight:600; color:var(--fg);
    min-width:0; overflow-wrap:anywhere; padding:0;
  }
  .rcSetGridPlayer{ display:block; }
  /* The cell is a real INPUT now (direct entry, 2026-08-10, owner override for the system
     numeric keypad): --line-interactive or nothing (--line is 1.27:1 by design and
     forbidden as a sole boundary), 48px floor — this fill sits on --sub inside a --bg
     dialog, where --line-interactive measures 3.92:1 night / 3.89:1 day. The focused cell
     wears the seg pattern's pressed pair. Disabled (P5, until the seats are complete) =
     dashed, same as the seat inputs. The scroll-margins are the keyboard-burial answer's
     CSS half: any focus scrolling keeps the set header above and the verdict line below
     inside the dialog's own scroller. */
  input.rcSetCell{
    width:100%; min-height:var(--tap-min);
    appearance:none; -webkit-appearance:none;
    text-align:center;
    border:1.5px solid var(--line-interactive); border-radius:var(--r-sm);
    background:var(--sub); color:var(--fg);
    font:inherit; font-variant-numeric:tabular-nums; font-size:var(--fs-num-16);
    font-weight:800; direction:ltr; unicode-bidi:isolate; padding:0;
    scroll-margin-block:calc(var(--sp-5) + var(--sp-3)) calc(var(--sp-5) * 3);
  }
  input.rcSetCell:focus, input.rcSetCell:focus-visible{
    outline:none; background:var(--tint); color:var(--accent-ink);
    border-color:var(--accent-edge);
    box-shadow:inset 0 0 0 1.5px var(--accent-edge);
  }
  /* a digit the rules refuse: the boundary flips to the danger ink (6.97:1 on --surface)
     and the sentence appears in the verdict line — a mark AND words, never a mark alone */
  input.rcSetCell[aria-invalid="true"], input.rcSetCell[aria-invalid="true"]:focus{
    border-color:var(--danger); box-shadow:inset 0 0 0 1.5px var(--danger);
    background:var(--tint-danger); color:var(--fg);
  }
  input.rcSetCell:disabled{ background:var(--sub); color:var(--muted); border-style:dashed; }
  /* The owner's superscript — real markup BESIDE the input (an input cannot hold a <sup>),
     display-only, never clipped. Unicode superscript digits stop at one digit; 12-10 and
     40-38 are routine. The points are edited in the .rcTbEntry row below the table. */
  .rcSetCellBox{ position:relative; }
  .rcSetCellBox .rcSetCellSup{
    position:absolute; top:0; inset-inline-end:2px;
    font-size:var(--fs-12-5px); font-weight:700; color:var(--muted); pointer-events:none;
  }
  /* the decider's games-or-tiebreak seg, beside the grid whenever the decider is drawn */
  .rcGridKind{ margin-top:var(--sp-2); }
  .rcGridKind button{ min-height:var(--tap-min); }
  /* the tiebreak points, one labelled row per 7-6 set: two typed cells sized to their
     two digits, and "לא זוכר" as an answer, not a refusal */
  .rcTbEntry{
    margin-top:var(--sp-2); display:flex; align-items:center; flex-wrap:wrap;
    gap:var(--sp-2);
  }
  .rcTbLabel{ font-size:var(--fs-small); font-weight:700; color:var(--fg); }
  .rcTbCells{ display:flex; gap:var(--sp-1); }
  .rcTbEntry .rcTbCell{ width:calc(var(--tap-min) + var(--sp-1)); }
  .rcTbForgot{
    min-height:var(--tap-min); padding:0 var(--sp-3);
    border:1.5px solid var(--line-interactive); border-radius:var(--r-sm);
    background:var(--sub); color:var(--fg);
    font:inherit; font-size:var(--fs-small); font-weight:700; cursor:pointer;
  }
  /* PRESSED IS PAINTED (owner canary finding, bundle 6: "הוא לא מסומן כנבחר"). aria-pressed
     flipped since bundle 5 and no rule read it, so the collapse worked while the control
     LOOKED untouched. Same selected triple as .seg button[aria-pressed="true"] and the
     chips — --tint fill, --accent-ink ink, --accent-edge boundary — because a second
     selected-state vocabulary is a second thing to keep at AA in both themes. */
  .rcTbForgot[aria-pressed="true"]{
    background:var(--tint); color:var(--accent-ink); font-weight:800;
    border-color:var(--accent-edge);
  }
  .rcTbForgot:disabled{ color:var(--muted); border-style:dashed; cursor:default; }
  /* graft 3 — the stored line a policy cannot express, read-only beside the empty grid:
     the substance of the ask, so tabular, nowrap, never ellipsised. */
  .rcGridStoredLine{
    margin:var(--sp-2) 0 0; font-size:var(--fs-small); color:var(--muted);
  }
  .rcGridStoredLine .num{
    font-variant-numeric:tabular-nums; white-space:nowrap;
    direction:ltr; unicode-bidi:isolate; color:var(--fg); font-weight:800;
  }
  /* The live verdict. Three states, each on a paired fill/ink from the status triple so the
     ink is a guaranteed-contrast partner in BOTH themes rather than a colour chosen against
     one of them — the Material-3 pairing this project treats as a house rule. */
  .rcFormHint{ margin:0; font-weight:700; }
  .rcFormHint[data-tone="legal"]{ color:var(--status-approved-on); }
  .rcFormHint[data-tone="refused"]{
    padding:var(--sp-2) var(--sp-3); border-radius:var(--r-sm);
    background:var(--status-disputed-fill); color:var(--status-disputed-on);
    box-shadow:inset 0 0 0 1px var(--status-disputed);
  }
  .rcRecovery:empty{ display:none; }
  .rcRecovery{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcRecovery .appButton{ width:100%; }

  /* ---- CHOOSING THE OPPONENT -------------------------------------------------------------
     A seat is a ROW WITH A CONTROL, never a text field. The owner typed an opponent's
     username into the bare input this replaces and was refused every time, because a name is
     not an identifier and nothing on the screen ever offered him one.

     Mobile-first throughout: every rule below is the small-screen layout, and the only
     media query raises the row from stacked to inline once there is width for it. */
  .rcSeatRow{ display:flex; flex-direction:column; gap:var(--space-4); }
  .rcSeatLabel{ color:var(--muted); font-size:var(--fs-12-5px); font-weight:800; }

  /* The chooser and the fixed "me" row share their box so the seats read as one ladder.
     --line-interactive and not --line: this is the sole boundary of a CONTROL, and --line is
     1.27:1 by design — legal as a decorative rule, forbidden as the edge of something
     tappable (§8.4, the reason --line-interactive was added at all). */
  .rcSeatPick, .rcSeatFixed{
    display:flex; align-items:center; gap:var(--sp-2); width:100%; text-align:start;
    min-height:var(--tap-min); padding:var(--sp-2) var(--sp-3);
    border-radius:var(--r-sm); background:var(--surface); color:var(--fg);
    border:1.5px solid var(--line-interactive); font:inherit;
  }
  .rcSeatFixed{ background:var(--sub); border-style:solid; border-color:var(--line); }
  .rcSeatPick{ cursor:pointer; }
  .rcSeatPick:hover{ background:var(--surface-2); }
  /* An EMPTY seat is dashed — the same vocabulary the disabled score field already uses for
     "nothing here yet" — and its text is --muted, which is the dimmest ink that still clears
     AA on every surface it can land on (--faint holds the same value for exactly that reason). */
  .rcSeatPick.isEmpty{ border-style:dashed; }
  .rcSeatEmpty{ color:var(--muted); font-weight:700; }
  .rcSeatIcon{ display:inline-flex; color:var(--accent-ink); }
  /* The action word is the affordance. It never shrinks below the body size and never
     truncates: "בחירה" and "החלפה" are the difference between adding and replacing. */
  .rcSeatAction{
    margin-inline-start:auto; color:var(--accent-ink);
    font-size:var(--fs-small); font-weight:900; white-space:nowrap;
  }

  /* ---- WHERE THE MATCH WAS PLAYED — a disclosure inside the form, never a panel ----
     Mobile-first: the row stacks the recall chip above the chooser and only inlines them when
     there is width for it. */
  .rcCourtRow{ display:flex; flex-direction:column; gap:var(--space-4); }
  /* AN OPTIONAL FIELD MUST NOT WEAR THE REQUIRED FIELD'S COSTUME. `.isEmpty` is dashed and is the
     seat vocabulary for "not done yet"; a seat in that state REFUSES the send and this field can
     never refuse anything. Solid edge, --muted ink, and the label says (not required). */
  .rcSeatPick.isOptional{ border-style:solid; }
  .rcSeatPick.isOptional .rcCourtValue{ color:var(--muted); font-weight:700; }
  /* HIDDEN MUST ACTUALLY HIDE. `.rcSeatPick` sets display:flex, and an author display rule of
     equal specificity BEATS the UA's [hidden]{display:none} — so without this line the recall chip
     renders at full height and fully tappable while `hidden` is true, and every signal says it is
     gone. tests/hidden-means-hidden.test.js walks the real page for exactly this and deliberately
     has no bounding-rect filter, so an offender inside a closed overlay can no longer hide.
     This repository has shipped that bug more than once. */
  .rcSeatPick[hidden]{ display:none; }
  .rcCourtBody{
    display:flex; flex-direction:column; gap:var(--sp-2);
    /* The dialog is bounded rather than the page: the largest single city holds 22 venues, and a
       region holds up to 148. Derive, do not quote:
       node -e "const c=require('./courts.json').courts;const m={};c.forEach(x=>m[x.city]=(m[x.city]||0)+1);console.log(Math.max(...Object.values(m)))" */
    max-height:52vh; overflow-y:auto;
  }
  .rcCourtBody[hidden]{ display:none; }
  .rcCourtFilter{ display:flex; flex-direction:column; gap:var(--space-4); }
  /* An author display of equal specificity beats the UA's [hidden]{display:none}. */
  .rcCourtFilter[hidden]{ display:none; }
  .rcCourtHead{ display:flex; align-items:center; gap:var(--sp-2); }
  .rcCourtChips{ display:flex; flex-wrap:wrap; gap:var(--space-4); }
  /* A region is a choice among five, so it is a chip with aria-pressed — the same vocabulary the
     rest of the app uses for a small exclusive set. --line-interactive because this is the sole
     boundary of a control: --line is 1.27:1 and --line-soft 1.26:1 by design, legal as decorative
     rules and forbidden as the edge of something tappable. */
  .rcCourtChip{
    min-height:var(--tap-min); padding:var(--sp-2) var(--sp-3); font:inherit; cursor:pointer;
    border-radius:var(--r-sm); border:1.5px solid var(--line-interactive);
    background:var(--surface); color:var(--fg); font-weight:800;
  }
  .rcCourtChip:hover{ background:var(--surface-2); }
  .rcCourtChip[aria-pressed="true"]{
    border-color:var(--accent); background:var(--accent); color:var(--on-accent);
  }
  .rcCourtValue{ overflow-wrap:anywhere; min-width:0; }
  .rcCourtEmpty{ color:var(--muted); font-weight:700; margin:0; }

  @media (min-width:560px){
    .rcCourtRow{ flex-direction:row; align-items:stretch; }
    .rcCourtRow > .rcSeatPick{ flex:1 1 0; }
  }

  /* ---- the picker panel. A panel of the same dialog, not a second overlay. ---- */
  .rcPicker{ display:flex; flex-direction:column; gap:var(--sp-4); }
  .rcPicker[hidden]{ display:none; }
  .rcPickerHead{ display:flex; align-items:center; gap:var(--sp-2); }
  .rcPickerHead .rcFormStepTitle{ border-bottom:0; padding-bottom:0; flex:1; }
  .rcPickerGroupTitle{
    margin:0 0 var(--space-8); font-size:var(--fs-12-5px); font-weight:900;
    color:var(--muted); text-transform:none;
  }
  .rcPickerList{ display:flex; flex-direction:column; gap:var(--space-4); }

  /* A candidate row. The whole row is the target — a 48px minimum, and the name is the
     accessible name rather than a nested link nobody can hit on a phone.

     --line-interactive, and it shipped as --line-soft, which is the same defect the two comments
     above this one exist to prevent — written three rules after the second of them. This row is
     the SOLE boundary of a control, and the numbers say so rather than the principle: the old
     edge painted 1.25:1 night and 1.20:1 day over this row's own --sub fill, and that fill is
     1.26:1 / 1.05:1 against the surface behind it, so there was no perceivable boundary in
     either theme against §1.4.11's 3:1. --line-interactive is 3.92:1 night / 3.89:1 day on --sub.
     The width stays 1px: WCAG sets no width and the row's three siblings differ from it in fill
     already, so widening the stroke would move the ladder without buying contrast. */
  .rcPickRow{
    display:flex; align-items:center; gap:var(--sp-2); width:100%; text-align:start;
    min-height:var(--tap-min); padding:var(--sp-2) var(--sp-3); font:inherit;
    border-radius:var(--r-sm); border:1px solid var(--line-interactive);
    background:var(--sub); color:var(--fg); cursor:pointer;
  }
  .rcPickRow:hover{ background:var(--surface-2); }
  .rcPickText{ display:flex; flex-direction:column; gap:var(--sp-2px); min-width:0; }
  /* A display name is user data and may be long. It wraps rather than ellipsising: a
     truncated name is exactly the ambiguity this screen exists to remove. */
  .rcPickName{ font-weight:800; overflow-wrap:anywhere; }
  .rcPickReason{ color:var(--muted); font-size:var(--fs-12-5px); font-weight:700; }
  .rcPickAvatar{ flex:0 0 auto; }

  /* ---- the other two routes. ---- */
  .rcPickerRoutes{
    display:flex; flex-direction:column; gap:var(--sp-3);
    padding-top:var(--sp-3); border-top:1px solid var(--line);
  }
  /* The scan route is the one the owner singled out, so it carries the accent edge — the
     brand green clears 3:1 as a graphical boundary in BOTH themes (--accent-edge), which a
     raw brand fill behind text would not. The ink stays --fg on --surface; nothing here
     depends on a colour for its legibility. */
  .rcPickerRoute{
    display:flex; align-items:center; gap:var(--sp-3); width:100%; text-align:start;
    min-height:var(--tap-min); padding:var(--sp-3); font:inherit; cursor:pointer;
    border-radius:var(--r-sm); border:1.5px solid var(--accent-edge);
    background:var(--tint); color:var(--fg);
  }
  /* HIDDEN MUST ACTUALLY HIDE. An author `display` rule of equal specificity BEATS the UA's
     `[hidden]{display:none}`, and this repository has already shipped that bug — see
     tests/hidden-means-hidden.test.js. `.rcPickerRoute` sets `display:flex`, so without this
     line the scan route stayed 90px tall and fully tappable while `hidden` was true, and
     every signal said it was gone. Measured in a browser, not read off the stylesheet. */
  .rcPickerRoute[hidden]{ display:none; }
  /* DISABLED MUST LOOK DISABLED, and it must still be READABLE — those pull in opposite
     directions and only one of them is negotiable. The scan route is disabled on a seat on the
     reporter's own side, where its only possible outcome is `step8_attach_seat_side`, and the
     REASON is rendered into .rcPickerRouteHint beneath it (rider H16: a refusal in words, never
     a dead button with its reason off screen). So the affordance is what dims — the brand edge
     and the tint go, the cursor stops promising a tap — and the INK does not: a player who
     cannot read why a route is closed has been told nothing. WCAG exempts disabled controls
     from contrast; that exemption is not taken here, because the hint below it is the whole
     point of the state. */
  .rcPickerRoute:disabled{
    cursor:default; border-color:var(--line); background:var(--surface-2); opacity:1;
  }
  .rcPickerRoute:disabled > .ic{ color:var(--muted); }
  .rcPickerRoute > .ic{ flex:0 0 auto; color:var(--accent-ink); }
  .rcPickerRouteText{ display:flex; flex-direction:column; gap:var(--sp-2px); min-width:0; }
  .rcPickerRouteTitle{ font-weight:900; }
  .rcPickerRouteHint{ color:var(--muted); font-size:var(--fs-12-5px); font-weight:700; }
  .rcPickerGuestRow{ display:flex; flex-direction:column; gap:var(--space-8); }

  /* ---- the QR handshake: the code the reporter SHOWS, and the card the scanner ANSWERS.
     Mobile-first and single-column throughout — both of these are read at arm's length by
     somebody holding a phone on a court, and neither has a wide layout to grow into. ---- */
  .rcCodePanel{
    display:flex; flex-direction:column; align-items:center; gap:var(--space-12);
    padding:var(--space-16); border-radius:var(--radius-16);
    border:1px solid var(--line); background:var(--surface-2); color:var(--fg);
  }
  /* THE PLATE IS LIGHT IN BOTH THEMES ON PURPOSE — see the --qr-plate comment in :root. It
     is sized in `em` off a fixed base so the code scales with the type ramp but never
     outgrows a small phone: min() keeps it inside the viewport on a 320px screen, which is
     the width the whole system is designed up from. `image-rendering:pixelated` stops a
     fractional scale from blurring module edges into each other, which is what makes a code
     that "looks fine" fail to decode under a camera. */
  .rcCodePlate{
    display:flex; align-items:center; justify-content:center;
    width:min(260px, 72vw); height:min(260px, 72vw);
    padding:var(--space-12); border-radius:var(--radius-12);
    background:var(--qr-plate);
  }
  .rcCodePlate svg{ width:100%; height:100%; display:block; image-rendering:pixelated; }
  .rcCodePlate rect{ fill:var(--qr-ink); }
  /* Centred, because the whole panel is, and legible at a glance: the person reading it is
     holding the phone up towards somebody else and looking at it side-on. */
  .rcCodeHint{ text-align:center; }
  .rcCodeExpiry{
    margin:0; font-weight:800; font-variant-numeric:tabular-nums; text-align:center;
    color:var(--muted);
  }
  .rcCodeStatus{ margin:0; text-align:center; color:var(--fg); font-weight:700; }

  /* The scanner's answer card. It REPLACES the ordinary confirm/dispute controls rather
     than sitting beside them, so it deliberately reuses .rcActions for its button row and
     looks like the control it stands in for. */
  .rcHandshake{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcHandshakeWho{ margin:0; font-weight:800; overflow-wrap:anywhere; }
  .rcHandshakeStatus{ margin:0; color:var(--muted); font-weight:700; }
  .rcHandshakeStatus:empty{ display:none; }

  /* QR ATTACH — the OPEN seat. The scanner's review card, and it reuses .rcHandshakeWho and
     .rcActions rather than paralleling them: it stands in the same place, answers the same two
     questions and must look like the control it is a sibling of. What it adds is the SCORE and
     the CONSEQUENCE, because this reader has never seen this match before — they hold no seat
     in it and, until they approve, it does not exist.

     THE SCORE IS THE LARGEST THING ON THE CARD, deliberately: the owner's ruling is that they
     see the result BEFORE approving, and a score set in body copy beside two buttons is a
     screen somebody taps without reading. `tabular-nums` because it is digits and a pair of
     them must not shift width between "6-4" and "7-6"; `--fg` rather than a status tone
     because nothing has happened yet and a coloured score would imply an outcome. */
  .rcClaimReview{ display:flex; flex-direction:column; gap:var(--sp-2); }
  /* AND AN LTR RUN, which is not decoration: "6-4 6-3" is two number runs with a neutral
     between them, so in this RTL page they lay out RIGHT TO LEFT and the SET ORDER inverts.
     Every other score node carries `.num`; this one was hand-styled and inherited nothing —
     and it is the score a scanner consents to. */
  .rcClaimScore{
    margin:0; text-align:center; color:var(--fg); font-weight:800;
    font-size:var(--fs-num-24); font-variant-numeric:tabular-nums; letter-spacing:0.02em;
    direction:ltr; unicode-bidi:isolate; white-space:nowrap;
  }
  /* The consequence line stays on `--muted` over the card surface, which is the same pairing
     every other hint on this screen uses and is AA in both themes. It must never be given a
     brand wash: this sentence can say "this will count against your rating", and text whose
     colour depends on a band is the one thing CLAUDE.md refuses outright. */
  .rcClaimConsequence{ margin:0; }
  .rcClaimStatus{ margin:0; color:var(--muted); font-weight:700; }
  .rcClaimStatus:empty{ display:none; }
  .rcPickerGuestRow .appButton{ width:100%; }

  @media (min-width:420px){
    .rcSeatRow{ flex-direction:row; align-items:center; gap:var(--sp-3); }
    .rcSeatLabel{ flex:0 0 30%; }
    .rcPickerGuestRow{ flex-direction:row; align-items:center; }
    .rcPickerGuestRow .appButton{ width:auto; flex:0 0 auto; }
  }

  /* ---- a PROPOSED CORRECTION awaiting an answer. Same surface, second kind of thing. ----
     Token-driven throughout: tests/token-only-colour.test.js holds that every colour value in
     this file is a token DEFINITION and none sits in a component rule.
     The boundary is `--line-interactive` and deliberately NOT the retired decorative rule: this
     row carries controls, and a control's sole boundary needs 3:1 where that one gave 1.36:1.
     (The retired token is not named here on purpose — tests/rqs01-tokens.test.js counts the name
     anywhere in the corpus, comments included, and a mention is indistinguishable from a use.)
     THERE IS NO CLOCK CLASS HERE, deliberately. A correction has no window, and a countdown
     that reaches zero and does nothing teaches the player the app is broken. */
  .rcCorrectionCard{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcCorrectionList{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcCorrectionRow{
    display:flex; flex-direction:column; gap:var(--space-8);
    padding:var(--sp-3); border-radius:var(--r-sm);
    background:var(--sub); box-shadow:inset 0 0 0 1px var(--line-interactive);
  }
  .rcCorrectionWho{ margin:0; font-weight:800; min-width:0; overflow-wrap:anywhere; }
  /* The proposed score is the substance of the ask and must never clip or ellipsise:
     tabular-nums and nowrap, the same treatment every numeric cell in this product gets. */
  .rcCorrectionProposed{
    margin:0; font-weight:800;
    font-variant-numeric:tabular-nums; white-space:nowrap;
    overflow-x:auto; max-width:100%;
  }
  .rcCorrectionProposedLabel{ color:var(--muted); font-weight:600; margin-inline-end:var(--sp-1); }
  .rcCorrectionStatus{ margin:0; }
  .rcCorrectionControls{ display:flex; flex-direction:column; gap:var(--sp-2); }
  @media (min-width:380px){
    .rcCorrectionControls{ flex-direction:row; }
    .rcCorrectionControls .appButton{ flex:1 1 0; }
  }

  /* ---- what awaits an answer. Rendered into #approvalsBox, above the invitations. ---- */
  .rcPendingCard{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcPendingList{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcPendingRow{
    display:flex; flex-direction:column; gap:var(--space-8);
    padding:var(--sp-3); border-radius:var(--r-sm);
    background:var(--sub); box-shadow:inset 0 0 0 1px var(--line-soft);
  }
  .rcPendingHead{ display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap; }
  /* NEVER truncated and never ellipsised: this is a countdown, and a clipped one is a lie
     about how long is left. tabular-nums keeps the row from reflowing every second. */
  .rcPendingClock{
    margin-inline-start:auto; color:var(--muted);
    font-size:var(--fs-12-5px); font-weight:800;
    font-variant-numeric:tabular-nums; white-space:nowrap;
  }
  .rcPendingWho{ margin:0; font-weight:800; min-width:0; overflow-wrap:anywhere; }
  .rcPendingScore{
    margin:0; color:var(--muted); font-weight:800;
    font-variant-numeric:tabular-nums; direction:ltr; unicode-bidi:isolate;
    text-align:start; white-space:nowrap;
  }
  .rcPendingOpen{ width:100%; }
  /* The שחק stack's overflow control (play-inbox.js). Full width so it reads as the end of the
     list rather than as a fifth card, and spaced off the last card it follows. */
  .inboxOverflowToggle{ width:100%; margin-top:var(--sp-3); }

  /* ---- the correction control ---- */
  .rcReviseCard{ display:flex; flex-direction:column; gap:var(--sp-2); }
  .rcReviseOpen{ width:100%; }

  /* One readable column on a phone; on anything wider the two seats of a side sit side by
     side rather than stretching one field across a tablet. Mobile-first: this is the only
     breakpoint the surface has. */
  @media (min-width: 560px){
    #reportResultSeats{ display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-3); }
    .rcPendingRow{ display:grid; grid-template-columns:1fr auto; align-items:center; }
    .rcPendingOpen{ width:auto; grid-column:2; grid-row:1 / span 3; }
  }

@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    scroll-behavior:auto !important;
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
}

  /* The watch's workout, shown under the match summary. Same row rhythm as the stats
     table above it so the two read as one closing screen rather than two reports. */
  #workoutSummary{
    display:flex; flex-direction:column; gap:var(--sp-2);
    width:100%; max-width:420px; margin-top:var(--sp-3);
    padding:var(--sp-4); border-radius:var(--r-md);
    background:var(--surface); box-shadow:var(--shadow-md);
  }
  #workoutSummary[hidden]{ display:none; }
  .workoutSummaryRow{
    display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3);
  }
  .workoutSummaryRow span{ color:var(--muted); font-size:13px; }

/* ================= Stage 3.5e D1: inline-style sweep, index.html ================= */
/* One rule per unique declaration set that used to sit inline on an element in
 * index.html. Selectors are the elements own id (specificity 1,0,0) - never a bare
 * class - because a plain class loses to existing rules such as
 * fieldset button:not(.primary):not(.chip):not(.histDel):not(.switch):not([aria-pressed])
 * (specificity 0,5,2, styles.css ~line 153), which an inline style always used to beat
 * regardless of specificity. Elements that had no id got one minted (id="d1genN",
 * counter-based, checked against every id already in the document) purely to carry
 * this rule; nothing elsewhere depends on the name. Off-scale values became tokens
 * in the shared :root block above rather than being rounded onto the --sp-1..6 scale
 * - see that comment. display:none stays inline in index.html (57 pure + 35 hybrid)
 * and is deliberately absent from every rule below; see tests/token-ratchet.test.js
 * and tests/hidden-means-hidden.test.js. */
  #d1gen1{ position:absolute; }
  #d1gen2{ color:var(--accent-ink); font-weight:700; }
  #installSlim{ border:none; box-shadow:none; background:var(--tint-clay); color:var(--clay-ink); border-radius:var(--r-sm); min-height:var(--size-40px); font-size:var(--fs-12-5px); font-weight:700; text-align:right; padding:var(--sp-2) var(--sp-14px); }
  #installBanner, #pushNudge{ background:var(--surface); border:1px solid var(--line-soft); border-radius:var(--r-md); box-shadow:var(--shadow-sm); padding:var(--sp-14px) var(--sp-4); }
  #d1gen3, #d1gen20, #d1gen30, #d1gen31, #d1gen32, #d1gen60, #d1gen86{ display:flex; align-items:center; gap:var(--sp-10px); }
  #d1gen4, #d1gen21, #segMsMode, #d1gen65, #d1gen71, #d1gen75, #firstMatchReplayLabel{ flex:1; min-width:0; }
  #d1gen5, #d1gen22{ font-size:var(--fs-14px); }
  #installHint, #d1gen23, #setupCourtHint, #ratedHint, #profileNudgeText, #d1gen73{ margin-top:var(--sp-2px); }
  #d1gen6, #d1gen24{ display:flex; gap:var(--sp-2); margin-top:var(--sp-10px); }
  #installDo, #installGuide, #installDismiss, #pushNudgeDo, #pushNudgeDismiss{ flex:1; min-height:var(--size-42px); }
  #iosGuideWrap{ position:fixed; inset:0; z-index:var(--z-16); background:var(--sheet-scrim); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); align-items:flex-end; justify-content:center; }
  #iosGuide{ width:100%; max-width:var(--maxw-430px); max-height:var(--maxw-90dvh); overflow-y:auto; background:var(--bg); background-image:var(--bg-grad); border-radius:var(--r-lg) var(--r-lg) 0 0; padding:var(--sp-4) var(--sp-4) max(var(--sp-5), env(safe-area-inset-bottom)); box-shadow:0 -12px 44px var(--sheet-shadow-color); animation:sheetUp .28s var(--ease); position:relative; }
  #iosGuideTitle{ font-size:var(--fs-20px); font-weight:900; margin-bottom:var(--sp-1); }
  #d1gen7{ margin-bottom:var(--sp-4); }
  #d1gen8, #loggedOutUI, #authMain, #chatsListView{ display:flex; flex-direction:column; gap:var(--sp-3); }
  #d1gen9, #d1gen13, #d1gen17, #d1gen62, #d1gen68, #d1gen77{ display:flex; align-items:center; gap:var(--sp-3); }
  #d1gen10, #d1gen14, #d1gen18{ width:var(--size-30px); height:var(--size-30px); }
  #d1gen11, #d1gen15, #d1gen19, #segServer, #setupRegion, #setupCity, #ngRegion, #ngCity, #acRegion, #acCity, #inviteLinkBtn, #leaveGroupBtn, #adminPick, #groupCode, #addFriendInput, #profileNudgeDo, #pfAvatarBtn, #newFriend, #icPass, #openWatchGuide, #mintWatchPair{ flex:1; }
  #d1gen12, #d1gen16{ display:inline-grid; place-items:center; width:var(--size-26px); height:var(--size-26px); border-radius:var(--r-7px); background:var(--tint); color:var(--accent-ink); vertical-align:middle; margin-inline-start:var(--sp-1); }
  #iosGuideDone{ width:100%; margin-top:var(--sp-18px); min-height:var(--tap-min); }
  #loggedInUI{ align-items:center; gap:var(--sp-3); }
  /* The identity row is a real <button> (opens the own player card), styled back to a row:
     the base button chrome (surface, border, shadow, centered padding) would turn the
     account strip into two side-by-side pills. The GLOBAL button:focus-visible ring is the
     visible focus state, deliberately not overridden here; min-height keeps the tap target
     on the --tap-min token the base rule already guarantees. text-align:start keeps RTL. */
  #ownIdentityBtn{
    display:flex; align-items:center; gap:var(--sp-3);
    flex:1; min-width:0; min-height:var(--tap-min);
    background:none; border:none; box-shadow:none;
    padding:var(--sp-1); border-radius:var(--r-sm); text-align:start;
  }
  #userAvatar{ width:var(--size-44px); height:var(--size-44px); border-radius:50%; object-fit:cover; background:var(--sub); }
  #userAvatarFallback{ width:var(--size-44px); height:var(--size-44px); border-radius:50%; background:var(--accent); color:var(--on-accent); display:flex; align-items:center; justify-content:center; font-weight:900; font-size:var(--fs-18px); }
  #d1gen25, #listSheetBody{ display:flex; flex-direction:column; }
  #logoutBtn{ margin-inline-start:auto; }
  #googleBtn{ display:flex; align-items:center; justify-content:center; gap:var(--sp-10px); background:var(--google-btn-bg); color:var(--google-btn-fg); border:1px solid var(--google-btn-border); font-weight:700; min-height:var(--size-50px); }
  #d1gen26{ display:flex; align-items:center; gap:var(--sp-10px); color:var(--muted); font-size:var(--fs-12-5px); font-weight:700; }
  #d1gen27, #d1gen28{ flex:1; border-top:1px dashed var(--line-soft); }
  #authSubmit{ min-height:var(--size-52px); }
  #forgotBtn{ background:none; border:none; color:var(--accent-ink); font-weight:700; align-self:center; min-height:var(--size-44px); padding:var(--sp-2) var(--sp-3); cursor:pointer; }
  #rsConfirm{ min-height:var(--size-50px); }
  #rsBack{ background:none; border:none; color:var(--muted); align-self:center; min-height:auto; padding:var(--sp-1); cursor:pointer; }
  #d1gen29{ display:flex; flex-direction:column; gap:var(--sp-10px); }
  #d1gen33{ align-self:flex-start; color:var(--accent-ink); font-weight:800; font-size:var(--fs-13-5px); }
  #d1gen34, #d1gen35, #d1gen40, #d1gen41, #d1gen48, #d1gen56, #d1gen84{ display:flex; gap:var(--sp-2); }
  #setupCourtFilter{ margin:var(--sp-2px) 0 0; }
  #setupCourtSel, #setupCourt, #ngCourt, #ngPlace, #leagueH2H{ margin-top:var(--sp-6px); }
  #d1gen36{ text-align:center; }
  #d1gen37{ display:flex; justify-content:flex-end; }
  #d1gen38{ margin-top:var(--sp-14px); }
  #d1gen39, #mmFilters, #d1gen51{ display:flex; gap:var(--sp-2); flex-wrap:wrap; }
  #boardDate{ flex:1; min-width:var(--size-120px); }
  #boardMode, #boardRegion, #boardCity{ flex:1; min-width:var(--size-96px); }
  #newGameForm, #activityFeed, #chatsContent{ flex-direction:column; gap:var(--sp-3); }
  #ngInviteBanner{ background:var(--tint); border:1px solid var(--accent); border-radius:var(--r-sm); padding:var(--sp-10px) var(--sp-3); font-weight:800; color:var(--accent-ink); }
  #ngWhen{ font:inherit; color:var(--fg); background:var(--sub); border:1.5px solid var(--line-soft); border-radius:var(--r-sm); min-height:var(--tap-min); padding:var(--sp-2) var(--sp-3); }
  #ngCourtDetails{ margin-top:var(--sp-2); background:var(--sub); border-radius:var(--r-sm); padding:var(--sp-10px) var(--sp-3); font-size:var(--fs-small); flex-direction:column; gap:var(--sp-6px); }
  #courtReportLink{ border:none; background:none; box-shadow:none; min-height:var(--size-32px); color:var(--muted); font-size:var(--fs-12-5px); text-decoration:underline; }
  #addCourtBtn, #editCourtBtn{ min-height:var(--size-40px); font-size:var(--fs-small); }
  #addCourtForm, #editCourtForm{ flex-direction:column; gap:var(--sp-2); }
  #acPhone, #acSite, #ecPhone, #ecSite{ direction:ltr; text-align:right; }
  #acSubmit, #ecSubmit, #addFriendBtn{ min-height:var(--size-44px); }
  #d1gen42, #d1gen58{ overflow-x:auto; }
  /* THE LEAGUE TABLE'S OWN SCROLL (owner ruling 2026-08-13, ledger 3W) — the ONE seam every
     league table renders through, deliberately not shared with #d1gen58 (the community
     follow-table is not a league table; extending it is a product call, recorded not taken).
     -webkit-overflow-scrolling keeps iOS momentum inside the WebView; overscroll-behavior
     stops a finished inner scroll from dragging the page under it. */
  #d1gen42{
    max-height:var(--table-viewport);
    overflow-y:auto;
    -webkit-overflow-scrolling:touch;
    overscroll-behavior:contain;
  }
  /* The pinned header: sticky against the container above. It slides OVER rows (including
     tr.me's --tint wash), so it must paint on the card's own surface — a transparent sticky
     header is the classic unreadable-scroll bug. The 2px --line-soft rule the .stt th
     border-bottom draws cannot travel with a sticky cell under border-collapse, so inside
     this container the same 2px moves to a box-shadow and the border is dropped — identical
     ink, one seam, both themes by token. */
  #d1gen42 .stt th{
    position:sticky;
    top:0;
    z-index:2;
    background:var(--surface);
    border-bottom:none;
    box-shadow:0 2px 0 var(--line-soft);
  }
  /* THE TOP-100 WINDOW's three control rows (owner ruling 2026-08-13, ledger 3Y — the
     constant and the ruling live at LEAGUE_TABLE_WINDOW, league.js). The expand button and
     the gap separator live INSIDE #d1gen42 so revealing more grows the scrollable content,
     never the page (3W). The viewer's own out-of-window row is PINNED — sticky against the
     container bottom, the mirror of the sticky header above — and like that header it must
     paint opaque: --tint is an opaque token by design (its own definition says so), so the
     rows scrolling beneath never bleed through, in both themes by token. */
  .lgMoreRow td{ padding:var(--sp-2); text-align:center; }
  #leagueMoreBtn{ width:100%; min-height:var(--tap-min); font-size:var(--fs-small); }
  .lgGapRow td{ text-align:center; color:var(--muted); font-weight:800; padding:var(--sp-1) 0; }
  #d1gen42 .stt tr.tailSelf td{
    position:sticky;
    bottom:0;
    z-index:1;
    background:var(--tint);
    box-shadow:0 -2px 0 var(--line-soft);
  }
  #leagueEmpty{ text-align:center; padding:var(--sp-18px) var(--sp-2); }
  #d1gen43, #d1gen52{ width:var(--size-44px); height:var(--size-44px); color:var(--accent-ink); margin-bottom:var(--sp-2); }
  #d1gen44, #d1gen53{ font-weight:800; }
  #d1gen45, #d1gen54, #segProfile{ margin-top:var(--sp-1); }
  #d1gen46, #crownBtn, #resetLeague, #mmMineNote, #anStatus{ margin-top:var(--sp-2); }
  #d1gen47{ display:flex; gap:var(--sp-2); margin-top:var(--sp-6px); }
  #addAdminRow{ gap:var(--sp-2); }
  #deleteGroupBtn{ margin-top:var(--sp-2); color:var(--danger); border-color:color-mix(in srgb, var(--danger) 45%, transparent); }
  #mmView{ margin-bottom:var(--sp-3); }
  #mmResult, #mmOpp, #mmRange{ flex:1; min-width:var(--size-110px); }
  #mmList, #mmGrid{ margin-top:var(--sp-10px); }
  #mmMineEmpty{ text-align:center; padding:var(--sp-18px); }
  #d1gen49{ display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2); }
  #mmPrev, #mmNext{ min-width:var(--size-40px); }
  #mmMonth, #d1gen72{ font-size:var(--fs-body); }
  #mmDay, #anLocked{ margin-top:var(--sp-3); }
  #d1gen50{ margin:0 0 var(--sp-10px); }
  #msContent, #friendsContent{ flex-direction:column; gap:var(--sp-4); }
  #msGraph{ width:100%; height:var(--size-130px); display:block; }
  #msEmpty{ text-align:center; padding:var(--sp-5) var(--sp-2); }
  #friendsMain{ display:flex; flex-direction:column; gap:var(--sp-4); }
  #d1gen55, #d1gen57{ margin:var(--sp-1) 0; }
  #d1gen59{ display:flex; align-items:center; justify-content:space-between; gap:var(--sp-10px); }
  #chatsListTitle{ margin:0; }
  #chatsList{ display:flex; flex-direction:column; gap:var(--sp-2); }
  #chatThreadView{ flex-direction:column; gap:var(--sp-10px); }
  #chatBack{ min-height:var(--size-40px); padding:0 var(--sp-3); }
  #chatThreadTitle{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  #chatMessages{ display:flex; flex-direction:column; gap:var(--sp-2); height:var(--size-52vh); overflow-y:auto; padding:var(--sp-6px) var(--sp-2px); }
  #d1gen61{ display:flex; gap:var(--sp-2); align-items:flex-end; }
  #chatSend{ height:var(--tap-min); flex:none; padding:0 var(--sp-18px); font-size:var(--fs-15px); }
  #profileNudge{ background:var(--surface); border:1px solid var(--line-soft); border-radius:var(--r-md); box-shadow:var(--shadow-sm); padding:var(--sp-14px) var(--sp-4); margin-bottom:var(--sp-14px); }
  #d1gen63{ width:var(--size-44px); height:var(--size-44px); flex:none; }
  #d1gen64{ width:var(--size-24px); height:var(--size-24px); }
  #d1gen66{ font-size:var(--fs-15px); }
  #d1gen67{ display:flex; gap:var(--sp-2); margin-top:var(--sp-3); }
  #d1gen69{ width:var(--size-46px); height:var(--size-46px); }
  #d1gen70{ width:var(--size-26px); height:var(--size-26px); }
  #clickerHeroBtn, #pfGoogleLink{ display:flex; align-items:center; justify-content:center; gap:var(--sp-2); }
  #d1gen74, #d1gen76{ cursor:default; }
  #tourReplay, #firstMatchReplay{ min-height:var(--size-38px); display:inline-flex; align-items:center; gap:var(--sp-6px); }
  #pfAvatarPreview{ width:var(--size-64px); height:var(--size-64px); border-radius:50%; object-fit:cover; background:var(--sub); }
  #d1gen78{ margin:0 0 var(--sp-3); }
  #anHeHint{ color:var(--accent-ink); }
  #d1gen79{ margin-top:var(--sp-4); }
  #anSaveBtn{ width:100%; margin-top:var(--sp-3); }
  #d1gen80{ display:flex; gap:var(--sp-2); margin-top:var(--sp-2); }
  #d1gen81{ text-align:center; margin:var(--sp-22px) 0 var(--sp-6px); }
  #d1gen82{ color:var(--muted); text-decoration:underline; }
  #endFriendly{ margin-top:var(--sp-6px); font-size:var(--fs-12-5px); font-weight:800; color:var(--accent-ink); background:var(--tint); border-radius:var(--r-999px); padding:var(--sp-5px) var(--sp-14px); }
  #d1gen83{ text-align:center; margin-bottom:var(--sp-1); }
  #kudosBtn{ align-items:center; justify-content:center; gap:var(--sp-2); }
  #instantConfirm{ max-width:var(--maxw-440px); width:100%; }
  #d1gen85{ font-weight:800; color:var(--muted); }
  #sumStats{ width:100%; max-width:var(--maxw-420px); }
  #listSheetTitle{ font-size:var(--fs-19px); font-weight:900; margin-bottom:var(--sp-3); }
  #segStatSet{ margin-bottom:var(--sp-10px); }
  #d1gen87, #d1gen88{ display:flex; align-items:center; gap:var(--sp-10px); margin-top:var(--sp-2); }
  #d1gen89, #d1gen90{ display:flex; gap:var(--sp-2); align-items:center; }
