/* ══════════════════════════════════════════════════════════════════
   Table Blue · Scoreboard
   Blue is the table, white rules are its lines, orange is the ball.
   Figures are always mono + tabular so columns of digits line up.
   ══════════════════════════════════════════════════════════════════ */

/* iPhone Safari enlarges text on its own when the phone turns to
   landscape - the nav most of all. Sizes here are already set for every
   width, so it gets told not to. */
html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

/* `hidden` always hides. Without this an element's own display (flex,
   grid, inline-flex) outranks the attribute's default - which is how the
   poster preview's Close set it hidden and left it on screen. */
[hidden] {
    display: none !important;
}

:root {
    --bg: #071A2B;
    --s1: #0D2740;
    --s2: #123553;
    --tx: #EAF2F8;
    --mu: #8DAFC8;
    --ball: #FF6B18;
    --chalk: #FFFFFF;
    --line-strong: rgba(255, 255, 255, .30);
    --line: rgba(120, 166, 198, .32);
    --line-soft: rgba(120, 166, 198, .12);

    /* Latin faces come from Google Fonts, which is unreachable from
       mainland China - every stack below therefore names real local
       fallbacks, and the CJK stack is system-only by design so Chinese
       never depends on a webfont at all. */
    --f-disp: "Archivo", "Helvetica Neue", Arial, sans-serif;
    --f-body: "IBM Plex Sans", "Helvetica Neue", Arial, sans-serif;
    --f-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    --f-cjk: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", sans-serif;
    /* Chinese mode keeps the Latin faces for Latin and digits: the browser picks
       a font per character, and Plex/Archivo only carry Latin (see fonts.css'
       unicode-range), so Chinese characters fall through to the CJK list.
       Replacing the whole stack with --f-cjk drew every rating, date and score
       in PingFang's Latin instead - different shapes, and not the fixed-width
       Plex Mono the labels use in English (2026-10-01). */
    --f-zh-mono: "IBM Plex Mono", var(--f-cjk);

    --rail-w: 216px;

    /* Column bounds. The minima are the measured ink of whichever is
       widest - header or content, English or Chinese - plus padding:
       ASSOCIATION vs a flag and five CJK characters, YOB vs 出生年份,
       RATING vs 2000.00, and so on. */
    --cell-x: 8px;
    --body-x: 20px;
    --th-track: .14em;
}

* {
    box-sizing: border-box;
}

/* One knob for the whole type scale - every size below is rem. */
html {
    font-size: 18px;
}

@media (max-width: 800px) {
    html {
        font-size: 17px;
    }
}

html[lang="zh"] {
    --f-disp: "Archivo", var(--f-cjk);
    --f-body: "IBM Plex Sans", var(--f-cjk);
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--tx);
    font-family: var(--f-body);
    font-size: 16px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
}

a {
    color: inherit;
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

:focus-visible {
    outline: 2px solid var(--ball);
    outline-offset: 2px;
}

.disp {
    font-family: var(--f-disp);
    font-weight: 800;
    letter-spacing: -.035em;
    line-height: 1;
}

html[lang="zh"] .disp {
    font-weight: 700;
    letter-spacing: 0;
}

.cap {
    font: 400 .63rem/1.5 var(--f-mono);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--mu);
}

html[lang="zh"] .cap {
    font-family: var(--f-zh-mono);
    letter-spacing: .06em;
    text-transform: none;
}

/* Between 801 and 1000px the table would not otherwise fit, so the rail
   and the cell padding give up the ~100px needed rather than the table
   overflowing into a horizontal scroll. */
@media (min-width: 801px) and (max-width: 1000px) {
    :root {
        /* Rail deliberately unchanged across this boundary - only the
           cell padding, tracking and column widths tighten. */
        --cell-x: 5px;
        --body-x: 11px;
        /* tighter tracking buys back the header width rank gives up */
        --th-track: .06em;
    }
}

/* ── layout ── */
.layout {
    display: grid;
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
    min-height: 100vh;
}

/* ── rail ── */
.rail {
    background: var(--s1);
    border-right: 1px solid var(--line-strong);
    padding: 18px 15px 15px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.rail .brand {
    /* Sized so "Table Tennis / WHR Ranking" holds two lines. The brand
       shares the rail's top line with a 58px language toggle, which
       leaves it about 118px - tighter than it looks. */
    font-size: .98rem;
}

/* gender switch */
.sw {
    display: flex;
    border: 1px solid var(--line);
    border-radius: 3px;
    overflow: hidden;
}

.sw button {
    flex: 1;
    font: 600 .66rem/1 var(--f-body);
    letter-spacing: .07em;
    text-transform: uppercase;
    padding: 7px 4px;
    color: var(--mu);
    background: transparent;
    border: 0;
    cursor: pointer;
    transition: background-color .15s, color .15s;
}

html[lang="zh"] .sw button {
    letter-spacing: .04em;
    text-transform: none;
}

.sw button:hover:not([aria-pressed="true"]) {
    background: var(--s2);
    color: var(--tx);
}

.sw button[aria-pressed="true"] {
    background: rgba(234, 242, 248, .13);
    color: var(--tx);
    box-shadow: inset 0 -2px 0 var(--ball);
}

/* nav */
.rail nav,
.drawer {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.rail nav a,
.drawer a {
    font: 500 .77rem/1 var(--f-body);
    color: var(--mu);
    text-decoration: none;
    padding: 7px 0 7px 10px;
    border-left: 2px solid transparent;
    transition: color .15s, border-color .15s;
}

.rail nav a:hover,
.drawer a:hover {
    color: var(--tx);
}

.rail nav a.on,
.drawer a.on {
    color: var(--tx);
    font-weight: 600;
    border-left-color: var(--ball);
}

/* ── rolling top-ten roster: one card at a time, sliding sideways ── */
.roster {
    display: flex;
    flex-direction: column;
    gap: 10px;
    border-top: 1px solid rgba(120, 166, 198, .24);
    padding-top: 13px;
}

/* WOMEN'S SINGLES · TOP 10 is 24 characters, which at the .cap tracking
   measures 207px against the 186px the rail has inside its padding. The
   tracking is what gives way - the label is the widest fixed string in
   the rail and the only one that has to be told. */
.roster .cap {
    letter-spacing: .04em;
    /* The card under it is centred - photo, standing, name - so a label
       hard against the left edge read as belonging to something else. */
    text-align: center;
}

.vp {
    overflow: hidden;
    width: 100%;
}

.track {
    display: flex;
    width: 100%;
    animation: roster-slide 30s infinite;
}

/* A portrait card: the face, then the standing, then who it is. Centred,
   because the photograph is and everything under it reads as a caption to
   it. */
.rcard {
    flex: 0 0 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    text-align: center;
    text-decoration: none;
}

/* Its natural size - the thumbnail is written at 96px wide, so anything
   larger here is an upscale of a file we chose to keep small. */
/* No background and no radius: the headshots are cut out, so the rail's
   own surface is what should be behind them. A tint here was a leftover
   from when the thumbnails were opaque - it showed as a lighter rectangle
   the moment they gained an alpha channel - and a corner radius on a
   transparent image only risks clipping a shoulder. */
.rcard .face {
    display: block;
    width: 96px;
    height: auto;
}

/* Rank and rating share a line, aligned on their baselines so the small
   figure sits on the big one's foot rather than floating in the middle of
   it. */
.rcard .stand {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 9px;
}

.rcard .rk {
    font: 600 2rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    letter-spacing: -.02em;
    color: var(--chalk);
}

.rcard .rt {
    font: 600 1.15rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--ball);
}

.rcard .who {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    max-width: 100%;
    min-width: 0;
    font-weight: 600;
    font-size: .85rem;
    line-height: 1.2;
    color: var(--chalk);
}

.rcard .who > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.rcard .flag {
    flex: 0 0 auto;
    width: 16px;
    height: 11px;
}

@keyframes roster-slide {
    0%, 7%    { transform: translateX(0) }
    10%, 17%  { transform: translateX(-100%) }
    20%, 27%  { transform: translateX(-200%) }
    30%, 37%  { transform: translateX(-300%) }
    40%, 47%  { transform: translateX(-400%) }
    50%, 57%  { transform: translateX(-500%) }
    60%, 67%  { transform: translateX(-600%) }
    70%, 77%  { transform: translateX(-700%) }
    80%, 87%  { transform: translateX(-800%) }
    90%, 97%  { transform: translateX(-900%) }
    100%      { transform: translateX(-1000%) }
}

.vp:hover .track,
.vp:focus-within .track {
    animation-play-state: paused;
}

/* ── rail footer ── */
.rfoot {
    border-top: 1px solid rgba(120, 166, 198, .24);
    padding-top: 13px;
    display: flex;
    flex-direction: column;
    text-align: center;
}

.rfoot p {
    margin: 0;
    font-size: .63rem;
    line-height: 1.65;
    color: var(--mu);
}

.rfoot p.lk {
    margin-bottom: 7px;
}

.rfoot a {
    color: #9DC4DC;
    text-decoration: none;
    border-bottom: 1px solid rgba(157, 196, 220, .32);
}

.rfoot a:hover {
    color: var(--tx);
    border-bottom-color: var(--tx);
}

.rfoot .pv {
    font: 400 .61rem/1.7 var(--f-mono);
    color: #6B8CA3;
    margin-top: 13px;
}

html[lang="zh"] .rfoot .pv {
    font-family: var(--f-zh-mono);
}

/* brand and language toggle share the top line of the rail */
.rail-top {
    display: flex;
    align-items: flex-start;
    gap: 8px;
}

.rail-top .brand {
    flex: 1;
    min-width: 0;
}

.lang-toggle {
    display: flex;
    border: 1px solid var(--line);
    border-radius: 3px;
    overflow: hidden;
    flex: 0 0 auto;
}

.lang-toggle button {
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--mu);
    font: 600 .66rem/1 var(--f-body);
    cursor: pointer;
    transition: background-color .15s, color .15s;
}

