/* ============================================================================
   ENDURANCE — category page.
   ============================================================================
   Inherits the platform shell from shared/style.css (header, main-grid,
   controls-panel, toggles, run button, result panel, sublabels). Everything
   below is the part Endurance does not share with any other category.

   THE LEADERBOARD IS THE SPINE. Every other category resolves a bounded
   repetitive contest with a round counter or a scorecard. This one is a hundred
   and fifty days across six to ten legs with a whole-route fork, and the one
   view a fork does not break is a ranked list of expected finishes.

   So the rows are ABSOLUTELY POSITIONED and moved with translateY rather than
   reordered in the flow. A list that reorders instantly reads as a redraw; a
   list whose rows slide past each other reads as an overtake, and the overtake
   is the event.
   ============================================================================ */

/* ── Controls ─────────────────────────────────────────────────────────────── */

.end-standfirst {
    font-size: 14px;
    line-height: 1.55;
    color: var(--dim);
    margin: 0;
}

/* Route names are long, so the route toggles stack instead of sitting in a row. */
.end-route-group,
.end-century-group,
.end-quarter-group { flex-wrap: wrap; }

/* ── THE ROUTE ACCORDION ──────────────────────────────────────────────────── */
/* Each route is a row that owns a drawer. The rows are the flex children now,
   not the buttons, so the hairline between routes moves onto the item. */
.end-route-group { flex-direction: column; }
.end-route-item { display: flex; flex-direction: column; }
.end-route-item + .end-route-item { border-top: 1px solid var(--border-hi); }
.end-route-group .toggle {
    text-align: left;
    padding: 10px 12px;
    font-size: 14px;
    width: 100%;
}

/* THE DRAWER. Nickolay asked for the description "in black, immediately under
   the name of the route" — so it is an extension of the selected row rather than
   a note below the group, and the selected row is yellow. Black on yellow.

   OPENS ON grid-template-rows 0fr TO 1fr, not on a max-height guess. The three
   descriptions are two, three and four lines long; a max-height big enough for
   the longest makes the shortest snap open early and shut late, and one exactly
   right for one route is wrong for the other two. 1fr is the content's own
   height, whatever it turns out to be. */
.end-route-blurb {
    display: grid;
    grid-template-rows: 0fr;
    background: var(--yellow-fill);
    transition: grid-template-rows 340ms cubic-bezier(.22, .9, .24, 1);
}
.end-route-item.is-open .end-route-blurb { grid-template-rows: 1fr; }
/* The clip lives here and the padding lives inside it, so a closed drawer is
   genuinely zero high rather than a padded sliver of yellow. */
.end-route-blurb-inner { overflow: hidden; }
.end-route-blurb-copy {
    padding: 0 12px 12px;
    font-size: 13px;
    line-height: 1.5;
    color: #0c0c0c;
}

/* ── THE CENTURY AND SEASON BOXES READ AS THE ACTION ROW DOES ─────────────── */
/* Nickolay, 2026-08-26: "the actual months listed in those boxes. The font needs
   to be the same as the font used in RUN THE PASSAGE."
   The FAMILY was already the same — everything on this platform is system-ui.
   What differed was the treatment: 15px weight 400 mixed case against the
   button's 12px weight 700 caps, so the control read larger than the action it
   sat above. Same four values now, and the button's own values rather than a
   copy: calc(15px * --im-text-scale) is what the action row resolves to.

   THE ROUTE LIST IS DELIBERATELY LEFT ALONE. It stopped being a toggle row when
   it became an accordion carrying a name and a description, and setting a route
   name in tracked caps would fight the paragraph underneath it. */
/* Scoped through .controls-panel so it outranks the rule further down this file
   that sets every toggle in the panel to 15px — that one loads later and would
   otherwise win at equal specificity. */
.controls-panel .end-quarter-group .toggle,
.controls-panel .end-century-group .toggle {
    font-size: calc(15px * var(--im-text-scale));
    font-weight: 700;
    letter-spacing: calc(0.16em * var(--track));
    /* NO text-transform. The blanket uppercase was turning "1500s" into "1500S",
       which is a different word — the trailing s is part of how a century is
       written, not a letter to be shouted. Case belongs to the content instead:
       the century labels already read "1500s", and the season labels arrive
       uppercase from quarterShort(). Nothing here has to guess. */
    /* OCTOBER TO DECEMBER took THREE lines where its neighbours took two, and it
       missed by 1.2px: "OCTOBER TO" wants 86.2px in an 85px box. Tightening the
       WORD space rather than the letter space buys those pixels back from the one
       gap that has them to give, and leaves the letter-spacing identical to the
       button — which is the thing that was asked to match. */
    word-spacing: -2px;
}

.control-detail {
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--dim);
    margin-top: 10px;
}


/* ── Result: idle ─────────────────────────────────────────────────────────── */

/* .end-idle removed with its markup: the idle prompt is .idle-prompt in
   #state-idle now, shared with every other category. */


/* ── Result: condition bar ────────────────────────────────────────────────── */

.end-condition {
    font-family: var(--mono);
    font-size: 13px;
    letter-spacing: calc(0.05em * var(--track));
    color: var(--yellow);
    padding-bottom: 14px;
}

/* ── The opening, on a forked route ───────────────────────────────────────── */

.end-fork {
    border: 1px solid var(--border-hi);
    padding: 14px 16px;
    margin-bottom: 18px;

    border-radius: var(--r);
}
.end-fork-rows { display: flex; flex-direction: column; gap: 5px; }

.end-fork-row {
    display: grid;
    grid-template-columns: 168px 1fr 104px;
    align-items: baseline;
    gap: 14px;
    font-family: var(--mono);
    font-size: 12.5px;
    color: var(--dim);
    white-space: nowrap;
}
.end-fork-name {
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
}
.end-fork-opts { display: flex; gap: 8px; align-items: baseline; }
.end-fork-sep  { opacity: 0.4; }
/* The option the party actually chose. Colouring the number it chose ON is
   better than a third column repeating the label: the choice and its reason are
   the same object. */
.end-fork-opt.is-took { color: var(--yellow); font-weight: 700; }
.end-fork-gap { text-align: right; opacity: 0.7; }

/* ── The board ────────────────────────────────────────────────────────────── */

.end-board-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 10px;
}
.end-board-title {
    font-size: var(--label-size);
    font-weight: 700;
    letter-spacing: var(--label-track);
    text-transform: uppercase;
    color: var(--white);
}
.end-day {
    font-family: var(--mono);
    font-size: 14px;
    color: var(--yellow);
}

.end-board {
    position: relative;
    /* Set inline by app.js from the roster size, so the panel never jumps. */
    transition: height 0.35s ease;
}

.end-row {
    position: absolute;
    left: 0; right: 0; top: 0;
    height: 42px;
    display: grid;
    grid-template-columns: 34px minmax(120px, 1fr) 78px 92px 62px 62px 84px;
    align-items: center;
    gap: 10px;
    padding: 0 10px;
    border-bottom: 1px solid var(--border);
    /* The overtake. Everything else about this page is legible from a still;
       this is the one thing that has to move to mean anything. */
    transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1),
                opacity 0.35s ease,
                background 0.4s ease;
    will-change: transform;
}

.end-place {
    font-family: var(--mono);
    font-size: 15px;
    color: var(--dim);
}
.end-row.is-lead .end-place { color: var(--yellow); }

.end-name {
    font-family: var(--mono);
    font-size: 14px;
    color: var(--text);
    letter-spacing: calc(0.03em * var(--track));
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.end-row.is-lead .end-name { color: var(--yellow); font-weight: 700; }

/* Which way a forked party went, carried on its row so the two theatres stay
   readable while the board mixes them. */
.end-branch {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: calc(0.08em * var(--track));
    text-transform: uppercase;
    color: var(--dim);
    opacity: 0.75;
}

.end-men {
    font-family: var(--mono);
    font-size: 13.5px;
    color: var(--dim);
    text-align: right;
}
/* Men remaining decides the result, so the count earns a colour when it falls
   far enough to threaten the founding. */
.end-men.is-thin  { color: var(--orange); }
.end-men.is-grave { color: var(--red); }

.end-eta {
    font-family: var(--mono);
    font-size: 14px;
    color: var(--text);
    text-align: right;
}
.end-row.is-arrived .end-eta { color: var(--yellow); }

/* A row taking a hit. Bright, brief, and on the row rather than over the panel,
   so the cost and the party it happened to land together. */
.end-row.is-hit { background: rgba(255, 204, 0, 0.16); }

/* A party that has ended fades where it stands, then leaves the ordering. */
.end-row.is-out { opacity: 0.3; }
/* The ending replaces the projection, because a party that has stopped has no
   expected finish and showing its last one would be a number that means nothing. */
.end-row.is-out .end-eta {
    font-size: 10.5px;
    letter-spacing: calc(0.08em * var(--track));
    text-transform: uppercase;
    color: var(--orange);
}

/* ── The shelf ────────────────────────────────────────────────────────────── */

.end-shelf {
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid var(--border);
}
.end-shelf-rows { display: flex; flex-direction: column; gap: 4px; }

.end-shelf-row {
    display: grid;
    grid-template-columns: 1fr auto auto;
    gap: 12px;
    align-items: baseline;
    font-family: var(--mono);
    font-size: 12.5px;
    color: var(--dim);
}
.end-shelf-name { color: var(--text); }
/* Each ending reads as its own kind of loss rather than a variant of turning
   back, so each gets its own colour. */
.end-shelf-how                      { text-transform: uppercase; letter-spacing: calc(0.07em * var(--track)); }
.end-shelf-how[data-how="turned back"]     { color: var(--dim); }
.end-shelf-how[data-how="detained"]        { color: var(--yellow-dim); }
.end-shelf-how[data-how="forced back"]     { color: var(--orange); }
.end-shelf-how[data-how="broke up"]        { color: var(--red); }
.end-shelf-how[data-how="failed founding"] { color: var(--orange); }

/* ── Beats ────────────────────────────────────────────────────────────────── */

/* .end-beat is gone. What happened is announced INSIDE the frame now, as the
   third line of the chyron — see .end-chyron-what. */

/* ── Phone ────────────────────────────────────────────────────────────────── */

@media (max-width: 900px) {
    .main-grid { grid-template-columns: 1fr; }
    .controls-panel { border-right: none; border-bottom: 1px solid var(--border-hi); }
    .result-panel { padding: 24px 18px; min-height: 0; }
    .end-row {
        grid-template-columns: 24px 1fr auto 52px 62px;
        gap: 7px;
        padding: 0 4px;
        height: 40px;
    }
    .end-name   { font-size: 12.5px; }
    .end-branch { display: none; }
    .end-eta    { font-size: 12.5px; }
    .end-men    { font-size: 12px; }
}

/* ── THE STAGE: TWO STATES, NOT TWO ZONES ─────────────────────────────────── */
/* Everything here uses the shared tokens and nothing invents a colour: --surface,
   --border-hi, --label at 13px / 0.18em, --mono for numerals, zero radius. The
   stage is a new arrangement of the existing board, not a new look. */

.end-stage {
    display: grid;
    grid-template-columns: var(--end-board-w, 190px) 1fr;
    gap: 18px;
    /* stretch, not start: the media column has to be given a height before the
       media can fill one. */
    align-items: stretch;
    flex: 1;
    min-height: 0;
    transition: grid-template-columns var(--end-expand-ms, 520ms) cubic-bezier(.22,.9,.24,1);
}
.end-stage.is-event { --end-board-w: 62%; }

.end-stage-board { min-width: 0; }
.end-stage-media { min-width: 0; min-height: 0; display: flex; justify-content: flex-start; }
/* On an event the board takes the centre and the location is pushed out right. */
.end-stage.is-event .end-stage-media { justify-content: flex-end; }
/* Pushed out to the right on an event, and it gives up the cap so the board can
   have the centre. */
.end-stage.is-event .end-media { width: auto; max-width: 100%; }

/* THE LOCATION FILLS THE SPACE BELOW THE HEAD, IN 4:3.
   It was sized by WIDTH, which on a wide panel gives a wide short box — 4:3 of
   a 1000px column is 750px tall whether or not there is 750px to give. Sizing
   by HEIGHT instead makes the aspect ratio a constraint on the shape rather
   than a consequence of the column, so the frame is as large as the space
   allows and stays 4:3 at every width. */
.end-media {
    position: relative;
    height: 100%;
    width: auto;
    max-width: 100%;
    aspect-ratio: 4 / 3;
    background: #050505;
    border: 1px solid var(--border-hi);
    overflow: hidden;
    transition: opacity 300ms ease, filter 300ms ease;

    border-radius: var(--r);
}
/* Whether the displaced location freezes or keeps playing is a thing to settle
   by looking, so it is a class rather than a decision baked into the markup. */
.end-stage.is-event .end-media.is-frozen { filter: saturate(.3) brightness(.55); }

.end-map { display: block; width: 100%; height: 100%; }

/* The clip stands in for the 5 to 6 second location video. */
.end-clip {
    position: absolute; inset: 0;
    display: none; align-items: center; justify-content: center;
    /* Clear the chyron. The slate centred on the whole frame sat underneath it. */
    padding-bottom: 56px;
    background: #080808;
    background-image:
        repeating-linear-gradient(0deg, #ffffff05 0 1px, transparent 1px 3px);
}
.end-media.is-clip .end-clip { display: flex; }
.end-media.is-clip .end-map  { visibility: hidden; }

.end-clip-slate { text-align: center; }
.end-clip-id {
    display: block;
    font-family: var(--mono);
    font-size: 20px;
    letter-spacing: calc(0.14em * var(--track));
    color: var(--text);
}
.end-clip-shot {
    display: block;
    margin-top: 8px;
    font-size: 11px;
    letter-spacing: var(--label-track);
    color: var(--dim);
}

/* Which asset is on screen, so the pull is legible while it is being tuned. */
.end-media-tag {
    position: absolute; top: 10px; left: 12px;
    font-size: 10px;
    letter-spacing: var(--label-track);
    color: var(--yellow-dim);
}

/* THE CHYRON. Every punch-in is named. Not decoration: with eight parties and
   no fixed screen position there is no other way to know who is on screen. */
.end-chyron {
    position: absolute; left: 0; right: 0; bottom: 0;
    padding: 12px 14px;
    background: linear-gradient(to top, #000000e6 0%, #000000b0 55%, #0000 100%);
    transform: translateY(100%);
    transition: transform 320ms cubic-bezier(.22,.9,.24,1);
}
.end-chyron.is-on { transform: translateY(0); }
.end-chyron-who {
    font-size: 17px;
    letter-spacing: calc(0.06em * var(--track));
    text-transform: uppercase;
    color: var(--text);
}
/* WHAT HAPPENED, in the frame. It was a 13px line under the stage at the foot
   of the page — the update was landing outside the picture it was about. Yellow
   because it is the one line here that is news rather than a caption. */
.end-chyron-what {
    margin-top: 6px;
    font-size: 14px;
    letter-spacing: calc(0.03em * var(--track));
    color: var(--yellow);
}
.end-chyron-what:empty { display: none; }

.end-chyron-where {
    margin-top: 3px;
    font-size: 11px;
    letter-spacing: var(--label-track);
    color: var(--label);
}

/* The board reads as the active element while it holds the centre. */
.end-stage.is-event .end-stage-board .end-board-head { border-color: var(--yellow-dim); }

@media (max-width: 900px) {
    .end-stage { grid-template-columns: 1fr; }
    .end-stage.is-event { --end-board-w: 1fr; }
    .end-stage-media { order: -1; }
}

/* ── THE COMPACT BOARD ────────────────────────────────────────────────────── */
/* "Compact form" is a real constraint, not a look: the full row grid sums to
   256px and the resting column is 190px, so leaving it alone spilled the men and
   eta columns straight across the location panel. At rest the board carries what
   ranks a party — place, name, expected finish. Men remaining decides the result,
   so it comes back the moment the board takes the centre. */
/* THE COMPACT ROW IS GONE WITH THE RAIL. It dropped to place, name and finish
   because a 190px column could not hold more, and it was still winning over the
   seven-track row after the board moved to the centre: the board rendered three
   columns of the five and wrapped the rest onto a second line inside a 42px row,
   which is what the overlap was. The board has the full width now and shows all
   of it, all the time. */
.end-row .end-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── THE LOGLINE STANDS WHERE THE NAMES STOOD ─────────────────────────────── */
/* Nickolay, 2026-08-25: "remove all the explorer names at the top. keep the
   logline but change it to all caps, non-italic and change the font size to be
   the same as what the font size of the names we are removing is."

   THE SIZE IS READ, NOT COPIED. --vs-name-size is the token .vs-name itself now
   uses, so this is the same number at every width by construction. Writing the
   literal would have meant picking one of the three places the title size is set
   — and the one that wins on a desktop is the DENSITY block at the foot of the
   shared sheet, 40px x 0.6, not the 40px anyone would have copied.

   The negative top margin went with the names. It existed to pull the line up
   under them, and with nothing above it, it was pulling the line into the rule. */
/* ONE ROW, AND SIZED AGAINST THE PANEL RATHER THAN AGAINST A NUMBER.
   At the names' 24px this line wants 684px and the head is 584px, so it broke
   over two rows. A smaller fixed size would fit here and break again on the next
   narrower panel, so the size is a fraction of the container instead: 3.35cqw
   keeps it on one row at every width, capped at --vs-name-size so it never grows
   past the size the names had.

   NO white-space:nowrap ON PURPOSE. The copy comes from the catalog and Nickolay
   rewrites it. Nowrap would send a longer line off the edge of the panel; left
   wrappable, a longer line simply takes two rows again, which is visible and
   harmless rather than broken. */
.result-head { container-type: inline-size; }
.result-head .vs-tagline {
    /* Floored at 15px. Pure min() kept it on one row by taking it down to 11px on
       a phone, which is one row nobody can read — below the floor it wraps to two
       and stays legible, which is the better trade on a 375px screen. */
    font-size: clamp(15px, 3.35cqw, var(--vs-name-size));
    font-style: normal;
    text-transform: uppercase;
    line-height: 1.15;
    /* Bold, like the names it replaced — .vs-name is 700 and this line stands
       exactly where those stood. */
    font-weight: 700;
    color: var(--yellow);
    margin: 0 0 14px;
}

/* THE PROMPT MOVED UP under the logline, out of the foot of the idle state.
   It told you what to do after it had already shown you the odds board and the
   buttons, which is the wrong order to read them in. */
.result-head .idle-prompt { margin: 0 0 18px; font-weight: 700; }
/* And it goes when the result arrives. It lived inside #state-idle, which app.js
   hides on the first run; up here nothing was hiding it, so it would have sat
   under the logline telling you to run a passage you had already run. */
.result-panel:has(.result-inner:not([hidden])) .idle-prompt { display: none; }

/* THE BOARD FILLS THE PANEL. The shared rule caps it at 440px in a 584px column,
   which is right for two prices and leaves 72px of dead black either side of
   eight. */
#state-idle .odds-board { max-width: none; }

/* Once the passage is running the head is hidden outright, so what used to be
   here — the eight names stepping back to a quiet strip — has nothing left to
   act on. The head's own spacing while the result shows is still wanted. */
.result-panel:has(.result-inner:not([hidden])) .result-head {
    padding-bottom: 12px;
    margin-bottom: 10px;
}

/* ── THE EIGHT-WAY OUTRIGHT BOARD ─────────────────────────────────────────── */
/* Nothing is restyled. The board reuses .odds-board, .odds-board-head, .odds-row,
   .odds-name, .odds-american and .odds-prob straight from the shared sheet, so
   eight prices are set in the same type as two. The only difference eight rows
   need is a rule between them, since the two-sided board separates its pair by
   spacing that does not read once there are eight. */
.end-field-odds .odds-row + .odds-row { border-top: 1px solid var(--border); }
.end-field-odds .odds-row { padding: 6px 0; }

/* ── NOTE 8 AND 11: THE PAGE FONT ─────────────────────────────────────────── */
/* The controls column and the leaderboard were set in --mono, which reads as a
   terminal readout rather than as this platform. They now use --sans, the face
   the roster cards use (.rfighter). The GOLD UPPERCASE LABELS keep the register
   they had: ROUTE, CENTURY, TIME OF YEAR are labels, not prose, and .control-label
   already carries them. */
.controls-panel .toggle,
.controls-panel .control-detail {
    font-family: var(--sans);
}
.controls-panel .toggle { font-size: 15px; letter-spacing: calc(0.01em * var(--track)); }

.end-row,
.end-row .end-eta,
.end-row .end-men,
.end-row .end-place,
.end-board-head,
.end-day {
    font-family: var(--sans);
}
/* Numerals still line up in columns, which is what --mono was really buying. */
.end-row .end-eta,
.end-row .end-men,
.end-row .end-place,
.end-day { font-variant-numeric: tabular-nums; }

/* ── NOTE 10: BREAKING ────────────────────────────────────────────────────── */
/* Anything that stops, slows or kills a party is announced over the pictures.
   The flag flashes; the line under it does not, because a moving headline is
   harder to read than a still one and the line is the part carrying the fact. */
/* THE BREAKING CARD SITS ABOVE THE BOARD, NOT ON IT. It was absolutely placed
   at the top of the media box, which was correct while that box held a picture
   and wrong the moment the box started holding the standings: it covered the
   board's own head and the column names, so the one card announcing a change
   hid the thing the change had changed. It is in the flow now and the board
   begins under it. */
.end-breaking {
    position: relative;
    order: -1;
    left: auto; right: auto; top: auto;
    padding: 14px 16px 16px;
    background: linear-gradient(to bottom, #000000f2 0%, #000000cc 60%, #0000 100%);
    z-index: 3;
}
.end-breaking-flag {
    display: inline-block;
    padding: 5px 12px 4px;
    background: var(--red);
    color: #fff;
    font-family: var(--sans);
    font-size: 15px;
    font-weight: 800;
    letter-spacing: calc(0.22em * var(--track));
    text-transform: uppercase;
    animation: end-breaking-flash 900ms steps(1, end) infinite;
}
.end-breaking-line {
    margin-top: 9px;
    font-family: var(--sans);
    font-size: 17px;
    line-height: 1.35;
    color: var(--white);
    text-wrap: balance;
}
@keyframes end-breaking-flash {
    0%, 55%  { background: var(--red);   color: #fff; }
    56%, 100%{ background: #4a0d0d;      color: #ffb3b3; }
}
@media (prefers-reduced-motion: reduce) {
    .end-breaking-flag { animation: none; }
}

/* ── NOTE 9: ON AN EVENT THE BOARD TAKES THE WHOLE PAGE ───────────────────── */
/* THE PAGE ITSELF WIDENS. .app is capped at 1060px across the whole platform,
   which is right for a page you read and wrong for a reveal you watch: the rail
   collapsing bought 380px that the cap then refused to hand to the pictures.
   Scoped to the passage, and only on this category. */
body.end-running .app { max-width: 1720px; }
body.end-running { transition: none; }
.app { transition: max-width var(--end-expand-ms, 520ms) cubic-bezier(.22,.9,.24,1); }

/* The leaderboard used to grow inside the result panel while the controls column
   sat there unchanged. It now pushes the controls column off to the left and
   takes its place, and the location panel grows into everything the board just
   vacated plus what it already had. */
.main-grid {
    transition: grid-template-columns var(--end-expand-ms, 520ms) cubic-bezier(.22,.9,.24,1);
}
/* THE CONTROLS COLUMN STAYS, 2026-08-31. This collapsed it to zero and slid it
   out on every event, because the board used to be a narrow rail that needed the
   room to expand into. The board lives in the centre now, so it needs nothing
   from the left column, and Nickolay asked for the variables the run was set
   with to stay on screen the way they do on every other category. A page that
   throws away what the user chose the moment the run starts cannot answer "what
   did I set this to". */
.controls-panel {
    transition: opacity 260ms ease, transform var(--end-expand-ms, 520ms) cubic-bezier(.22,.9,.24,1);
}
/* With the rail gone the stage has the full width: the board takes the space the
   controls held, and the location takes the rest. */
.main-grid.is-event .end-media { width: auto; max-width: 100%; }
.main-grid.is-event .end-stage-media { justify-content: stretch; }

/* ── THE HEAD STANDS DOWN FOR THE PASSAGE ─────────────────────────────────── */
/* Nickolay, 2026-08-25: the band of names with RUN and PLAY WITH FRIENDS under
   it is "a moot point at this stage". Once the passage is running, it is: the
   names are the eight rows of the board, and neither button can be pressed.
   Before this it shrank to a quiet strip, which is still a strip.

   GATED ON body.end-running, WHICH IS THE RUN AND ONLY THE RUN. The obvious
   gate was :has(.result-inner:not([hidden])) like the rules above, but the
   result stays on screen after the passage ends — that selector would keep RUN
   hidden once it re-enabled, and there would be no way to run a second passage.
   app.js clears end-running at the same moment it re-enables the button. */
body.end-running .result-head { display: none; }

/* THE STAGE TAKES WHAT THE HEAD GAVE UP, both ways.
   Up is free: .result-inner is a flex column and the stage is its flex:1 child,
   so the height lands on the stage the moment the head stops occupying it.

   Right needs the track to change. The board was a fixed 360px with the location
   on 1fr, so the location got a column WIDER than a 4:3 box of that height could
   fill and the difference sat as dead space to its right. Reversing them — board
   elastic, location sized to its own content — puts the frame's right edge on
   the stage's right edge and hands the slack to the board, which wanted it: the
   names were being truncated at 360px. */
.main-grid.is-event .end-stage { grid-template-columns: minmax(300px, 1fr) auto; }

/* ── THE COLUMN THAT CARRIES THE HEIGHT DOWN ──────────────────────────────── */
/* .result-panel is a centred flex column with a min-height. For the stage to
   fill what is left under the head, every wrapper between them has to pass a
   height through rather than shrink to its content. */
.result-panel:has(.result-inner:not([hidden])) { justify-content: flex-start; }
.result-inner { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.end-stage-board { display: flex; flex-direction: column; min-height: 0; }

/* ── THE BOARD TAKES THE CENTRE, 2026-08-31 ────────────────────────────────
   Nickolay: the leaderboard sits in the middle, inside the clip box, and the
   left column keeps the variables the run was set with. So the stage stops
   being a rail beside a picture and becomes one column holding the board. */
.end-stage { grid-template-columns: 1fr; }
.end-stage.is-event { --end-board-w: 1fr; }
.end-stage-media { justify-content: center; }
.end-stage.is-event .end-stage-media { justify-content: center; }

.end-boardwrap {
    width: 100%;
    display: flex;
    flex-direction: column;
    min-height: 0;
    padding: 18px 20px 20px;
}

/* SEVEN TRACKS: the rank, the name, and the five.
   The name takes the slack so the numerals stay in fixed columns and read down
   the board rather than drifting with the length of a name. */
.end-board-cols,
.end-stage .end-row {
    display: grid;
    grid-template-columns: 34px minmax(120px, 1fr) 78px 92px 62px 62px 84px;
    align-items: center;
    gap: 0 10px;
}
.end-board-cols {
    padding: 0 0 8px;
    border-bottom: 1px solid var(--border-hi);
    margin-bottom: 4px;
    font: 600 10px/1.25 var(--sans, system-ui);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--label, #8a8a8a);
}
/* The five headings are over numerals, so they sit over them. */
.end-board-cols span:nth-child(n+3) { text-align: right; }

.end-stage .end-num {
    font-family: var(--mono, ui-monospace, Menlo, monospace);
    font-size: 14px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.end-stage .end-lost.is-thin  { color: var(--yellow, #FFCC00); }
.end-stage .end-lost.is-grave { color: var(--down, #d64030); }
.end-stage .end-lead { color: var(--label, #8a8a8a); }

/* The place-change colours, which carry only for the second or two of the move
   rather than as a standing property of the row. */
.end-stage .end-row .end-arrow { font-style: normal; margin-left: 8px; font-size: 10px; }

/* The media box becomes a column so the breaking card can take a place in it
   above the board rather than floating over the top of it. */
.end-media { display: flex; flex-direction: column; }
.end-boardwrap { flex: 1; }

/* ── THE FOUR STATES A ROW CAN BE IN ───────────────────────────────────────
   Nickolay, 2026-08-31: a party moving up turns green during the switch and
   the one it passed turns red; a party that has quit stays red to the end;
   the leader is always gold.

   They are written in that order of precedence deliberately. Quitting outranks
   moving, because a party that has stopped is not overtaking anyone. Moving
   outranks the lead, because taking the lead IS a move and should be seen as
   one before it settles into gold. */
:root { --up: #19c37d; --down: #e2553f; }

/* GOLD, the standing state of the leader. */
.end-row.is-lead .end-place,
.end-row.is-lead .end-name { color: var(--yellow); font-weight: 700; }

/* GREEN and RED, for the length of the move only. */
.end-row.is-up   .end-place, .end-row.is-up   .end-name,
.end-row.is-up   .end-num,   .end-row.is-up   .end-arrow { color: var(--up); }
.end-row.is-down .end-place, .end-row.is-down .end-name,
.end-row.is-down .end-num,   .end-row.is-down .end-arrow { color: var(--down); }
.end-row.is-up   { background: rgba(25, 195, 125, .10); }
.end-row.is-down { background: rgba(226, 85,  63, .12); }
.end-row.is-up, .end-row.is-down { transition: background 220ms ease, color 220ms ease,
    transform 0.55s cubic-bezier(0.4, 0, 0.2, 1); }

/* RED TO THE END for a party that has stopped. It was grey at 30%, which reads
   as absent rather than as out: a party forced back is part of the story of the
   race and stays legible to the finish. */
.end-row.is-out { opacity: 1; }
.end-row.is-out .end-place,
.end-row.is-out .end-name,
.end-row.is-out .end-num  { color: var(--down); opacity: .72; }
.end-row.is-out .end-eta  { color: var(--down); opacity: 1; }

/* ── THE OVERTAKE COMES TOWARD THE VIEWER ──────────────────────────────────
   Two rows swapping in a list of eight is a small movement over a short
   distance and was easy to miss even in colour. The pair lifts toward the
   viewer, trades places at that size, then recedes into the new order.

   The scale rides a custom property because the transform also carries the
   row's position, which JavaScript writes. Composing them in the same string
   from one function is what stops the two from overwriting each other. */
.end-row { --row-scale: 1; transform-origin: center left; }
.end-row.is-moving {
    --row-scale: 1.085;
    z-index: 5;                 /* over its neighbours while it is forward */
    box-shadow: 0 6px 26px rgba(0, 0, 0, .55);
}
.end-row.is-moving .end-name { font-weight: 700; }
/* Scale and colour settle together, and the position keeps its own longer
   curve so the row travels while it is still forward. */
.end-row {
    transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1),
                background 220ms ease,
                color 220ms ease,
                box-shadow 260ms ease;
}

/* ── HOVERING A ROUTE OPENS IT, DIMLY ──────────────────────────────────────
   Nickolay, 2026-08-31: hovering a route title turns it dimmed gold and opens
   its description; choosing it turns it bright gold. The drawer used to open
   only on the route already chosen, which is the one route a reader has least
   need to read about. */
.end-route-item.is-preview .end-route-blurb { grid-template-rows: 1fr; }
/* DIMMED GOLD, WRITTEN OUT. color-mix(in srgb, var(--yellow) 62%, transparent)
   resolved achromatic here — a light grey with no yellow in it at all — so the
   hover state was the right shape and the wrong colour. Mixing toward
   transparent also mixes toward black in srgb, which is not what dimming a gold
   means. These are the gold at reduced alpha, which is. */
.end-route-group .toggle.is-preview {
    color: rgba(255, 204, 0, .70);
    border-color: rgba(255, 204, 0, .38);
    background: rgba(255, 204, 0, .07);
}
/* The chosen route is bright, and stays bright while the cursor is elsewhere. */
.end-route-group .toggle.active,
.end-route-group .toggle.active.is-preview {
    color: #111;
    background: var(--yellow);
    border-color: var(--yellow);
}

/* ── THE LIVE ODDS BOX, SIZED TO THE LEADERBOARD ────────────────────────────
   Nickolay, 2026-09-01: far too large, with a lot of dead black space in the
   middle. Size it to match the leaderboard box that appears once the matchup
   starts.

   MEASURED RATHER THAN EYEBALLED. The leaderboard it hands over to is 636 x 336:
   eight rows at 42px, no gap between them, no padding, no border. The odds board
   was 696 x 457 — eight rows at 46px with a 4px gap, a 42px head and 26px of
   vertical padding — so it stood 60px wider and 121px taller than its own
   successor, and the extra width is where the dead space down the middle came
   from. The numbers below are the leaderboard's own. Only the head is left over,
   and the leaderboard carries a head of its own above the box.
   Scoped to the field board so no other category's odds box moves. */
/* The ID above (#state-idle .odds-board) outranks a bare class, so the width has
   to be set at the same weight or it is silently dropped. It was: padding landed,
   max-width did not, and the box stayed 696 wide while the rule read 636. */
#state-idle .end-field-odds {
    max-width: 636px;
    margin-left: auto;
    margin-right: auto;
    padding: 8px 0;
}
.end-field-odds .odds-board-head {
    margin-bottom: 8px;
    padding-bottom: 8px;
    padding-left: 10px;
    padding-right: 10px;
}
.end-field-odds .odds-rows { gap: 0; }
/* 4px vertical keeps the row at 42px, which is the leaderboard's row height exactly
   (was 46). 10px horizontal is also the leaderboard's, measured off .end-row rather
   than chosen: it sets its cells in `padding: 0 10px`.

   Nickolay, 2026-09-01: the names and the percentages were touching the edge of the
   box. They were 2px off it. Horizontal padding was 0 here, copied from the
   eight-way board rule above, and the box had none of its own either, so nothing
   stood between the text and the border.

   The padding sits on the ROW rather than on the board, so the rules between rows
   still run the full width while the text steps in. Putting it on the board would
   have pulled the rules in with it. */
.end-field-odds .odds-row { padding: 4px 10px; }

/* ── THE HOLD ON A RUNNING PASSAGE ──────────────────────────────────────────
   Nickolay, 2026-09-01: once a run starts there is no way to stop it, go back or
   pause. Two answers went in: ALL MATCHUPS in the header, which leaves, and this,
   which holds. It was a STOP for one revision and was wrong: it cleared the run
   and put you back on the odds board, which loses the frame you paused to look at.

   Deliberately quiet rather than gold while running: gold on this page is the lamp
   on the launch button, and a second gold control competing with it during the one
   moment the page is doing what it was launched to do would read as the next step
   rather than a control. Held is the exception — a paused passage should not look
   like a running one, so RESUME is the one state that lights up. */
.end-conditionrow {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
}
.end-conditionrow .end-condition { padding-bottom: 14px; }
.end-hold-btn {
    flex: none;
    padding: 5px 12px;
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: calc(0.14em * var(--track));
    text-transform: uppercase;
    color: var(--muted);
    background: rgba(0, 0, 0, 0.34);
    border: 1px solid var(--border-hi);
    border-radius: var(--r);
    cursor: pointer;
    transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.end-hold-btn:hover,
.end-hold-btn:focus-visible {
    color: var(--yellow);
    border-color: rgba(255, 204, 0, 0.55);
    background: rgba(255, 204, 0, 0.10);
}

/* HELD. The one state that announces itself: a paused passage looks paused. */
.end-hold-btn.is-held {
    color: #000;
    background: var(--yellow);
    border-color: var(--yellow);
}
.end-hold-btn.is-held:hover { background: #ffd633; border-color: #ffd633; color: #000; }

/* ══ BATCH 3: COUNTDOWN, ANNOUNCEMENT, ROW ZOOM ═════════════════════════════
   Nickolay, 2026-09-01. All three cover the run box, which is where the
   leaderboard sits, because each one is the thing to look at while it is up.
   Every duration is a parameter in app.js TIMING, not a constant in here, so a
   timing note can be answered without touching the stylesheet. */

.end-countdown,
.end-announce,
.end-zoom {
    position: absolute;
    inset: 0;
    z-index: 6;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    pointer-events: none;
}
.end-countdown[hidden],
.end-announce[hidden],
.end-zoom[hidden] { display: none; }

/* ── 1. COUNTDOWN TO LAUNCH ─────────────────────────────────────────────── */
/* TRANSPARENT, per the note. The count belongs over the map, not over a
   black panel: the board is already held back for it, so there is nothing
   behind it that needs covering. A trace of wash keeps the white numerals
   off bare jungle without hiding the country. */
.end-countdown { background: rgba(3, 3, 3, 0.14); }
.end-countdown-label {
    color: var(--red);
    font-family: var(--sans);
    font-weight: 800;
    font-size: 22px;
    letter-spacing: calc(0.28em * var(--track));
    text-transform: uppercase;
}
.end-countdown-n {
    margin-top: 8px;
    color: #fff;
    font-family: var(--sans);
    font-weight: 800;
    font-size: 132px;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    animation: end-count-tick 900ms ease-out;
}
/* THE NUMBER DOES NOT CHANGE SIZE. Nickolay, 2026-09-02.
   It was scaling from 1.35 down to 1 on every tick, which made five different
   sizes out of one countdown. The scale also carried a second fault: the number
   is centred with `transform: translateY(-50%)`, and a keyframe that sets
   `transform` REPLACES that translate rather than adding to it, so the digit
   dropped half its own height for the length of the animation and rose back.
   Removing the transform fixes the size and the position in one move: with only
   opacity animating, the centring transform is left alone. */
@keyframes end-count-tick {
    0%   { opacity: 0; }
    30%  { opacity: 1; }
    100% { opacity: 1; }
}

/* ── 4. THE ANNOUNCEMENT ────────────────────────────────────────────────────
   BREAKING is 90px, which is 6x the 15px flag it replaces. The board behind is
   dimmed rather than hidden: the announcement is about a row on it. */
.end-announce { background: rgba(3, 3, 3, 0.86); padding: 0 28px; }
/* superseded by batch 4b: the board leaves rather than dims. */

.end-announce-flag {
    color: var(--red);
    font-family: var(--sans);
    font-weight: 800;
    font-size: 90px;
    line-height: 1;
    letter-spacing: calc(0.06em * var(--track));
    text-transform: uppercase;
    transition: opacity 420ms ease;
}
.end-announce-flag.is-out { opacity: 0; }
.end-announce-flag[hidden] { display: none; }

.end-announce-body { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.end-announce-who,
.end-announce-cost,
.end-announce-delay {
    font-family: var(--sans);
    text-transform: uppercase;
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 420ms ease, transform 420ms ease;
}
.end-announce-who.is-in,
.end-announce-cost.is-in,
.end-announce-delay.is-in { opacity: 1; transform: none; }
.end-announce-who[hidden],
.end-announce-cost[hidden],
.end-announce-delay[hidden] { display: none; }

/* ITEM 6: the party name is WHITE under a RED obstacle. It was gold, which put
   two signal colours in one card and made the name compete with the headline it
   is meant to sit beneath. */
.end-announce-who   { color: #fff; font-weight: 800; font-size: 42px; line-height: 1.14;
                      letter-spacing: calc(0.05em * var(--track)); }
.end-announce-cost  { color: #fff;          font-weight: 700; font-size: 30px;
                      letter-spacing: calc(0.10em * var(--track)); }
.end-announce-delay { color: var(--red);    font-weight: 800; font-size: 30px;
                      letter-spacing: calc(0.10em * var(--track)); }

/* ── 7. THE ROW ZOOM ────────────────────────────────────────────────────────
   The announced row and the rows it is about to change places with, lifted off
   the board and brought forward. The move happens here, at this size, so the
   change is legible; then everything clicks back. */
.end-zoom { background: rgba(3, 3, 3, 0.72); }
.end-zoom-stack {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: min(88%, 620px);
    animation: end-zoom-in 420ms cubic-bezier(.2,.8,.25,1);
}
@keyframes end-zoom-in {
    0%   { transform: scale(0.72); opacity: 0; }
    100% { transform: scale(1);    opacity: 1; }
}
.end-zoom.is-out .end-zoom-stack { animation: end-zoom-out 380ms ease-in forwards; }
@keyframes end-zoom-out {
    0%   { transform: scale(1);    opacity: 1; }
    100% { transform: scale(0.72); opacity: 0; }
}

.end-zrow {
    display: grid;
    grid-template-columns: 54px 1fr auto;
    align-items: center;
    gap: 14px;
    padding: 12px 16px;
    background: rgba(12, 12, 12, 0.96);
    border: 1px solid var(--border-hi);
    border-radius: var(--r);
    font-family: var(--sans);
    /* The reorder itself: rows slide to their new places rather than cutting. */
    transition: transform 700ms cubic-bezier(.2,.8,.25,1),
                border-color 300ms ease, background 300ms ease;
}
.end-zrow-place { font-size: 30px; font-weight: 800; color: var(--muted); font-variant-numeric: tabular-nums; }
.end-zrow-name  { font-size: 26px; font-weight: 700; color: #fff; text-align: left; }
.end-zrow-eta   { font-size: 20px; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Green for up, red for down, on the party the announcement was about. */
.end-zrow.is-subject { border-color: var(--yellow); }
.end-zrow.is-up      { border-color: #19c37d; background: rgba(25, 195, 125, 0.14); }
.end-zrow.is-up    .end-zrow-place, .end-zrow.is-up   .end-zrow-name { color: #19c37d; }
.end-zrow.is-down    { border-color: #e2553f; background: rgba(214, 64, 48, 0.14); }
.end-zrow.is-down  .end-zrow-place, .end-zrow.is-down .end-zrow-name { color: #e2553f; }

@media (prefers-reduced-motion: reduce) {
    .end-countdown-n, .end-zoom-stack { animation: none; }
    .end-zrow { transition: none; }
}

/* ══ BATCH 4: THE RUN TAKES THE PAGE ═══════════════════════════════════════
   Nickolay, 2026-09-02, items 1, 2, 3 and 5.

   ITEM 1 REVERSES A DECISION FROM 31 AUGUST, deliberately. The controls column
   was kept on screen through the run so the page could still answer "what did I
   set this to". The run box now takes the whole width instead, and the answer to
   that question moves to the condition bar inside it, which already carries the
   route, century and season.

   THE SHAPE IS PINNED THIS TIME. The old `aspect-ratio: 4 / 3` on .end-media was
   dead: the same rule set `height: 100%`, and aspect-ratio only governs when one
   axis is auto, so the layout resolved both and the declaration did nothing. It
   rendered between 1.02 and 1.05 depending on viewport HEIGHT, which is why it
   looked square and why it drifted. Height is released below so the ratio can
   actually hold. */

body.end-running .main-grid {
    grid-template-columns: 1fr;          /* one column: the run box and nothing else */
}
/* The side columns leave rather than disappear, so the eye follows them out. */
body.end-running .controls-panel,
body.end-running .nextup {
    display: none;
}
body.end-running .app { max-width: none; padding-left: 24px; padding-right: 24px; }

/* ── ITEM 1: A SINGLE 16:9 BOX, FULL WIDTH ──────────────────────────────── */
body.end-running .result-panel {
    padding: 0;
    min-height: 0;
    background: none;                    /* ITEM 3 */
    border-radius: 0;
}
body.end-running .end-stage {
    grid-template-columns: 1fr;
    gap: 0;
}
body.end-running .end-stage-media { justify-content: stretch; }
body.end-running .end-media {
    width: 100%;
    height: auto;                        /* releases the axis the old rule pinned */
    max-width: none;
    aspect-ratio: 16 / 9;
    /* ITEM 3: the box is transparent, so the map behind it is the ground the run
       plays on. The hairline stays: without it the run has no edge at all. */
    background: none;
    border-color: rgba(255, 255, 255, 0.10);
}

/* ── ITEM 3: EVERYTHING INSIDE STOPS PAINTING ITS OWN GROUND ────────────────
   The board, its wrapper and the announcement each carried a dark fill. Left
   alone they would sit on the map as three stacked panels and the transparency
   would only be visible at the edges. */
body.end-running .end-stage-board { background: none; }
body.end-running .end-announce { background: rgba(3, 3, 3, 0.55); }
body.end-running .end-zoom      { background: rgba(3, 3, 3, 0.42); }

/* The board's rows need to hold their own legibility now that nothing sits behind
   them. A scrim under the board only, not the whole frame, so the map stays open
   where the board is not. */
/* ONE SURFACE, NOT THREE. The scrim was painted separately on the board, its
   column heads and its head band, so each drew its own gradient and the seams
   between them read as the panel breaking into pieces over the map. It goes on
   the wrapper now, once, and the three children paint nothing. */
body.end-running .end-boardwrap {
    /* A QUARTER MORE TRANSPARENT. Nickolay, 2026-09-02. Each stop reduced by 25%
       of its own value rather than by a flat amount, so the gradient keeps its
       shape: it still opens up toward the right where the map is worth seeing and
       holds where the names and numbers are. 0.86 / 0.78 / 0.42 -> 0.65 / 0.59 / 0.32. */
    background: linear-gradient(to right,
        rgba(6, 8, 10, 0.65) 0%, rgba(6, 8, 10, 0.59) 62%, rgba(6, 8, 10, 0.32) 100%);
}
body.end-running .end-board,
body.end-running .end-board-cols,
body.end-running .end-board-head,
body.end-running .end-row { background: none; }
body.end-running .end-row { text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9); }

/* ── ITEM 5: THE COUNTDOWN SITS HIGH, THE NUMBER SITS CENTRE ────────────────
   The pair was centred together, which put the label in the middle of the frame
   and the number below it. The label belongs in the top third and the number on
   the centre line. */
.end-countdown { justify-content: flex-start; }
.end-countdown-label {
    position: absolute;
    top: 33.333%;
    left: 0; right: 0;
    transform: translateY(-50%);
}
.end-countdown-n {
    position: absolute;
    top: 50%; left: 0; right: 0;
    transform: translateY(-50%);
    margin-top: 0;
}

/* ── ITEM 1, CONTINUED: THE WIDTH HAS TO BE THE THING THAT LEADS ────────────
   First attempt gave an exact 16:9 that was 1536 wide inside an 1872 column,
   sitting left of centre. The ratio was holding, but backwards: the wrappers
   between .result-inner and .end-media all stretch, so the parent handed down a
   height and aspect-ratio derived the WIDTH from it. Releasing the height makes
   width lead, which is what "occupying the width" means.

   The cap stays, because a 16:9 box across 1872px is 1053px tall and the page
   would scroll during a passage. A reveal you have to scroll is not a reveal. So
   it fills the width where the height allows and fits the height where it does
   not, centred either way. */
body.end-running .result-inner,
body.end-running .end-stage,
body.end-running .end-stage-media,
body.end-running .end-stage-board {
    height: auto;
    min-height: 0;
}
/* NOT align-items:center on these. They are flex COLUMNS, so centring on the
   cross axis is centring the WIDTH, and it shrank every wrapper to its content:
   the box came out 634px wide inside an 1872px column, neatly centred and wrong.
   They stay stretched; only the media row centres. */
body.end-running .end-stage-media {
    justify-content: center;
    align-items: flex-start;
}
/* SPECIFICITY: `.main-grid.is-event .end-media` sets width:auto and is three
   classes; `body.end-running .end-media` is two classes plus an element and
   loses to it, so width:100% was being discarded and the box sized itself from
   its content at 634px. Scoped through .end-stage to outrank it. */
body.end-running .end-stage .end-media {
    flex: none;                 /* a flex item shrinks by default; this one must not */
    /* 16:9 HOLDS, AND THE WIDTH GIVES WAY WHERE IT MUST.
       max-height and an explicit width fight: the cap clipped the box to 1872x850,
       a ratio of 2.20, so the shape broke rather than the size. Taking the lesser
       of the column width and the width that the AVAILABLE HEIGHT allows at 16:9
       keeps the ratio exact at every viewport and never scrolls.

       On a 1920x1080 screen the two cannot both be had: a full-width 16:9 box is
       1053px tall and only about 850px is free under the header, which stays per
       item 1. So it fills the height and centres, and the width is as much as the
       shape permits. On a taller or narrower screen it fills the width instead. */
    --end-run-chrome: 230px;          /* header, brand line, condition bar, margin */
    width: min(100%, calc((100vh - var(--end-run-chrome)) * 16 / 9));
    height: auto;
    max-width: none;
    aspect-ratio: 16 / 9;
    margin: 0 auto;
}

/* ══ BATCH 4b ══════════════════════════════════════════════════════════════
   Nickolay, 2026-09-02, after looking at it.

   THE EXPANSION WAS A CUT, NOT A MOVE. The square box became a 16:9 box in one
   frame. The box now grows into its shape over a second, and the side columns go
   with it rather than vanishing under it.

   The columns animate by track width rather than by `display: none`, which cannot
   be transitioned. The grid keeps its three tracks and two of them close to zero,
   so the box widens into the room they give up instead of jumping into it. */
:root { --end-expand-ms: 1000ms; }

body.end-running .main-grid {
    grid-template-columns: 0 1fr 0;
}
body.end-running .controls-panel,
body.end-running .nextup {
    display: block;
    opacity: 0;
    overflow: hidden;
    min-width: 0;
    padding-left: 0;
    padding-right: 0;
    pointer-events: none;
    transition: opacity 380ms ease;
}
body.end-running .end-stage .end-media {
    transition: width var(--end-expand-ms) cubic-bezier(.22, .9, .24, 1),
                aspect-ratio var(--end-expand-ms) cubic-bezier(.22, .9, .24, 1);
}

/* ── ONE THING IN THE BOX AT A TIME ────────────────────────────────────────
   The announcement was transparent over the leaderboard, which was itself over
   the map: three layers, and the headline read against the standings it was
   interrupting. The board now leaves while an announcement is up and comes back
   when it clears. It is a fade rather than a cut, so the board's return is the
   signal that the announcement is over. */
.end-boardwrap { transition: opacity 420ms ease; }
.end-media.is-dimmed .end-boardwrap {
    opacity: 0;
    pointer-events: none;
    filter: none;            /* replaces the brightness dim, which left it legible */
}

/* ══ THE TRACK LAYER ═══════════════════════════════════════════════════════
   Note 3, 2026-09-02. Eight markers on the page background, under the run box,
   in the register of the reference frame: a soft halo, a hard ring, a small
   core, and the name set beside it rather than under it. */
#end-track {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 700ms ease;
    z-index: 1;                        /* above #bg-img, below #bg-overlay's content */
}
#end-track.is-on { opacity: 1; }
#end-track-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

.end-mark { --mark: #5fe3d0; transition: transform 900ms cubic-bezier(.34,.72,.3,1); }
.end-mark-halo {
    fill: var(--mark);
    opacity: 0.14;
}
.end-mark-ring {
    fill: none;
    stroke: var(--mark);
    stroke-width: 2;
    opacity: 0.95;
}
.end-mark-core { fill: var(--mark); }
.end-mark-label {
    fill: var(--mark);
    font-family: var(--sans);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: calc(0.14em * var(--track));
    paint-order: stroke;
    stroke: rgba(0, 0, 0, 0.75);
    stroke-width: 3.5;
    stroke-linejoin: round;
}
/* The leader's ring pulses; nothing else moves, so the eye finds it without
   hunting. A party that is out stops pulsing and goes red. */
.end-mark[data-lead="1"] .end-mark-ring { animation: end-mark-pulse 2.4s ease-out infinite; }
.end-mark[data-state="out"]  { opacity: 0.55; }
.end-mark[data-state="out"] .end-mark-core { fill: none; stroke: var(--mark); stroke-width: 2; }
@keyframes end-mark-pulse {
    0%   { r: 9;  opacity: 0.95; }
    70%  { r: 19; opacity: 0; }
    100% { r: 19; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .end-mark { transition: none; }
    .end-mark[data-lead="1"] .end-mark-ring { animation: none; }
}

/* Each party's own line, drawn only where it has actually walked. */
.end-trail {
    stroke: var(--mark, #5fe3d0);
    stroke-width: 3.4;   /* note 1: thicker */
    opacity: 0.42;
    stroke-linecap: round;
    fill: none;
}
/* HALTED. A party inside a stopped stretch of its leg. The ring stops being a
   ring and becomes a bar, which reads as "held" at a glance and does not rely on
   the viewer noticing an absence of motion. */
.end-mark[data-state="halted"] .end-mark-ring { stroke-dasharray: 3 4; opacity: 0.8; }
.end-mark[data-state="halted"] .end-mark-halo { opacity: 0.06; }

/* A HALTED LEADER DOES NOT PULSE. The pulse says "moving at the front"; on a
   party that has stopped it says the opposite of what happened. */
.end-mark[data-state="halted"][data-lead="1"] .end-mark-ring { animation: none; }

/* ── THE CATEGORY NAV STANDS DOWN FOR A PASSAGE ────────────────────────────
   Nickolay, 2026-09-02. The eight category names run across the top right, and
   the map now runs underneath them: a party crossing that corner puts its label
   through ATHLETICS · COMBAT · HORSEPOWER and both become unreadable.

   The links go, and so does the ☰ that opens them below 1200px, since a menu
   whose only contents are hidden is a button that does nothing. SIGN IN and the
   account entry stay: they are not category names and they are the one control
   a viewer might still want mid-passage. */
body.end-running .header-links,
body.end-running .header-menu-toggle {
    display: none;
}

/* ── THE COUNT STANDS ALONE OVER THE MAP ───────────────────────────────────
   Nickolay, 2026-09-02: "we still see through the countdown to the leaderboard
   under. We should see through the countdown to the map."
   The board is not hidden with `hidden`, so the panel keeps its size and nothing
   jumps when it arrives; it simply is not painted yet. */
.end-media.is-counting .end-boardwrap { opacity: 0; pointer-events: none; }
.end-media.is-counting .end-board-head,
.end-media.is-counting .end-board-cols { opacity: 0; }

/* ── THE BOARD LOSES ITS HEADINGS ──────────────────────────────────────────
   Nickolay, 2026-09-02: remove EXPECTED FINISH, EXPLORER and the day counter,
   and close the gap they leave.

   The column heads above the numbers stay: PROJECTED FINISH, DAYS TRAVELLED and
   the rest are doing work, because those columns are not self-describing. The
   three going are the ones that label things the reader can already see: the
   board is obviously a board, the first column is obviously the explorers, and
   the day is now carried per party in DAYS TRAVELLED.

   Hidden rather than removed, so app.js can keep writing to the day element
   without a guard and nothing else has to know. */
body.end-running .end-board-title,
body.end-running .end-day { display: none; }
body.end-running .end-board-head {
    /* it held a title and a day; with both gone it is only the rule under them */
    min-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: none;
}
/* EXPLORER is the first cell of the column-head row; the row keeps its other
   labels, so the cell is emptied rather than the row removed. */
body.end-running .end-board-cols > span:nth-child(2) { font-size: 0; }
body.end-running .end-boardwrap { padding-top: 10px; }

/* ══ THE BOARD, RESET LEFT WITH CENTRED FIGURES ════════════════════════════
   Nickolay, 2026-09-02: move the columns left, fit each column name on one row,
   and centre the numbers under the name.

   The old track had the data columns right-aligned and pushed to the far edge of
   a 16:9 box, so the names sat a long way from the figures they described and the
   whole board read as two separate things. Each column is now sized to hold its
   own name on one line, the set is packed to the left, and the head and the value
   share one alignment so a figure sits under the middle of its own label. */
body.end-running .end-board-cols,
body.end-running .end-row {
    grid-template-columns: 34px minmax(150px, 300px)
                           124px 118px 122px 84px 84px;
    justify-content: start;
    column-gap: 18px;
}
/* Head and value take the same alignment, or they cannot line up. */
body.end-running .end-board-cols span:nth-child(n+3),
body.end-running .end-row .end-num {
    text-align: center;
}
body.end-running .end-board-cols span {
    white-space: nowrap;      /* one row per name, per the note */
    line-height: 1.2;
}
/* The rank and the name stay left; only the figures centre. */
body.end-running .end-board-cols span:nth-child(1),
body.end-running .end-board-cols span:nth-child(2) { text-align: left; }

/* The rows carry `padding: 0 10px` and the column heads do not, so every figure
   sat exactly 10px left of the label above it. Measured, identical on all five
   columns, which is the signature of a padding mismatch rather than a grid one. */
body.end-running .end-board-cols { padding-left: 10px; padding-right: 10px; }

/* THE ARRIVAL MATCHES THE REST. I made it gold on the reasoning that reaching the
   Caribbean is the leg's one piece of good news. Nickolay, 2026-09-02: it should be
   red like every other announcement, in the same face at the same size. He is
   right that a headline changing colour by category makes the category the subject
   rather than the event. */

/* ── THE ANNOUNCEMENT: TRANSPARENT, AND ANCHORED HIGH ──────────────────────
   Nickolay, 2026-09-02. Two changes, and the second is the one that matters.

   The card stacked DOWNWARD from centre, so each line pushed the block lower and
   the fourth line finished near the bottom of a 16:9 box. The headline is now
   pinned in the top third and the lines below it hang from there, so the red line
   is in the same place every time and only the tail grows.

   Transparent to match the countdown: the board is already faded out behind an
   announcement, so there is nothing back there that needs covering. */
body.end-running .end-announce {
    background: rgba(3, 3, 3, 0.14);
    justify-content: flex-start;
    padding-top: 0;
}
/* HIGHER, AND STACKED RATHER THAN OVERLAID.
   Both were pinned to the same 33.3% line, the headline centred on it and the body
   hung 62px below it. That works for a one line headline and breaks for a two line
   one: SUPPLIES EXHAUSTED BEFORE THE COAST grew downward through its own body and
   the party name landed inside the second line of red.

   They are in normal flow now, in a block anchored near the top of the frame, so
   the headline starts high and everything below it follows whatever height it
   takes. Nothing can overlap anything. */
body.end-running .end-announce {
    justify-content: flex-start;
    padding-top: 11%;
}
body.end-running .end-announce-flag {
    position: static;
    transform: none;
    margin: 0 0 26px;
    padding: 0 28px;
}
body.end-running .end-announce-body {
    position: static;
    margin: 0;
    padding: 0 28px;
    gap: 14px;
}
/* Over a moving map with no panel behind it, the type carries its own contrast. */
body.end-running .end-announce-flag,
body.end-running .end-announce-who,
body.end-running .end-announce-cost,
body.end-running .end-announce-delay {
    text-shadow: 0 2px 14px rgba(0, 0, 0, 0.95), 0 0 42px rgba(0, 0, 0, 0.8);
}

/* ── THE BOX REACHES THE RULE ──────────────────────────────────────────────
   Nickolay, 2026-09-02: the route name goes inside the box, above the explorer
   names, which frees the row it was in so the box can rise to the black line. */
body.end-running .end-conditionrow { display: none; }   /* nothing left in it */

/* The route heads the board, in the board's own left column so it starts on the
   same line as the explorer names beneath it. */
body.end-running .end-boardwrap > .end-condition {
    padding: 0 10px 10px;
    margin: 0;
    font-size: 13px;
}

/* ── THE HOLD SITS IN THE HEADER BAND ──────────────────────────────────────
   Nickolay, 2026-09-02: "move the Pause Passage to just above the black line and
   the top right of the run box."

   It is a child of the header now rather than of the media box, because the box
   carries overflow: hidden and nothing parented inside it can sit above its own
   edge. It is pinned to the same bottom band as CATEGORY UNDER CONSTRUCTION, so
   the two share one line above the rule with the back link.

   `right: 0` is the app's content edge, which during a run is exactly the run
   box's right edge, since the box spans the full width by then. */
body.end-running .end-hold-btn {
    position: absolute;
    right: 0;
    /* The same 18px as .category-state, so the hold and CATEGORY UNDER
       CONSTRUCTION sit on one band rather than four pixels apart. */
    bottom: 18px;
    top: auto;
    z-index: 7;
}

/* And with the row gone, the panel closes up to the header rule. */
body.end-running .result-panel { padding-top: 0; }
body.end-running .result-inner { padding-top: 0; }