.lang-toggle button[lang="zh"] {
    font-family: var(--f-cjk);
    font-size: .74rem;
}

.lang-toggle button:hover:not([aria-pressed="true"]) {
    background: var(--s2);
    color: var(--tx);
}

.lang-toggle button[aria-pressed="true"] {
    background: rgba(234, 242, 248, .13);
    color: var(--tx);
    box-shadow: inset 0 -2px 0 var(--ball);
}

/* ── main ── */
.body {
    padding: 18px var(--body-x) 24px;
    min-width: 0;
}

.bhead {
    display: flex;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.bhead .t {
    font-size: 1.3rem;
    margin: 0;
}

.tbl-scroll {
    overflow-x: auto;
}

table {
    width: 100%;
    border-collapse: collapse;
    /* fixed, so a column never resizes itself around whichever names
       happen to be in it - switching draw or language must not reflow
       the table. Every column but the name is pinned, so the name is
       what absorbs any leftover width. */
    table-layout: fixed;
}

th {
    font: 500 .64rem/1 var(--f-mono);
    letter-spacing: var(--th-track);
    text-transform: uppercase;
    color: var(--mu);
    text-align: left;
    padding: 0 var(--cell-x) 8px;
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
}

html[lang="zh"] th {
    font-family: var(--f-zh-mono);
    letter-spacing: .05em;
    text-transform: none;
}

td {
    padding: 7px var(--cell-x);
    border-bottom: 1px solid var(--line-soft);
    font-size: .82rem;
    vertical-align: middle;
}

tbody tr:hover {
    background: var(--s1);
}

/* Plain lengths, because clamp() with a percentage inside is ignored for
   table columns under table-layout:fixed - the whole value falls back to
   auto and the columns split evenly instead. The per-band values below are
   the bounds; the name column absorbs everything in between. */
.c-rank   { width: clamp( 52px, calc(0.055 * (100vw - 256px)), 104px) }
.c-ittf   { width: clamp( 52px, calc(0.055 * (100vw - 256px)), 104px) }
.c-name   { width: auto }
.c-assoc  { width: clamp(110px, calc(0.128 * (100vw - 256px)), 240px) }
.c-yob    { width: clamp( 68px, calc(0.074 * (100vw - 256px)), 140px) }
.c-rating { width: clamp( 88px, calc(0.108 * (100vw - 256px)), 200px) }
.c-err    { width: clamp( 78px, calc(0.088 * (100vw - 256px)), 168px) }
.c-trend  { width: clamp( 86px, calc(0.150 * (100vw - 256px)), 300px) }

td.rk {
    font: 600 .95rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--tx);
}

tr.one td.rk {
    color: var(--ball);
}

td.num {
    font: 400 .68rem/1 var(--f-mono);
    letter-spacing: .07em;
    color: var(--mu);
    font-variant-numeric: tabular-nums;
}

td.rating {
    font: 600 .86rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
}

td.nm a {
    font-weight: 600;
    text-decoration: none;
    color: var(--tx);
}

td.nm a:hover {
    color: var(--ball);
}

.state {
    text-align: center;
    padding: 28px 8px;
    color: var(--mu);
    font-style: italic;
}

.flag {
    width: 18px;
    height: 12px;
    border-radius: 1.5px;
    display: block;
    object-fit: cover;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .18);
}

td.assoc {
    white-space: nowrap;
    overflow: hidden;
}

.assoc-cell {
    display: flex;
    align-items: center;
    gap: 7px;
}

.spark {
    display: block;
    width: 100%;
    height: 17px;
}

/* ── long names roll instead of truncating (phones) ── */
.nm-wrap {
    display: block;
    overflow: hidden;
    max-width: 100%;
}

.nm-wrap > a {
    display: inline-block;
    white-space: nowrap;
}

.nm-wrap.rolling > a {
    animation: name-roll var(--roll-dur, 8s) ease-in-out infinite;
}

@keyframes name-roll {
    0%, 14%   { transform: translateX(0) }
    50%, 64%  { transform: translateX(calc(-1 * var(--roll-by, 0px))) }
    100%      { transform: translateX(0) }
}

/* ── phone chrome, hidden on desktop ── */
.mobile-bar,
.mobile-switch,
.drawer {
    display: none;
}

/* ══════════════ phones ══════════════ */
@media (max-width: 800px) {
    /* One column, and the rail is kept in the flow rather than hidden so
       its footer can sit at the foot of the page. `1fr auto` is what pins
       it there: with both rows auto-sized, grid's default stretch would
       split the spare height between them and leave the footer floating
       in the middle of a short page. `order` reverses them, and grid
       auto-placement honours order-modified document order. */
    .layout {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: 1fr auto;
    }

    .body {
        order: 1;
    }

    /* Everything else in the rail earns its place only on a desktop: the
       nav is already in the drawer, the draw switch has its own bar, and
       the rolling ten has nowhere to roll. The credits do not depend on
       any of that, so they stay. */
    .rail {
        display: flex;
        order: 2;
        background: transparent;
        border-right: 0;
        padding: 0 13px 24px;
        gap: 0;
    }

    .rail-top,
    #rail-switch,
    #rail-nav,
    .roster {
        display: none;
    }

    .mobile-bar {
        display: flex;
        align-items: center;
        gap: 11px;
        padding: 10px 13px;
        background: var(--s1);
        border-bottom: 1px solid var(--line);
        position: sticky;
        top: 0;
        z-index: 20;
    }

    /* .disp sets line-height 1, which is shorter than Archivo's own
       ascender-plus-descender - harmless until overflow:hidden (here for
       the ellipsis) starts clipping what sticks out, which shaved the
       tail off the g in "Table Tennis WHR Ranking". 1.4 clears it and
       still measures under the 28px language toggle, so the bar height,
       and the sticky offsets pinned to it, do not move. */
    .mobile-bar .brand {
        font-size: .88rem;
        line-height: 1.4;
        flex: 1;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .burger {
        display: flex;
        flex-direction: column;
        gap: 3px;
        background: transparent;
        border: 0;
        padding: 4px 2px;
        cursor: pointer;
        flex: 0 0 auto;
    }

    .lang-toggle button {
        width: 26px;
        height: 26px;
    }

    .burger span {
        width: 16px;
        height: 1.7px;
        background: var(--tx);
        border-radius: 1px;
        display: block;
        transition: transform .2s, opacity .2s;
    }

    .burger[aria-expanded="true"] span:nth-child(1) {
        transform: translateY(4.7px) rotate(45deg);
    }

    .burger[aria-expanded="true"] span:nth-child(2) {
        opacity: 0;
    }

    .burger[aria-expanded="true"] span:nth-child(3) {
        transform: translateY(-4.7px) rotate(-45deg);
    }

    .drawer {
        display: none;
        background: var(--s1);
        border-bottom: 1px solid var(--line);
        padding: 8px 13px 12px;
        position: sticky;
        top: 45px;
        z-index: 19;
    }

    .drawer.open {
        display: flex;
    }

    .drawer a {
        padding: 8px 0 8px 10px;
        font-size: .82rem;
    }

    .mobile-switch {
        display: block;
        padding: 10px 13px 4px;
    }

    .body {
        padding: 8px var(--body-x) 20px;
    }

    /* The draw switch is what usually separates the sticky bar from the
       content; without it the page starts flush against the bar. */
    body.no-mswitch .body {
        padding-top: 15px;
    }

    .nm-wrap {
        max-width: 40vw;
    }

    /* draw name and update time are always stacked - letting them share
       a row when they happen to fit made the header jump between
       languages */
    .bhead {
        flex-direction: column;
        align-items: flex-start;
        margin-bottom: 15px;
        gap: 9px;
    }

    /* The ranking page's header keeps the stack, but the switch rides on
       the title's row: the update time is ~300px of caps and leaves no
       room for it, the title leaves plenty. */
    .vhead {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto auto;
        align-items: center;
        column-gap: 12px;
    }

    .vhead .cap {
        grid-column: 1 / -1;
        grid-row: 2;
    }

    .vhead .vsw {
        grid-column: 2;
        grid-row: 1;
    }

    .vhead .pexport {
        grid-column: 3;
        grid-row: 1;
    }


    /* five visible columns; name takes the remainder and rolls when a
       long one does not fit, which is expected rather than avoided */
    .c-rank   { width: 40px }
    .c-ittf   { width: 42px }
    .c-name   { width: auto }
    .c-assoc  { width: 30px }
    .c-rating { width: 108px }

    table {
        max-width: none;
    }

    .bhead .t {
        font-size: 1.05rem;
    }

    :root {
        --cell-x: 5px;
        --body-x: 11px;
    }

    /* assoc, birth year, trend and the standalone error column all drop;
       the flag carries the association and the error joins the rating */
    .c-yob,
    .c-err,
    .c-trend,
    td.yob,
    td.err,
    td.trend {
        display: none;
    }

    /* the association column narrows to just its flag, and its header
       goes with it - ASSOCIATION is far wider than the column and was
       colliding with the next one. Hidden visually, still announced. */
    .assoc-label,
    .c-assoc > span,
    .s-assoc > span {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    td.rating {
        white-space: nowrap;
    }

    .rating .short {
        font: 600 .95rem/1 var(--f-mono);
        font-variant-numeric: tabular-nums;
        color: var(--chalk);
    }

    .rating .e {
        font: 400 .63rem/1 var(--f-mono);
        font-variant-numeric: tabular-nums;
        color: var(--mu);
        margin-left: 5px;
    }
}


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

    .nm-wrap.rolling > a {
        animation: none;
    }

    .nm-wrap {
        overflow: visible;
    }

    .nm-wrap > a {
        white-space: normal;
    }

    * {
        transition-duration: .01ms !important;
    }
}

/* ── bits referenced from rankings.js ── */

/* desktop shows the precise figures in their own columns; the phone
   shows whole numbers with the error tucked beside the rating */
.rating .short,
.rating .e {
    display: none;
}

@media (max-width: 800px) {
    .rating .full {
        display: none;
    }

    .rating .short,
    .rating .e {
        display: inline;
    }
}

/* a code with no flag is only worth printing where the label is hidden */
.flag-fallback {
    display: none;
}

@media (max-width: 800px) {
    .flag-fallback {
        display: inline;
    }
}

/* sparklines inherit their colour, so the leader's line is the accent
   and everyone else recedes */
td.trend {
    color: rgba(234, 242, 248, .45);
}

tr.one td.trend {
    color: var(--ball);
}

.flag-fallback {
    font: 400 .62rem/1 var(--f-mono);
    letter-spacing: .08em;
    color: var(--mu);
}

/* ══════════════════════════════════════════════════════════════════
   Historical rankings
   ══════════════════════════════════════════════════════════════════ */

/* Year selector. The page is paginated by ISO year; a chip per year
   wrapped onto three lines at narrow widths and was unusable on a phone,
   so it is a menu. */
.yearsel {
    align-self: center;
}

.yearsel select {
    font: 500 .74rem/1 var(--f-mono);
    letter-spacing: .04em;
    color: var(--tx);
    background: var(--s1);
    border: 1px solid var(--line);
    border-radius: 3px;
    padding: 7px 26px 7px 10px;
    cursor: pointer;
    appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, var(--mu) 50%),
                      linear-gradient(135deg, var(--mu) 50%, transparent 50%);
    background-position: calc(100% - 15px) 52%, calc(100% - 10px) 52%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

.yearsel select:hover {
    border-color: var(--mu);
}

.yearsel select option {
    background: var(--s1);
    color: var(--tx);
}

/* Column headers stay put; a 22-year table loses its headings otherwise.
   This needs the wrapper NOT to be a scroll container: overflow-x:auto
   makes the other axis compute to auto as well, which turns .tbl-scroll
   into the sticky containing block - and since it has no height it never
   scrolls, so `top` only shoved the header down over the first row. The
   history table is fixed-layout and sized to fit, so it can do without
   the horizontal scrollport. */
.tbl-sticky {
    overflow: visible;
}

.hist thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--bg);
}

.hist {
    table-layout: fixed;
}

.h-date { width: clamp(78px, calc(0.095 * (100vw - 256px)), 118px) }
.h-p1,
.h-p2,
.h-p3,
.h-p4,
.h-p5 { width: auto }

td.hd a {
    display: block;
    text-decoration: none;
    color: inherit;
}

td.hd .wk {
    display: block;
    font-weight: 600;
    font-size: .78rem;
    line-height: 1.25;
    color: var(--tx);
    white-space: nowrap;
}

td.hd .dt {
    display: block;
    white-space: nowrap;
    font: 400 .66rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--mu);
    margin-top: 3px;
}

td.hd a:hover .wk,
td.hd a:hover .dt {
    color: var(--ball);
}

td.hp {
    line-height: 1.25;
}

td.hp a {
    font-weight: 600;
    font-size: .78rem;
    color: var(--tx);
    text-decoration: none;
}

td.hp a:hover {
    color: var(--ball);
}

td.hp .hr {
    display: flex;
    align-items: center;
    gap: 5px;
    font: 400 .66rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--mu);
    margin-top: 3px;
}

td.hp .hr b {
    font-weight: 400;
}

td.hp .hr .flag {
    flex: 0 0 auto;
    width: 15px;
    height: 10px;
}

/* First place is the column this page is read for. */
td.hp.lead a {
    color: var(--chalk);
}

/* td.hp a:hover ties with the rule above on specificity - one element and
   two classes either way - so the later one won and first place was the
   one name on the page that did not light up. Naming both classes settles
   it without moving anything. */
td.hp.lead a:hover {
    color: var(--ball);
}

td.hp.lead .hr {
    color: var(--ball);
}

td.hp.empty {
    color: var(--mu);
}

/* Back sits inside the subtitle, so it inherits its type entirely and
   only needs to look like a link. */
.backlink {
    color: var(--mu);
    text-decoration: none;
    border-bottom: 1px solid rgba(141, 175, 200, .35);
    transition: color .15s, border-color .15s;
}

.backlink:hover {
    color: var(--ball);
    border-bottom-color: var(--ball);
}

@media (max-width: 800px) {
    .h-date { width: 70px }

    /* .bhead is a column here, so align-self:center would centre the year
       menu across the page instead of leaving it under the title. This
       has to live after the base .yearsel rule, not before it. */
    .yearsel,
    .yearbox {
        align-self: flex-start;
    }

    /* three names is what a phone can carry legibly */
    .h-p4,
    .h-p5,
    td.hp:nth-child(5),
    td.hp:nth-child(6) {
        display: none;
    }

    /* clears the phone's top bar, sticky at 0 and about 46px tall */
    .hist thead th {
        top: 46px;
    }

}

/* ══════════════════════════════════════════════════════════════════
   Player profile

   Class names here are prefixed where the obvious name is a word the
   rest of the sheet might want later (.pdate, .pev, .pwl) - the rest
   are scoped under .ph / .tl / .ms and cannot collide.

   Sizes are borrowed from whichever element already plays the same role
   on the other pages - .thead span from th, .stat .k and .ptxt from
   .cap, .stat .s from .rfoot .pv, .range button from .sw button - and no
   html[lang="zh"] rule here changes font-size. That is the convention
   the rest of the sheet follows: Chinese swaps the family, the tracking
   and the casing, never the size, so nothing moves when the language is
   toggled. These rules were ported from a standalone mockup and briefly
   broke it.
   ══════════════════════════════════════════════════════════════════ */

.pstate {
    text-align: center;
    padding: 40px 8px;
    color: var(--mu);
    font-style: italic;
}

/* ── identity ── */
.ph {
    display: flex;
    gap: 18px;
    align-items: flex-start;
    padding-bottom: 15px;
    border-bottom: 1px solid var(--line);
}

/* The slot is always in the DOM and the page always asks
   player_photo.php for the picture, because that request is what makes
   ITTF's photo get fetched and cached in the first place. Rendering
   nothing would mean no photo is ever acquired. */
.ph .shot {
    flex: 0 0 88px;
    height: 112px;
    border-radius: 4px;
    background: var(--s2);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

/* With a real photo there is no box: the frame collapses to the image,
   which keeps its own aspect ratio inside the same footprint. Filling a
   fixed 88x112 meant cropping or distorting every headshot. */
.ph .shot.has-photo {
    background: none;
    border-radius: 0;
    height: auto;
    flex: 0 0 auto;
    display: block;
}

.ph .shot img {
    display: block;
    width: auto;
    height: auto;
    /* ITTF headshots are all 400x640. Sized off the height rather than
       the width: at the placeholder box's 88px the photo came out 141px
       tall, far taller than the name and subtitle beside it. 70px wide
       puts it at exactly the 112px the box occupies, so the header keeps
       its proportions whichever state a player is in - the photo is just
       narrower than the box, which needs the width for its text. */
    max-width: 70px;
    max-height: 112px;
    border-radius: 4px;
}

/* Same box as a real headshot, and dimmed - it stands for a player whose
   photograph does not exist, not for the player. */
.ph .shot img.silhouette {
    opacity: .55;
}

.ph .shot .ptxt {
    font: 400 .63rem/1.5 var(--f-mono);
    letter-spacing: .07em;
    color: var(--mu);
    text-align: center;
    padding: 0 6px;
}

html[lang="zh"] .ph .shot .ptxt {
    font-family: var(--f-zh-mono);
    letter-spacing: .04em;
}

.ph .who {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* A long name wraps rather than rolling: .disp is set at line-height 1
   for headings, and clipping it to one line would cut the descenders
   off every 迪 and every g. */
.ph .nm {
    font-size: 1.45rem;
    line-height: 1.15;
}

.ph .sub {
    display: flex;
    align-items: center;
    gap: 6px;
    font: 400 .74rem/1.2 var(--f-mono);
    letter-spacing: .04em;
    color: var(--mu);
    flex-wrap: wrap;
}

html[lang="zh"] .ph .sub {
    font-family: var(--f-zh-mono);
    letter-spacing: 0;
}

.ph .sub i {
    font-style: normal;
    color: rgba(141, 175, 200, .5);
}

.ph .sub .s1 {
    display: flex;
    align-items: center;
    gap: 6px;
}

.ph .sub .s2 {
    white-space: nowrap;
}

.ph .sub .flag {
    width: 16px;
    height: 11px;
}

/* English already prints the three-letter code beside the flag and
   Chinese prints the association name, so a missing PNG needs no
   stand-in here - it would just repeat the code. */
.ph .sub .flag-fallback {
    display: none;
}

/* ── standing, top right of the header ── */
.ph .rankbox {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 5px;
    padding-left: 10px;
    text-align: right;
}

.ph .rankbox .k {
    font: 400 .63rem/1 var(--f-mono);
    letter-spacing: .13em;
    text-transform: uppercase;
    color: var(--mu);
}

html[lang="zh"] .ph .rankbox .k {
    font-family: var(--f-zh-mono);
    letter-spacing: .04em;
    text-transform: none;
}

/* Chalk, not the accent: RATING in the strip below is already orange. */
.ph .rankbox .v {
    font: 600 2rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--chalk);
    white-space: nowrap;
}

/* An unranked player keeps the figure's size so the header does not
   change shape from player to player - only the colour recedes. */
.ph .rankbox .v.none {
    color: var(--mu);
}

.ph .rankbox .s {
    font: 400 .61rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--mu);
    white-space: nowrap;
}

/* Already the muted colour, so an absent ITTF rank only needs to stop
   drawing the eye as a figure would. */
.ph .rankbox .s.none {
    color: rgba(141, 175, 200, .55);
}

/* ── career figures ── */
.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
    border-radius: 4px;
    overflow: hidden;
    margin-top: 13px;
}

.stat {
    background: var(--bg);
    padding: 9px 11px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.stat .k {
    font: 400 .63rem/1 var(--f-mono);
    letter-spacing: .13em;
    text-transform: uppercase;
    color: var(--mu);
}

html[lang="zh"] .stat .k {
    font-family: var(--f-zh-mono);
    letter-spacing: .04em;
    text-transform: none;
}

.stat .v {
    font: 600 1rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
}

.stat .v.acc {
    color: var(--ball);
}

.stat .s {
    font: 400 .61rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--mu);
}

html[lang="zh"] .stat .s {
    font-family: var(--f-zh-mono);
}

/* ── section head, shared by the chart and the timeline ── */
.chead {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}

.chead .range,
.chead .ystep,
.chead .yearsel {
    margin-left: auto;
}

/* Same active treatment as the draw switch and the language toggle. */
.range {
    display: flex;
    border: 1px solid var(--line);
    border-radius: 3px;
    overflow: hidden;
    align-self: center;
}

.range button {
    font: 500 .66rem/1 var(--f-mono);
    letter-spacing: .05em;
    padding: 5px 10px;
    background: transparent;
    border: 0;
    color: var(--mu);
    cursor: pointer;
    transition: background-color .15s, color .15s;
}

html[lang="zh"] .range button {
    font-family: var(--f-zh-mono);
    letter-spacing: 0;
}

.range button:hover:not([aria-pressed="true"]) {
    background: var(--s2);
    color: var(--tx);
}

.range button[aria-pressed="true"] {
    background: rgba(234, 242, 248, .13);
    color: var(--tx);
    box-shadow: inset 0 -2px 0 var(--ball);
}

/* The ranking page's All/Active switch: the player page's range control,
   laid out as equal columns so both buttons take the wider label's width
   in either language and the switch never changes shape. */
.vsw {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
}

.vhead .vsw {
    margin-left: auto;
}

/* With no switch before it (the Top Players page) the export button is
   what goes to the right. */
.vhead > .cap + .pexport {
    margin-left: auto;
}

/* The poster export: an icon-only button beside the All/Active switch, in
   the same box, line and hover as the switch. */
.pexport {
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 4px 7px;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: transparent;
    color: var(--mu);
    cursor: pointer;
    transition: background-color .15s, color .15s;
}

.pexport:hover:not(:disabled) {
    background: var(--s2);
    color: var(--tx);
}

.pexport:disabled {
    cursor: progress;
    opacity: .5;
}

/* ── rating curve ── */
.chartwrap {
    padding-top: 18px;
}

.cv {
    position: relative;
}

.cv canvas {
    display: block;
    width: 100%;
    height: 200px;
    touch-action: pan-y;
}

/* Driven by a class rather than :hover so a touch drag reads it too. */
.readout {
    position: absolute;
    top: 5px;
    left: 7px;
    font: 500 .66rem/1.5 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--tx);
    background: rgba(7, 26, 43, .92);
    border: 1px solid var(--line);
    border-radius: 3px;
    padding: 4px 8px;
    pointer-events: none;
    opacity: 0;
    transition: opacity .12s;
}

.readout.on {
    opacity: 1;
}

/* ── tournament appearances ── */
.tl {
    margin-top: 24px;
}

.thead {
    display: grid;
    gap: 10px;
    padding: 0 4px 8px;
    border-bottom: 1px solid var(--line);
}

.thead span {
    font: 500 .64rem/1 var(--f-mono);
    letter-spacing: var(--th-track);
    text-transform: uppercase;
    color: var(--mu);
    white-space: nowrap;
}

html[lang="zh"] .thead span {
    font-family: var(--f-zh-mono);
    letter-spacing: .05em;
    text-transform: none;
}

.thead .h-rr {
    text-align: right;
}

/* date · event · W-L · rating. No delta column since 2026-10-05: a signed
   change beside a 10-0 event that still lost points (the rating is the
   whole curve's, not the event's) read as an error to readers. */
.thead,
.ev > summary {
    grid-template-columns: 96px minmax(0, 1fr) 58px 84px;
}

.ev {
    border-bottom: 1px solid var(--line-soft);
}

/* Events that closed on the same day are one block: the rating and the
   delta belong to the day, not to any one of them, so only the first
   row carries them and the rules between them come off. */
.ev.grp {
    border-bottom: 0;
}

.ev > summary {
    list-style: none;
    cursor: pointer;
    display: grid;
    align-items: center;
    gap: 10px;
    padding: 9px 4px;
}

.ev > summary::-webkit-details-marker {
    display: none;
}

.ev > summary:hover {
    background: var(--s1);
}

.pdate {
    font: 500 .72rem/1.3 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--mu);
    white-space: nowrap;
}

.pev {
    font-weight: 600;
    font-size: .8rem;
}

.pev.none {
    color: var(--mu);
    font-weight: 400;
    font-family: var(--f-mono);
    font-size: .74rem;
}

.pwl {
    font: 600 .74rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
}

.pwl i {
    font-style: normal;
    color: var(--mu);
    font-weight: 400;
}

.prr {
    font: 600 .8rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.pblank {
    text-align: right;
    font: 400 .72rem/1 var(--f-mono);
    color: rgba(141, 175, 200, .35);
}

.ms {
    /* Was padded 106px to clear the date column. That space is now a real
       cell in .pm instead, so match dates sit under the event's own date
       and under the table head, and the padding drops to match .ev summary. */
    padding: 2px 4px 12px 4px;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.pm {
    display: grid;
    /* First column mirrors .ev > summary's date column exactly - same width,
       same gap, same origin - so the two levels align. */
    grid-template-columns: 96px 20px 46px minmax(0, 1fr);
    gap: 10px;
    align-items: center;
    padding: 4px 0;
    font-size: .78rem;
}

.pm .r {
    font: 700 .66rem/1 var(--f-mono);
}

html[lang="zh"] .pm .r {
    font-family: var(--f-zh-mono);
}

.pm .r.w {
    color: #5FD3A0;
}

.pm .r.l {
    color: #F2686A;
}

.pm .r.d {
    color: var(--mu);
}

.pm .sc {
    font: 500 .72rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
}

.pm .o {
    color: var(--mu);
}

.pm .o a {
    color: var(--tx);
    font-weight: 600;
    text-decoration: none;
}

.pm .o a:hover {
    color: var(--ball);
}

/* Rolling wrapper for anything that is not a ranking-table name -
   .nm-wrap carries a 40vw cap on phones that the event column must not
   inherit. chrome.js measures both. */
.roll {
    display: block;
    overflow: hidden;
    max-width: 100%;
}

.roll > * {
    display: inline-block;
    white-space: nowrap;
}

.roll.rolling > * {
    animation: name-roll var(--roll-dur, 8s) ease-in-out infinite;
}

/* desktop prints two decimals; the phone has room for neither the
   decimals nor the century */
.pshort {
    display: none;
}

@media (max-width: 800px) {
    .ph {
        gap: 12px;
        padding-bottom: 12px;
    }

    .ph .shot {
        flex: 0 0 68px;
        height: 86px;
    }

    /* The phone slot is 68px less 6px padding a side, and "Retrieving"
       is ten mono characters - 64px at the desktop size. */
    .ph .shot .ptxt {
        font-size: .54rem;
    }

    .ph .shot img {
        max-width: 54px;
        max-height: 86px;
    }

    .ph .nm {
        font-size: 1.1rem;
    }

    /* Still the largest figure in the header, just not by as much - the
       name has only ~200px left beside it at 360px wide. */
    .ph .rankbox {
        padding-left: 6px;
        gap: 4px;
    }

    .ph .rankbox .v {
        font-size: 1.4rem;
    }

    /* Sized so a four-digit rank measures exactly what #999 does at
       1.4rem - five mono characters against four. The rank is not capped,
       and the deepest is #4140, so five characters is the widest the
       figure ever gets. Phones only; the desktop header has the room. */
    .ph .rankbox .v.wide {
        font-size: 1.12rem;
    }

    /* two rows, and ITTF# always gets its own */
    .ph .sub {
        flex-direction: column;
        align-items: flex-start;
        gap: 5px;
    }

    .ph .sub .sep {
        display: none;
    }

    .cv canvas {
        height: 152px;
    }

    .thead,
    .ev > summary {
        grid-template-columns: 44px minmax(0, 1fr) 38px 54px;
        gap: 7px;
    }

    .ms {
        padding-left: 4px;
    }

    /* Mirrors the phone .ev > summary grid the same way the desktop rule
       mirrors the desktop one. */
    .pm {
        grid-template-columns: 44px 20px 46px minmax(0, 1fr);
        gap: 7px;
    }

    .pfull {
        display: none;
    }

    .pshort {
        display: inline;
    }

    /* four figures in a clean 2x2; ACTIVE is the one a phone gives up */
    .stats {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .stat.st-active {
        display: none;
    }

    .stat {
        padding: 8px 9px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .roll.rolling > * {
        animation: none;
    }

    .roll {
        overflow: visible;
    }

    .roll > * {
        white-space: normal;
    }
}

/* ══════════════════════════════════════════════════════════════════
   Player profile search

   The results table borrows every cell rule from the ranking table -
   th, td, td.rk, td.num, td.nm, .assoc-cell, .flag - and differs only
   in which columns exist, so its widths carry their own .s-* names
   rather than fighting .c-* over the same six classes.

   No html[lang="zh"] rule here changes font-size, per the convention
   the rest of the sheet follows: Chinese swaps the family, the tracking
   and the casing, never the size, so nothing moves on a toggle.
   ══════════════════════════════════════════════════════════════════ */

/* Two rows: the two text fields, then the narrow ones and the actions.
   Every track is a fixed length or a share of what is left over - the
   button pair included, at 200px rather than auto. Sized to its content,
   SEARCH CLEAR and 搜索 清除 measure differently, and the fields around
   them changed width on a language toggle.

   The association field is 270px because four tags at 60.5px plus their
   gaps come to 254, and four is the most the control accepts. */
.sform {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 11px 12px 12px;
    background: var(--s1);
    border: 1px solid var(--line);
    border-radius: 4px;
}

.srow {
    display: flex;
    gap: 12px;
}

/* Row one hangs from the top: the association field grows a tag row
   taller once something is picked, and aligning on the bottom would drag
   the name box down with it. Row two hangs from the bottom instead - the
   buttons have no label above them, so only their lower edge can line up
   with the boxes beside them. */
.srow-1 {
    align-items: flex-start;
}

.srow-2 {
    align-items: flex-end;
}

/* The two text fields share the row evenly at every width. The narrowest
   desktop, 801px, leaves them 262px each, which still clears the 254px
   four tags plus their gaps come to - and four is the most the control
   accepts. */
.srow-1 .f-q,
.srow-1 .f-a {
    flex: 1;
    min-width: 0;
}

/* Five controls, each a share of the row rather than a fixed length, so
   the row spans the card at every width:

       gender 15 | year 20 | year 20 | search 35 | clear 10 */
.srow-2 .f-g {
    flex: 15;
    min-width: 0;
}

.srow-2 .f-y {
    flex: 40;
    min-width: 0;
}

.srow-2 .btnrow {
    flex: 45;
    min-width: 0;
}

.btnrow .btn.go {
    flex: 35;
}

.btnrow .btn.clr {
    flex: 10;
}

.sform .fld {
    display: flex;
    flex-direction: column;
    gap: 7px;
    min-width: 0;
}

.sform label {
    cursor: pointer;
}

/* The ground is one step darker than the card, so a field reads as a
   well rather than as another panel. */
.sform input,
.sform select {
    width: 100%;
    height: 2.11rem;
    padding: 0 10px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 3px;
    color: var(--tx);
    font: 400 .82rem/1 var(--f-body);
    outline: none;
}

.sform input::placeholder {
    color: rgba(141, 175, 200, .45);
}

.sform input:focus,
.sform select:focus {
    border-color: var(--ball);
}

.selwrap {
    display: block;
    position: relative;
}

/* The native arrow is drawn in the platform's own colours, which on a
   dark ground is either invisible or a white wedge nothing else on the
   page matches. */
.sform select {
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    padding-right: 24px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238DAFC8' stroke-width='1.4' stroke-linecap='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 9px center;
}

/* Not .range - that is the chart's 5Y/10Y/All control on the profile
   page, and a gap between its buttons would break the segmented pill. */
.yspan {
    display: flex;
    align-items: center;
    gap: 7px;
}

.yspan i {
    font-style: normal;
    font-family: var(--f-mono);
    color: rgba(141, 175, 200, .6);
    flex: 0 0 auto;
}

/* Figures, so they are mono and centred like every other number on the
   site - and narrow enough that two of them plus the dash fit the phone
   row beside the gender menu. */
.yspan input {
    flex: 1 1 0;
    min-width: 0;
    padding: 0 6px;
    text-align: center;
    font-family: var(--f-mono);
    font-size: .75rem;
    letter-spacing: .04em;
}

/* ── association tags and their list ── */

/* The box itself, not the input: the tags live inside it, so the border
   and the focus ring belong to the wrapper and the input is a bare field
   that takes whatever width the tags leave. */
.combo {
    position: relative;
    display: flex;
    align-items: center;
    gap: 4px;
    min-height: 2.11rem;
    padding: 0 4px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 3px;
}

.combo:focus-within {
    border-color: var(--ball);
}

.combo input {
    flex: 1 1 40px;
    min-width: 0;
    height: auto;
    align-self: stretch;
    padding: 0 6px;
    background: transparent;
    border: 0;
    border-radius: 0;
}

/* At four there is nowhere left to type, and hiding the field is what
   keeps the box one line tall in every state. */
.combo input:disabled {
    display: none;
}

/* The list overlays what follows rather than pushing it: opening it must
   not move the row of fields underneath. */
.alist {
    position: absolute;
    top: calc(100% + 3px);
    left: 0;
    right: 0;
    z-index: 30;
    margin: 0;
    padding: 4px 0;
    list-style: none;
    max-height: 244px;
    overflow-y: auto;
    background: var(--s2);
    border: 1px solid var(--line-strong);
    border-radius: 3px;
    box-shadow: 0 10px 24px rgba(0, 0, 0, .45);
}

.alist li {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 9px;
    font-size: .75rem;
    line-height: 1.3;
    cursor: pointer;
}

.alist li.none {
    cursor: default;
    color: var(--mu);
    font-style: italic;
}

.alist li:hover,
.alist li[aria-selected="true"] {
    background: rgba(234, 242, 248, .13);
}

.alist .an {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-transform: uppercase;
}

/* The stored spellings are all caps, so uppercase is what keeps the one
   hand-written label - Individual Neutral Athletes - from standing out. */
html[lang="zh"] .alist .an {
    text-transform: none;
}

.alist .ac {
    font: 400 .64rem/1 var(--f-mono);
    letter-spacing: .07em;
    color: var(--mu);
    flex: 0 0 auto;
}

.alist i {
    font-style: normal;
    color: rgba(141, 175, 200, .45);
    flex: 0 0 auto;
}

.tags {
    display: flex;
    gap: 4px;
    flex: 0 0 auto;
}

.tags:empty {
    display: none;
}

/* No border - the lighter ground is what separates a tag from the box it
   sits in, and the two pixels it gives back are two the row needs:
   3 + 16 + 2 + 22.5 + 2 + 10 + 3 = 58.5px, so four tags and their gaps
   come to 246 inside a box whose narrowest desktop width is 262. */
.tag {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    height: 22px;
    padding: 0 3px;
    border-radius: 3px;
    background: var(--s2);
}

.tag .flag {
    width: 16px;
    height: 11px;
}

.tag b {
    font: 500 .64rem/1 var(--f-mono);
    letter-spacing: .05em;
    color: var(--tx);
}

.tag button {
    position: relative;
    width: 10px;
    height: 10px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--mu);
    font: 400 11px/1 var(--f-body);
    cursor: pointer;
}

.tag button:hover {
    color: var(--ball);
}

/* The glyph is 10px because the tag has no room for more; the target it
   answers to is the height of the tag, which a thumb can find. */
.tag button::after {
    content: '';
    position: absolute;
    inset: -6px -3px -6px -2px;
}

.btnrow {
    display: flex;
    gap: 8px;
}

/* Sharing the fixed track equally, so neither button is sized by its own
   label either. */
.btnrow .btn {
    flex: 1;
    padding: 0 8px;
}

.btn {
    height: 2.11rem;
    padding: 0 20px;
    border: 1px solid transparent;
    border-radius: 3px;
    font: 600 .7rem/1 var(--f-body);
    letter-spacing: .08em;
    text-transform: uppercase;
    cursor: pointer;
    white-space: nowrap;
}

html[lang="zh"] .btn {
    letter-spacing: .04em;
    text-transform: none;
}

.btn.go {
    background: var(--ball);
    border-color: var(--ball);
    color: #08192A;
}

/* An empty search would match all 32,241 players and mean nothing, so
   the button is not offered until at least one field carries something. */
.btn.go[disabled] {
    background: transparent;
    border-color: var(--line);
    color: rgba(141, 175, 200, .45);
    cursor: default;
}

.btn.clr {
    background: transparent;
    border-color: var(--line);
    color: var(--mu);
}

.btn.clr:hover {
    color: var(--tx);
    border-color: var(--line-strong);
}

.rbar {
    padding: 18px var(--cell-x) 9px;
}

/* Rank is not capped - current_rank holds the whole ranking, 4,140 men
   and 2,927 women - so four digits is the widest the figure gets. Sized
   so it measures exactly what a three-digit rank does, which is what
   keeps the column from having to be wider than the ranking table's. */
td.rk.wide {
    font-size: .71rem;
}

/* Unranked keeps the figure's size and gives up only the colour, the
   same way the profile header's em dash does. */
td.rk.none {
    color: var(--mu);
}

.s-rank   { width: clamp( 56px, calc(0.058 * (100vw - 256px)), 104px) }
.s-id     { width: clamp( 76px, calc(0.072 * (100vw - 256px)), 130px) }
.s-assoc  { width: clamp(110px, calc(0.128 * (100vw - 256px)), 240px) }
.s-name   { width: auto }
.s-gender { width: clamp( 68px, calc(0.070 * (100vw - 256px)), 120px) }
.s-yob    { width: clamp( 68px, calc(0.074 * (100vw - 256px)), 140px) }

@media (max-width: 800px) {
    /* Name and association each take the full width - which at 360px is
       about 316px, enough for four tags on one line; gender, and the two
       year boxes that share the rest of that row, are what a phone can
       carry side by side. */
    .sform {
        padding: 10px;
        gap: 11px;
    }

    .srow {
        flex-wrap: wrap;
        gap: 11px;
    }

    .srow-1 .f-q,
    .srow-1 .f-a {
        flex: 1 1 100%;
    }

    .srow-2 .f-g {
        flex: 0 0 92px;
    }

    /* Zero basis, not auto: auto resolves to the two year boxes' own
       content width, which no longer left room for the gender menu on
       the same line and wrapped it onto its own. */
    .srow-2 .f-y {
        flex: 1 1 0;
        min-width: 0;
    }

    .srow-2 .btnrow {
        flex: 1 1 100%;
    }

    /* No fixed track here - the pair spans the row, and Search keeps the
       weight rather than splitting it evenly with Clear. */
    .btnrow .btn.go {
        flex: 1;
    }

    .btnrow .btn.clr {
        flex: 0 0 auto;
        padding: 0 18px;
    }

    .rbar {
        padding: 16px var(--cell-x) 8px;
    }

    /* The ITTF id is the one column a phone can do without: the name and
       the flag identify a player, and the id is only ever copied from a
       profile. The association narrows to its flag, as on every other
       table here. */
    .s-id,
    td.pid {
        display: none;
    }

    .s-rank   { width: 46px }
    .s-assoc  { width: 30px }
    .s-name   { width: auto }
    .s-gender { width: 58px }
    .s-yob    { width: 56px }

    /* .nm-wrap's 40vw cap belongs to the ranking table, whose name column
       is far narrower than this one - .roll is the uncapped wrapper. */
    .srch .roll {
        max-width: 100%;
    }
}

/* ══════════════════════════════════════════════════════════════════
   Top players

   A canvas of every career that has touched the world top ten, and a
   list beside it that is both the legend and an all-time table - sorted
   by peak, so it reads on its own.

   No html[lang="zh"] rule here changes font-size, per the convention the
   rest of the sheet follows.
   ══════════════════════════════════════════════════════════════════ */

/* The chart is square where it can be, and always fits the window.

   Height follows width (aspect-ratio 1) up to the window height less the
   page chrome above and below it, so the chart never needs a scroll to be
   seen whole. On a wide screen that cap would leave a long flat strip, so
   the plot is also never wider than cap / .75: the chart is never flatter
   than 0.75 (height / width), and whatever width is left over stays empty
   on the right, where the rest of the site leaves it too.

   --tp-chrome is what sits above and below the chart on a desktop: body
   padding 18 + title row ~26 + its margin 14 + bottom padding 24 = ~82,
   rounded up for a title that wraps. Replaces the fixed 24rem (2026-09-30),
   which was 0.46 on a 1440 screen and taller than wide just past 800px. */
.tp {
    --tp-chrome: 96px;
    display: flex;
    gap: 16px;
    align-items: stretch;
}

.tp .plot {
    flex: 1;
    min-width: 0;
    max-width: calc((100vh - var(--tp-chrome)) / .75);
}

.tp canvas {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    max-height: calc(100vh - var(--tp-chrome));
    min-height: 12rem;
    touch-action: pan-y;
}

/* A column the height of the chart: the header and the picked block take
   what they need, and the rest of the names scroll in what is left. Its
   height is the chart's, not its own: size containment makes the column
   contribute nothing to the row's height, so the row is exactly as tall as
   the canvas and the column stretches to it. */
.tp .tside {
    /* 180px is what this column can give up at the narrowest desktop, and
       233 is all it can use - past that the chart should have the pixels.
       The same shape as the table columns above: a floor, a share of the
       width left over once the rail and the body padding are paid for,
       and a ceiling. 180 at 801px, 233 from about 962px on. */
    flex: 0 0 clamp(180px, calc(0.33 * (100vw - 256px)), 233px);
    min-width: 0;
    contain: size;
    display: flex;
    flex-direction: column;
    border-left: 1px solid var(--line-soft);
}

.tp .thd {
    flex: 0 0 auto;
    display: flex;
    justify-content: space-between;
    gap: 8px;
    padding: 0 8px 6px;
    border-bottom: 1px solid var(--line-soft);
}

.tp .list {
    flex: 1;
    min-height: 0;
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
}

/* The picked players, held at the top so they stay in view while the rest
   scrolls. The rule under them belongs to this block, so it is simply not
   there when nothing is picked - a divider that never has to be hidden. */
.tp .list.picked {
    flex: 0 0 auto;
    overflow: visible;
    border-bottom: 1px solid var(--line);
    padding-bottom: 3px;
    margin-bottom: 3px;
}

.tp .list li {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 8px;
    font-size: .77rem;
    color: var(--mu);
    cursor: pointer;
    white-space: nowrap;
}

.tp .list li:hover {
    color: var(--tx);
}

/* A name too long for the column rolls rather than being cut, the same
   way the ranking table's does - .roll is the uncapped wrapper, and
   chrome.js measures it. At 180px this is most of the Chinese names and
   a good few of the English ones. */
.tp .list li .nm {
    flex: 1 1 auto;
    min-width: 0;
}

/* Grey until pinned: colour on this page means "you picked this one",
   not "this one is good". */
.tp .list li .sw {
    width: 8px;
    height: 8px;
    border-radius: 2px;
    flex: 0 0 auto;
    background: rgba(234, 242, 248, .18);
}

.tp .list li .pk {
    margin-left: auto;
    font: 400 .64rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: #6B8CA3;
}

.tp .list li.on {
    color: var(--tx);
    background: rgba(234, 242, 248, .06);
}

/* Three slots, filled in click order and held until unpinned - the same
   three the canvas draws with. */
.tp .list li.s0 .sw { background: var(--chalk) }
.tp .list li.s1 .sw { background: #58C4EA }
.tp .list li.s2 .sw { background: #F19DB5 }
.tp .list li.s3 .sw { background: #B99BF0 }
.tp .list li.s4 .sw { background: #6FD3A6 }

/* The profile link is a link, not a third state of the row: the row
   itself still pins, and this opens the player's page in its own tab so
   a reader who has spent five clicks building a comparison does not lose
   it by following one. Transparent rather than hidden at rest, so the
   name never reflows when a row is hovered.

   It sits ahead of the peak rather than after it, so the peak stays the
   last thing in the row and its column keeps the right edge its header
   is set against. The name takes the leftover width, so the arrows line
   up in a column of their own. */
.tp .list li .go {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    padding: 0 2px;
    line-height: 1;
    color: transparent;
    text-decoration: none;
}

/* currentColor, so the transparent rest state hides the stroke too. */
.tp .list li .go svg {
    display: block;
    width: .66rem;
    height: .66rem;
}

.tp .list li:hover .go,
.tp .list li.on .go {
    color: var(--mu);
}

.tp .list li .go:hover {
    color: var(--ball);
}

/* Keyboard reaches it whether or not the row is hovered. */
.tp .list li .go:focus-visible {
    color: var(--ball);
    outline: 1px solid var(--line);
    outline-offset: 1px;
}

/* The average is the one line on the chart that is not a player, so its
   control is not a list row: a chip above the header, carrying the dash
   the canvas draws it with. Off, it drops to the same grey an unpinned
   swatch has. */
.tp .avgt {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin: 0 0 6px;
    padding: 4px 8px;
    border: 0;
    border-radius: 3px;
    background: transparent;
    font: 400 .68rem/1.3 var(--f-body);
    color: var(--mu);
    text-align: left;
    cursor: pointer;
}

.tp .avgt:hover {
    background: rgba(234, 242, 248, .06);
}

.tp .avgt .dash {
    width: 14px;
    height: 0;
    flex: 0 0 auto;
    border-top: 1.5px dashed var(--mu);
}

.tp .avgt[aria-pressed="false"] {
    color: #6B8CA3;
}

.tp .avgt[aria-pressed="false"] .dash {
    border-top-color: rgba(234, 242, 248, .18);
}

@media (max-width: 800px) {
    /* The list goes under the chart rather than beside it; a phone has
       no room for a column and no pointer to hover with, so picking a
       player is a tap on either. */
    .tp {
        flex-direction: column;
        gap: 10px;
    }

    .tp .plot {
        width: 100%;
        max-width: none;
    }

    /* Square on a phone, up to 60% of the visible screen so the start of
       the list stays in view below it. svh, not vh: the address bar
       showing and hiding must not make the chart jump. No shape floor
       here - at the 800px edge the chart simply goes flat. */
    .tp canvas {
        max-height: 60vh;
        max-height: 60svh;
    }

    .tp .tside {
        flex: none;
        width: 100%;
        border-left: 0;
        border-top: 1px solid var(--line-soft);
        padding-top: 6px;
    }

    /* No fixed column height on a phone - the block sits above the list
       rather than beside the chart, so it can take the room it needs. */
    .tp .tside {
        contain: none;
    }

    .tp .list {
        max-height: 11.5rem;
    }

    .tp .list.picked {
        max-height: none;
    }

    .tp .list li {
        padding: 5px 6px;
    }

    /* No hover on a phone, so the link is always visible - and wide
       enough to be a thumb's target rather than a glyph's. */
    .tp .list li .go {
        color: var(--mu);
        padding: 0 6px;
    }

    .tp .list li .go svg {
        width: .78rem;
        height: .78rem;
    }
}

/* ══════════════════════════════════════════════════════════════════
   Mid-band column widths (801-1000px)

   These MUST come after the base .c-* rules. They carry the same
   specificity, so declaring them earlier in the file - which is where
   they used to live, beside the :root overrides for this band - meant
   the base rules won at every width and this band never applied. The
   narrower floors and the 238px offset (rail 216 + an 11px gutter each
   side, against 256 wide) only take effect from here.
   ══════════════════════════════════════════════════════════════════ */

@media (min-width: 801px) and (max-width: 1000px) {
    /* Floors are measured ink + 10px padding. assoc carries a flag (18)
       + gap (7) + five CJK at .68rem (61) = 86 of ink. */
    .h-date   { width: clamp( 78px, calc(0.095 * (100vw - 238px)), 118px) }
    .c-rank   { width: clamp( 42px, calc(0.055 * (100vw - 238px)), 104px) }
    .c-ittf   { width: clamp( 42px, calc(0.055 * (100vw - 238px)), 104px) }
    .c-assoc  { width: clamp(103px, calc(0.128 * (100vw - 238px)), 240px) }
    .c-yob    { width: clamp( 60px, calc(0.074 * (100vw - 238px)), 140px) }
    .c-rating { width: clamp( 80px, calc(0.108 * (100vw - 238px)), 200px) }
    .c-err    { width: clamp( 65px, calc(0.088 * (100vw - 238px)), 168px) }
    .c-trend  { width: clamp( 77px, calc(0.150 * (100vw - 238px)), 300px) }

    /* search table: the same floors less the tracking this band gives up */
    .s-rank   { width: clamp( 46px, calc(0.058 * (100vw - 238px)), 104px) }
    .s-id     { width: clamp( 66px, calc(0.072 * (100vw - 238px)), 130px) }
    .s-assoc  { width: clamp(103px, calc(0.128 * (100vw - 238px)), 240px) }
    .s-gender { width: clamp( 58px, calc(0.070 * (100vw - 238px)), 120px) }
    .s-yob    { width: clamp( 60px, calc(0.074 * (100vw - 238px)), 140px) }
}

/* ══════════════════════════════════════════════════════════════════
   Data sources (sources.html)

   Fixed layout like every other table here, so switching language does
   not move the columns. Seasons and Source are set widths and Competition
   takes the rest, wrapping. Seasons is sized for a range - "2009/10 –
   2011/12", or "2009/10–2011/12赛季" (~8.4rem at this size) - and Source
   for "国际乒联（ITTF）" (~7rem), each with ~2rem to spare. Full width like
   the other tables; only the intro line is capped, being running text.
   ══════════════════════════════════════════════════════════════════ */

.srcs-intro {
    max-width: 46rem;
    margin: 0 0 18px;
    font-size: .82rem;
    color: var(--mu);
}

table.srcs .c-yr  { width: 10rem }
table.srcs .c-src { width: 9.5rem }

table.srcs td {
    vertical-align: top;
    line-height: 1.45;
}

table.srcs .yr {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

table.srcs a {
    color: #9DC4DC;
    text-decoration: none;
    border-bottom: 1px solid rgba(157, 196, 220, .32);
}

table.srcs a:hover {
    color: var(--tx);
    border-bottom-color: var(--tx);
}


@media (max-width: 800px) {
    /* At phone width both columns are as narrow as two lines allow (never
       three), and Competition takes the rest. The binding entries, at
       .82rem: Seasons - a future range "2009/10–2011/12赛季", which can only
       break after the dash, so "2011/12赛季" (~6em, ~5.5rem with padding)
       must fit one line; Source - "国际乒联（ITTF）", which splits as
       "国际乒联" / "（ITTF）" (~4em, ~3.9rem with padding). Both carry a
       little spare for font fallback. */
    /* Seasons is wider than two lines need: "2018-19赛季起" (~6.9em, ~6.3rem
       with padding) is kept on one line, at the owner's request. */
    table.srcs .c-yr  { width: 6.6rem }
    table.srcs .c-src { width: 4.6rem }

    table.srcs .yr {
        white-space: normal;
    }
}

/* Charts are touched, not read: a press-and-drag on a phone otherwise
   starts a text selection that swallows the chart and whatever is around
   it, and a long press on the canvas offers to save it as an image. The
   top players list is tapped to pin, so it does not select either - its
   profile links keep their long-press menu. */
.cv,
.tp .plot,
.tp .tside {
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}

/* touch-action stays pan-y (above): a swipe on a chart scrolls the page,
   and press-and-hold hands the finger to the chart - touchScrub in
   chrome.js. */
.cv canvas,
.tp canvas {
    -webkit-touch-callout: none;
}

/* The poster preview on a phone (rankings.js showPoster): the image over a
   dimmed page, saved with the share sheet or a long-press. */
.pshade {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: calc(16px + env(safe-area-inset-top, 0px)) 16px calc(16px + env(safe-area-inset-bottom, 0px));
    background: rgba(4, 17, 29, .94);
}

body.pshade-on {
    overflow: hidden;
}

.pimg {
    display: block;
    max-width: 100%;
    max-height: 72vh;
    max-height: 72svh;
    width: auto;
    height: auto;
    border: 1px solid var(--line);
    /* a long-press here is how the image reaches Photos - keep the menu */
    -webkit-touch-callout: default;
}

.pacts {
    display: flex;
    gap: 10px;
}

.pbtn {
    font: 600 .8rem/1 var(--f-body);
    padding: 11px 18px;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: transparent;
    color: var(--tx);
    cursor: pointer;
}

.pbtn.pmain {
    background: var(--ball);
    border-color: var(--ball);
    color: var(--chalk);
}

/* QR code | URL in the poster preview: the .range switch, at the size of
   the buttons beneath it. The poster dims while it redraws. */
.pqrsw button {
    font: 600 .8rem/1 var(--f-body);
    letter-spacing: 0;
    padding: 9px 16px;
}

.pshade.busy .pimg {
    opacity: .55;
    transition: opacity .15s;
}

.pbtn:disabled {
    cursor: progress;
    opacity: .6;
}

/* A competition with more than one source spans its rows (sources.html);
   the group highlights together, not row by row. */
table.srcs tbody.grp:hover tr {
    background: var(--s1);
}

/* ── Peak Rankings (peak.html) ──
   The current ranking's table with a peak-date column where the trend was
   and no ITTF column. The date is the full day on a desktop, year-month on
   a phone (2016-03, ~55px of mono ink + padding), shown with the same
   full/short pair the rating uses. */
.c-date { width: clamp(100px, calc(0.110 * (100vw - 256px)), 200px) }

td.pkd {
    font: 400 .82rem/1 var(--f-mono);
    font-variant-numeric: tabular-nums;
    color: var(--mu);
    white-space: nowrap;
}

.pkd .short {
    display: none;
}

@media (min-width: 801px) and (max-width: 1000px) {
    .c-date { width: clamp( 92px, calc(0.110 * (100vw - 238px)), 200px) }
}

@media (max-width: 800px) {
    .c-date { width: 66px }

    .pkd .full {
        display: none;
    }

    .pkd .short {
        display: inline;
    }
}

@media (max-width: 800px) {
    /* PEAK DATE in the header's spaced caps is ~68px against the column's
       66 and header text never wraps, so it spilled out and made the table
       scroll sideways; set solid it is ~55. */
    th.c-date {
        letter-spacing: 0;
    }
}

@media (max-width: 800px) {
    /* The Current Rankings, Peak Rankings and Top Players titles in English
       are too long for a phone's line: they break at the dot, the dot
       itself dropped (rankings.js, peak.js, top.js). */
    html[lang="en"] #draw-title .ttl-sep,
    html[lang="en"] #ttitle .ttl-sep {
        display: none;
    }

    html[lang="en"] #draw-title .ttl-2,
    html[lang="en"] #ttitle .ttl-2 {
        display: block;
    }
}

@media (max-width: 800px) {
    /* ...and its update line puts All / Active Players on a line of its
       own rather than letting it wrap mid-phrase. */
    html[lang="en"] #updated .upd-sep {
        display: none;
    }

    html[lang="en"] #updated .upd-2 {
        display: block;
    }
}

/* Year pagination (chrome.js YearStep): [-] [2026] [+] in one outlined
   box, the switch's line and hover. */
.yearbox {
    align-self: center;
}

.ystep {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--line);
    border-radius: 3px;
    overflow: hidden;
    align-self: center;
}

.ystep button {
    font: 600 .9rem/1 var(--f-mono);
    width: 30px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--tx);
    cursor: pointer;
    transition: background-color .15s, color .15s;
}

.ystep button:hover:not(:disabled) {
    background: var(--s2);
}

.ystep button:disabled {
    color: var(--mu);
    opacity: .4;
    cursor: default;
}

.ystep input {
    font: 500 .78rem/1 var(--f-mono);
    letter-spacing: .04em;
    /* content-box: the width is the four digits and their tracking alone,
       the padding goes outside it (border-box clipped "2026" to "202") */
    box-sizing: content-box;
    width: 4.6ch;
    padding: 7px 8px;
    text-align: center;
    color: var(--tx);
    background: var(--s1);
    border: 0;
    border-left: 1px solid var(--line);
    border-right: 1px solid var(--line);
    border-radius: 0;
    -webkit-appearance: none;
}

.ystep input:focus {
    outline: none;
    background: var(--s2);
    box-shadow: inset 0 -2px 0 var(--ball);
}

@media (max-width: 800px) {
    /* under the title, left-aligned, as the drop-down sat - after the base
       .yearbox rule above, which would otherwise centre it */
    .yearbox {
        align-self: flex-start;
    }
}
