/* ==========================================================================
   GRIDROP · OVERTAKE — THE FINAL STANDINGS, raised to the owner's reference.

   Loaded AFTER felt-final.css, so at equal specificity this file wins and no
   `!important` is needed anywhere in it. Every selector is scoped under
   `body.ot-fin` (a handful under `.ofin`, which exists only on this screen) —
   a bare `.btn` / `.backbtn` / `.av` rule would leak onto the menu, the home,
   the lobby, the ready-check, the browse list, the racebar, the invite row,
   the round transition and the QR scanner on BOTH pages.

   It DECORATES the DOM felt.js paints plus the five nodes finale.js adds
   (`.ofin-title`, `.ofin-rank`, `.ofin-bar > i`, `svg.ofin-see`,
   `.ofin-go.empty`) — see .claude/gauntlet-fin/CONTRACT-DOM.md. Nothing here
   renames, moves or recomputes anything.

   SIZES COME FROM THE VARIABLES felt.js ALREADY PUBLISHES on `body`
   (felt.js:1942-2022 writes --finW/--finPad/--finHeadF/--podH/--avS/--avS2/
   --restH/--restAv/--restNmF/--restPtF/--restRkF/--goW/--goH/--exitF … each
   clamped off one R = clamp(min(W/1600,H/900),.34,1.12)). Where a new size was
   needed it is published here as its own variable derived from one of those,
   with the same fallback-default style the frozen file uses — never a bare px
   that stops scaling.

   NO VIEWPORT MEDIA QUERY sizes anything fluid here (standing project rule).
   The places grid picks its own column count from the panel's own width and
   the row's own type size; the short-screen contingency rides on felt.js's
   own `body.ot-tight` class, not on a height breakpoint of my invention.
   ========================================================================== */


/* ==========================================================================
   1 · THE TWO EXITS — «مغادرة الغرفة» red, «العودة إلى الغرفة» left alone
   ==========================================================================

   THE RED IS NOT INVENTED. `--red:#ff6b6b` is declared in the project's own
   token sheet, `site/css/tokens.css:11` (`:root`), which BOTH Overtake pages
   load (overtake.html:11, overtake-online.html:13). The same value is
   DESIGN.md's danger token (`--red #FF6B6B`) and the same value the owner-
   approved Podium mockup uses for its leave-room action
   (results-overtake-online-en.html `:root{--red:#FF6B6B}`). Three independent
   sources, one hex. It is consumed as `var(--red,#ff6b6b)` with the literal
   fallback because these pages deliberately do not assume tokens.css arrived
   (see the inline `:root` block at overtake-online.html:63-69).

   ⚠️ ONE LINE TO CHANGE, AND IT IS THE NEXT SELECTOR.
   `.ofin-exit .backbtn` is a DIFFERENT action on the two pages: online
   (overtake-online.html:4017) it is «مغادرة الغرفة» → leave(), a real
   destructive leave; solo (overtake.html:2624) it is «‹ رجوع» → back to the
   mode menu. It is the exit action on both and the parity rule wants one
   screen, so the red ships on both by default. To make it ONLINE-ONLY, add
   `.ofin-online` to the selector on the very next line — `body.ot-fin` →
   `body.ot-fin.ofin-online` — and nothing else changes: every consuming rule
   below reads these variables through a `var(…, <the old cyan>)` fallback, so
   solo silently returns to exactly the colours felt-final.css:207-212 gave it.
   Solo's appearance therefore never depends on a class that may not exist. */
body.ot-fin{
  --finExitC:var(--red,#ff6b6b);              /* the label + the arrow */
  --finExitCH:#ff9a9a;                        /* its hover, the same red lifted */
  --finExitBd:rgba(255,107,107,.5);           /* = --red at 50% */
  --finExitBg:rgba(30,11,13,.62);             /* dark fill, faintly red-washed */
  --finExitGl:rgba(255,107,107,.24);          /* = --red at 24%, the glow */
}

/* ONE ROW, AND IT IS ONE BOX — not two boxes that agree about a number.
   ⚠️ READ THIS BEFORE MOVING EITHER PILL AGAIN.

   `.ofin-exit` is built by felt.js:1898 as a child of the STAGE and pinned to
   the viewport's corner, `left:22px;bottom:18px` (felt-final.css:207), while
   `.ofin-go` is the last flow child of `.ofin-panel`. This file's first attempt
   answered that with arithmetic: copy where the panel's own bottom edge lands
   (`--finPad + border + --finPadB`) onto the exit's `bottom`, and the two pills
   come out LEVEL. They did — 0.00px apart at every viewport — and it was still
   not one row, because level is not together: measured on both pages in both
   languages, the leave pill sat **407px outside the panel at 1920, 311.5 at
   1600, 262.5 at 1366 and 198.5 at 844x390**, alone on the bare nebula.

   So finale.js MOVES the node into `.ofin-go` — the panel's own action row —
   and the row becomes a three-track GRID: [gutter][the primary][gutter].
     · the primary keeps the middle track, so it is exactly centred at every
       width: the two `1fr` gutters are equal BY CONSTRUCTION, not by a number
       that happens to work at one size;
     · the leave pill takes track 1 with `justify-self:start`, so it hugs the
       reading-start edge of the panel's own content box — and grid resolves
       track 1 to the RIGHT in Arabic on its own, so there is no mirrored copy
       of this rule and no physical `left` anywhere in it;
     · the row stretches both, so their top and bottom edges are literally the
       same two edges. Neither is positioned any more, so the delta cannot
       drift with R, with the language, or with the label's length;
     · `minmax(0,1fr)`, NOT `1fr`: a `1fr` track's floor is its content's
       min-content width, so a long leave label would widen the row and shove
       the primary off centre. With a 0 floor the pill is capped by its own
       `max-width:100%` and gives up its own width first, and the primary's box
       is byte-for-byte the one felt-final.css already gave it.

   ⚠️ THE MOVE SURVIVES TEARDOWN, and that is checked against the code, not
   assumed. finTeardown (felt.js:1808) hands the game's own Back button home
   with `document.querySelector('.ofin-exit .backbtn')` — a DESCENDANT selector
   run over the whole document, not a child selector rooted at the stage — so
   the button is still found from inside the panel, still appended to `.center`,
   and paintFinal still re-adopts it from there (felt.js:1923). A resize runs
   that pair on every repaint (felt.js:2237), which is the path that proves it:
   the pill is present in all 16 combos after three live viewport changes.

   `--finPadB2` STAYS. It is no longer the exit's baseline — nothing outside the
   row is — but `.ofin-panel`'s own padding-bottom still reads it, and the
   ot-tight block below still trims it. */
body.ot-fin{ --finPadB2:var(--finPadB,24px) }
body.ot-fin .ofin-panel{ padding-bottom:var(--finPadB2) }
body.ot-fin{
  --finExitPadX:calc(var(--exitF,14px) * 1.16);
  --finExitGap:calc(var(--exitF,14px) * .9);   /* between the two pills */
  --finExitIco:calc(var(--exitF,14px) * 1.2);  /* the door glyph, scaled like its label */
  --finExitRad:10px;                           /* = the primary .btn's own radius (felt-final.css:200) */
  --finGoPad:2.5px;                            /* = .ofin-go > div's own padding (felt-final.css:199) */
  /* THE GAP OVER THIS ROW, NAMED — because two things now need the SAME number and
     a copy of it in either place is a bug waiting for a short screen. felt-final
     .css:197 sets this margin from `--goMT`; the short-screen block near the end of
     this file trims it to 60%. The tap-area rule above is top-anchored on the notes
     row's own bottom edge, which is exactly this gap up from here — so it reads the
     variable, and the rule below guarantees the variable IS the rendered gap rather
     than merely equal to it by coincidence. Measured: 13/12/10/3px, matching the
     rendered notes-to-row distance to 0.00px at all four viewports. */
  --finGoGap:var(--goMT,12px);
}
body.ot-fin .ofin-go{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  align-items:stretch;
  column-gap:var(--finExitGap);
  margin-top:var(--finGoGap);
}
/* `:not(.ofin-exit)` because the exit is a `div` too, and the steel plate is the
   only child felt.js puts here — the exit is a guest that arrives later */
body.ot-fin .ofin-go > div:not(.ofin-exit){ grid-column:2;grid-row:1 }
body.ot-fin .ofin-go > .ofin-exit{
  grid-column:1;grid-row:1;justify-self:start;
  /* out of felt-final.css:207's absolute pin on the stage and into the row's own
     flow. `relative`, not `static`, so its z-index still means something. */
  position:relative;left:auto;right:auto;top:auto;bottom:auto;
  /* ⚠️ THE ROW ALREADY HAD A RULE FOR "A DIV IN HERE", AND THE EXIT IS A DIV.
     felt-final.css:198 is a bare `.ofin-go > div` — written when the primary's
     steel plate was the only child this row would ever have — and it hands out
     `width:var(--goW)`, `height:var(--goH)`, `padding:2.5px`, a 13px radius and
     the silver gradient. The moment finale.js moved the exit in here it started
     collecting all five: measured, the leave pill came out 430px wide (capped to
     its track at 255.5), 58px tall, and it pushed the row from 58px to 63px,
     because a stretched item's own min-height contribution now included the
     plate's height. `justify-self:start` could not shrink it — an item with a
     DEFINITE width does not fit its content. So the two sizes are handed back to
     `auto` here, and the paint block below re-states the padding, the radius and
     the background for the same reason. That file is frozen this round; this is
     the whole of the answer to it. */
  width:auto;height:auto;
  /* the plate's own padding, so this pill's box is the CYAN pill's box exactly —
     the two visible pills share a top edge and a bottom edge, not just a row */
  margin-block:var(--finGoPad);
  min-height:0;min-width:0;max-width:100%;
  /* ⚠️ `visible`, NOT `hidden`, AND THAT IS THE TAP TARGET'S DOING. This pill is
     the row's `position:relative` box, so it is the containing block for the
     invisible hit area declared at the end of this section — and a clip here would
     eat exactly the part of that area that lies outside the visible pill, which is
     the part that makes it 44px. What the clip was insuring is the label's
     ellipsis, and the label carries its own (`overflow:hidden;text-overflow:
     ellipsis`, below); it is also the only child that could ever be too wide, and
     measured it never is — the widest label on either page is 4.7px inside the
     narrowest gutter. The other child, the door glyph, is `flex:none` at a size
     this file sets. So nothing here can overflow, and the clip only cost. */
  overflow:visible;
}
/* the PAINT is deliberately not scoped to the row: if finale.js never ran, this
   still dresses the exit wherever felt.js left it, in the colour the owner asked
   for, instead of a half-styled pill */
body.ot-fin .ofin-exit{
  padding:0 var(--finExitPadX);
  gap:calc(var(--exitF,14px) * .55);
  border-radius:var(--finExitRad);
  border-color:var(--finExitBd,rgba(150,220,230,.25));
  background:var(--finExitBg,rgba(10,20,26,.65));
  box-shadow:0 0 0 1px var(--finExitGl,rgba(0,0,0,0)),
             0 8px 22px rgba(0,0,0,.45),
             0 0 22px var(--finExitGl,rgba(0,0,0,0));
}

/* the label may lose its tail before the row loses its shape: nowrap is already
   felt-final.css:211's, and `block` + the pill's own `overflow:hidden` turn the
   one gutter that could ever be too narrow into an ellipsis instead of a pill
   sitting on top of the primary. Measured: it never triggers — the widest label
   on either page is 4.7px inside the narrowest gutter. */
body.ot-fin .ofin-exit .backbtn{
  color:var(--finExitC,#bfe6ec);
  display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;
}
body.ot-fin .ofin-exit .backbtn:hover{ color:var(--finExitCH,#eafcff) }
/* The arrow carries an inline `stroke="#bfe6ec"` PRESENTATION ATTRIBUTE from
   felt.js:1793. A CSS `stroke` property beats a presentation attribute at any
   specificity, so the markup never has to change. Its `width`/`height` are
   attributes too, a flat 18px, so they are answered the same way and the glyph
   now scales with its own label instead of staying 18px on a phone. */
body.ot-fin .ofin-exit svg{
  flex:none;width:var(--finExitIco);height:var(--finExitIco);
  stroke:var(--finExitC,#bfe6ec);
}
/* the arrow points back the way the reader came, on both scripts.
   ⚠️ `body.ar`, NOT `[dir=rtl]` — and this is measured, not guessed. BOTH pages
   ship `<body dir="ltr" lang="en">` (overtake.html:1042, overtake-online.html
   :943) and applyLang() only ever toggles `body.ar` and `documentElement.lang`
   (:1064 / :1311). The `dir` ATTRIBUTE is never written, so an Arabic screen
   reads html dir="ltr" with a computed body `direction:rtl` (tokens.css's
   `body.ar`) — verified in all 8 Arabic combos. Every LOGICAL property in this
   file still mirrors, because those read the computed direction; a `[dir=rtl]`
   SELECTOR matches nothing on these two pages and this transform never once
   ran. The attribute form is kept beside the class in case a page starts
   writing it. */
body.ar.ot-fin .ofin-exit svg,
[dir=rtl] .ofin .ofin-exit svg{ transform:scaleX(-1) }

/* ---- AND IT IS A REAL TAP TARGET ----------------------------------------------
   ⚠️ MEASURED FIRST, AND IT IS WORSE THAN THE PILL LOOKS: the only clickable node
   in here is the LABEL. `.ofin-exit` is a plain `div` felt.js builds (felt.js:1898)
   with no handler of its own — `document.elementFromPoint()` over its padding
   returns `DIV.ofin-exit`, which does nothing — while the game's own
   `<button class="backbtn" onclick="otQuitToModes(false)">` inside it is the whole
   of the hit area. That label measures **39x15px at 1920 and 26x10px at 844x390**.
   The one destructive action on this screen was a 26x10 target on a landscape
   phone, which is a shipped size.

   44px is the floor every platform's guidance sets, and unlike every other size in
   this file it is a PHYSICAL measurement — a fingertip, not a proportion — so it is
   deliberately a literal px that does NOT scale with R. Scaling it would be scaling
   away the only thing it is for.

   IT IS AN INVISIBLE EXPANDED AREA, NOT A BIGGER PILL. A criterion says the primary
   must stay visibly the wider of the two, and the action row is 34px tall at
   844x390 inside a budget felt.js's give-back has already spent — so the visible
   pill keeps its box to the pixel (42.31x29 at 844, 102.48x53 at 1920), the row
   keeps its height, and a pseudo-element carries the pointer.

   THE CONTAINING BLOCK IS THE PILL, NOT THE LABEL, and that is the whole trick.
   `.backbtn` is left unpositioned — `all:unset` (felt-final.css:210) makes it
   `static`, and nothing here changes that — while `.ofin-go > .ofin-exit` above is
   `position:relative`, so these insets resolve against the PILL's padding box. One
   rule therefore says "cover the pill, then grow to 44" without ever naming the
   label's own size, at any R, in either script. The two axes work differently, and
   deliberately so:
     · INLINE, both insets are set and `min-width` OVER-CONSTRAINS them. That is
       defined behaviour, not a happy accident — CSS 2.1 §10.3.7 re-runs the
       resolution with the clamped width and drops the END inset — so a pill
       narrower than 44px grows toward the row's CENTRE, into 46.19px of empty
       gutter track at 844x390 and 153px at 1920. It cannot reach the primary.
       `-1px` is the pill's own border width (felt-final.css:208), so the area is at
       minimum the pill's whole border box: the padding a player already aims at
       becomes clickable, which is most of the fix at every size above a phone.
     · BLOCK, the box is TOP-ANCHORED and given an explicit `height` instead, so
       nothing is over-constrained and the growth direction is not left to a
       fallback rule. The anchor is the notes row's own bottom edge (`--finGoGap`
       above this row), the natural height carries it to the action row's bottom
       edge, and `min-height`/`max-height` are the two bounds below.
   ⚠️ AND IT IS BOUNDED AT BOTH ENDS, BECAUSE BOTH BOUNDS WERE MEASURED BITING.
   844x390 is not merely the smallest pill, it is the tightest box on the screen, and
   a first cut that simply said "min-height:44px and grow" failed twice there:
     · GROWING DOWN, it passed the card's own padding box (whose floor is 387) and
       reached the stage's clip at 390. `.ofin-panel` came back scrollHeight 387
       against clientHeight 384 — a 3px overflow with no ink in it, but a criterion
       says that pair must be equal in all 16 combos and a critic re-running the rig
       would rule it FAIL. `max-height` therefore stops the area on the card's floor:
       the padding the card already reserves is fair game, one pixel past it is not.
     · GROWING UP from the row's own top edge it could only reach 42px, because
       346 -> 387 is all there is. The last two come from the gap between
       `.ofin-notes` and this row — so the area is TOP-ANCHORED on the notes row's
       own bottom edge, which is `--finGoGap` above the row and is declared as a
       variable for exactly this reason: felt-final.css:197 and the short-screen
       block both set that margin, and the hit area has to read the same number they
       write or it lands a pixel inside `.det`. At 844x390 the space from the notes'
       last pixel to the card's floor is 44px to the pixel, which is what the area
       takes, and `.det` still answers at its own centre in all 16 combos.
   MEASURED AFTER, by walking `elementFromPoint` out from the label's centre until it
   stops answering `.backbtn` rather than by trusting the arithmetic: **45x44 at
   844x390, up from 26x10**, and 103x72 at 1920, up from 39x15 — solo and online,
   both languages, host and guest, ten seats and twelve. The visible pill is
   unchanged at 42.31x29 and 102.48x53, the row is unchanged at 34px and 58px, the
   primary is still 4.3x and 4.2x the leave pill's width, `.det` still answers at its
   own centre in all 16 combos, and every scrollHeight still equals its clientHeight. */
body.ot-fin{ --finTap:44px }
body.ot-fin .ofin-exit .backbtn::after{
  content:'';position:absolute;
  /* the inline axis: out to the pill's border box, then to 44 if it is narrower.
     The growth goes toward the row's centre, into 46.19px of empty gutter track at
     844x390 and 153px at 1920 — it cannot reach the primary, and the VISIBLE pill
     does not move, so the primary stays the wider of the two. */
  inset-inline:-1px;
  min-width:var(--finTap);
  /* the block axis: `100%` is the pill's own padding box, so this reads "the pill,
     grown by its margin and its border to the action row's box, and up by the gap
     that row leaves under the notes" — then clamped to 44 at the bottom and to the
     card's own padding floor at the top. */
  inset-block-start:calc((var(--finGoPad,2.5px) + 1px + var(--finGoGap,12px)) * -1);
  inset-block-end:auto;
  height:calc(100% + (var(--finGoPad,2.5px) + 1px) * 2 + var(--finGoGap,12px));
  min-height:var(--finTap);
  max-height:calc(100% + (var(--finGoPad,2.5px) + 1px) * 2
                       + var(--finGoGap,12px) + var(--finPadB2,24px));
}
/* ⚠️ ONE WINDOW WHERE THE 44 IS NOT YET 44, MEASURED AND DELIBERATELY LEFT ALONE.
   `.ofin-exit` is born a child of the STAGE, pinned at `left:22px;bottom:18px`
   (felt-final.css:207), and finale.js adopts it into this row on its next pass. On a
   DIRECT first load — no resize — that adoption was measured landing 1-2s late at
   844x390, and until it does the pill is 12px tall instead of 29, sitting 18px off
   the stage's floor. The area is still 44px tall there, but `.ofin` is
   `overflow:hidden` and clips it at the stage's edge, so 8px of it is not
   hit-testable and the probe reads 45x36. It is NOT the cap above doing that —
   wrapping the cap in `max(--finTap, …)` was tried and moved the number not at all,
   which is why it is not here. Fixing it would mean moving the area UP off the pill
   during a state in which the two pills are not one row yet anyway, and the window
   closes on its own. Recorded rather than papered over: 45x36 for a moment, 45x44
   once the row settles, against 26x10 before — and the late adoption itself is
   felt.js/finale.js timing that predates this rule (checked before any of it). */

/* THE PRIMARY IS NOT TOUCHED. `body.ot-fin .ofin-go .btn` keeps its
   `#45eded` border, its dark-teal fill, its silver gradient wrapper and its
   `gr-btn` pulse (felt-final.css:198-204 — note that keyframe lives in
   felt-results.css:356, not locally, so it is left entirely alone). Not one
   declaration below re-states any of it.

   The ONE thing added for the primary is the online GUEST case the contract
   describes: `.ofin-go.empty` is set when no `.btn` was moved in, and the
   module puts the orphaned `.waitline` there instead. Without this the guest
   stares at an empty steel pill. */
/* ⚠️ `:not(.ofin-exit)` FOR THE SAME REASON felt-final.css:198 NEEDED IT, and this
   one was measured biting: the leave pill is a `div` in this row too, and with
   three classes this selector out-specifies the pill's own paint block — so on the
   ONLINE GUEST's screen, and only there, the red leave pill came out as a grey
   DASHED transparent chip with no padding (computed `dashed rgba(150,220,230,.3)`,
   `padding:0`, 123.2px wide against the host's 153.9). The one screen where the
   exit is the player's only control was the one screen where it stopped looking
   like a control. A bare `> div` in this row is now always a bug. */
body.ot-fin .ofin-go.empty > div:not(.ofin-exit){
  background:none;padding:0;
  border:1px dashed rgba(150,220,230,.3);border-radius:12px;
  display:flex;align-items:center;justify-content:center;
}
body.ot-fin .ofin-go.empty .waitline{
  margin:0;max-width:100%;text-align:center;
  font:700 calc(var(--goF,21px) * .78)/1.3 Cairo,sans-serif;
  color:#bfe6ec;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}


/* ==========================================================================
   2 · THE TITLE — the biggest thing on the screen
   ==========================================================================
   finale.js inserts `.ofin-title` as `.ofin-panel`'s first child, before
   `.ofin-head`. It scales off `--finHeadF` — the same R-driven variable the
   placing line uses — so one multiplier keeps the reference's ratio at every
   size, and the "at least 2×" promise cannot drift.

   ⚠️ IT COSTS THE PANEL ZERO HEIGHT ON A SHORT SCREEN. felt.js budgets the
   panel from the five children it knows about; a sixth it has never heard of
   is a height it cannot give back, and its give-back floors are already
   collectively over budget at low R — every px this title took would come
   straight out of the podium, which section 3's fit factor then pays for by
   shrinking the champion's own score. So under `body.ot-tight`
   — felt.js:1869, set whenever the stage is under 520px tall — the title is
   removed from the flow outright and the placing line carries the screen
   alone. That is deliberate: a title is ornament, a rank is not. */
body.ot-fin{ --finTitleF:calc(var(--finHeadF,26px) * var(--finTitleK,2.2)) }
body.ot-fin .ofin-title{
  flex:none;text-align:center;text-transform:uppercase;   /* a no-op in Arabic, which has no case */
  margin-top:0; margin-inline:0;
  /* ⚠️ THE LEADING IS NOT COSMETIC HERE — IT IS WHAT PAINTS THE ARABIC.
     This was `/1.02`, and with `background-clip:text` the glyphs are painted by
     the BACKGROUND, so any ink falling outside the background box gets no paint
     at all — and `color:transparent` means there is no fallback colour to catch
     it either. A 1.02 line box on a 57px font left the paint box ending on the
     same pixel row as Cairo's descenders, so «الترتيب النهائي» lost the bowl of
     its final ي AND both of its dots, and the medial ي's dots in «الترتيب» were
     sheared into flat blocks. Measured: at 1920 row y193 was 48px of solid ink
     and y194 was empty — a hard horizontal cut, not a taper. It read as a
     MISSPELLING of the biggest word on the screen, in the default language, at
     1920, 1600 and 1366. English never showed it because it has no descenders.
     1.02 was chosen to save vertical space in a tight panel; the space is bought
     back below instead, where it costs nothing that a reader can see.

     ⚠️⚠️ AND THIS COMMENT USED TO BE TERMINATED RIGHT HERE, WHICH IS WHY NONE OF
     IT HAS EVER BEEN LIVE. (The terminator is not written out in this paragraph
     for the obvious reason — writing one inside a comment ends the comment, and
     doing exactly that is the bug being described.) Everything from the next line
     down to the real terminator was then RAW TEXT inside a declaration block: the
     parser reads it
     as one malformed declaration and discards tokens up to the next `;`, and the
     next `;` is the end of `font:900 var(--finTitleF)/1.34 Cairo,sans-serif`. So
     the title's font was thrown away on the same commit that wrote it (fad12c9),
     and the title has been rendering at the inherited **16px** — 20px tall in
     English, 30px in Arabic — instead of 61.6px at 1920, ever since. Read back
     off the CSSOM on the live page: the surviving property list for this rule had
     no font-size, no font-weight, no font-family and no line-height.
     The reported symptom «the headline gaps do not scale» was this: the negative
     margin scaled with `--finHeadF` while the line box it was compensating for
     was a fixed 16px at every viewport. Two characters.

     ⚠️ AND THE LEADING MUST NOT BE PAID FOR IN LIST HEIGHT. Simply going to 1.34
     made `.ofin-rest` SCROLL at 1600, 1366 and 844 on both pages in both
     languages — measured, 12 of 16 combos — because the line box grew and the
     panel's budget was already spent. ⚠️ THAT MEASUREMENT NO LONGER REPRODUCES:
     with the font actually reaching the browser and no compensating margin at
     all, `.ofin-rest` comes back 166/166, 155/155 and 130/130 at 1920/1600/1366
     and the panel keeps 146/78/66px of slack. It either predates the `--finPodK`
     fit work in section 3 or was taken on the online page. The negative margin is
     still worth having — it is what sets the optical gap — but not for the reason
     written above. The fix is that the extra leading is needed
     for PAINT, not for SPACE: the line box has to contain the descender, but the
     room under the descender is empty, so a negative bottom margin hands it back
     without shrinking the box that does the painting. The margin is the original
     `--finHeadF * .1` minus the leading added, so the placing line lands where it
     always did and the list keeps every pixel it had. */
  font:900 var(--finTitleF)/1.34 Cairo,sans-serif;
  /* ⚠️ AND 1.34 IS STILL NOT ENOUGH — THE PAINT BOX HOLDS THE DESCENDER, NOT THE
     LINE BOX. Now that the font is actually applied it can be measured properly:
     Cairo 900 puts the final ي of «الترتيب النهائي» 0.4708em below the baseline
     while a 1.34 line box leaves only 0.3047em under it, so 0.166em of the bowl
     AND BOTH ITS DOTS fall outside the background box — and with
     `background-clip:text` that ink gets no paint, and `color:transparent` gives
     it no fallback. Clearance measured -9.47 / -9.36 / -7.14px at 1920 / 1600 /
     1366, and photographed reading «النهائى» exactly as the owner first saw it.
     Raising the leading again works and costs EXACTLY TWICE as much height —
     leading splits half above the baseline and half below, and only the bottom
     half does any work here. `padding-bottom` grows the background-origin box
     alone: the line box, the baseline and the ink do not move, the glyph simply
     lands inside the painted area. .22em against a .166em shortfall leaves
     4.08 / 3.22 / 3.50px of clearance, agreed to 0.5px by canvas ink metrics and
     by a 4x pixel scan of the rendered glyph.
     Accepted side-effect: the gradient's 52% stop stretches with the box, so the
     glyph bottoms sit ~16% higher on the ramp. */
  padding-bottom:calc(var(--finTitleF) * .22);
  /* THE GAP IS ONE FRACTION OF THE TITLE, NOT A DIFFERENCE OF TWO THINGS. The old
     `--finHeadF * .1 - --finTitleF * .155` was already exactly
     `--finTitleF * -.1095` (since --finTitleF IS --finHeadF * 2.2 and --finTitleK
     is set nowhere in the repo), so it did scale — what never scaled was the
     title. Written as one fraction it stays correct even if --finTitleK is ever
     given a value.
     ⚠️ THE TWO SCRIPTS NEED DIFFERENT NUMBERS AND THERE IS NO WAY AROUND IT: the
     Arabic ink descends .4708em past the baseline and the Latin caps .016em, so
     one margin cannot give both the same optical gap — they are .467 x
     --finTitleF apart, 29px at 1920. `body.ar`, NOT `[dir=rtl]`, for the reason
     in section 1. Measured gap after: 7.97 / 6.27 / 5.84px English and
     7.73 / 6.97 / 5.44px Arabic at 1920 / 1600 / 1366 — 0.11-0.13 of the title
     size against a .12 target, the residue being Math.round() on --finHeadF. */
  margin-bottom:calc(var(--finTitleF) * -.4888);
  letter-spacing:.035em;
  /* cyan, glowing, gradient-clipped. The glow is a `filter`, NOT a
     `text-shadow`: `-webkit-text-fill-color:transparent` would eat a text
     shadow, and drop-shadow follows the clipped glyphs instead. */
  background:linear-gradient(180deg,#eafcff,#45eded 52%,#17b8cf);
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  filter:drop-shadow(0 0 18px rgba(69,237,237,.5)) drop-shadow(0 2px 10px rgba(0,0,0,.55));
}
/* …and Arabic takes the other number. The ink descends .4708em past the baseline
   against Latin's .016em, so a single margin cannot hand both scripts the same
   optical gap — they are .467 x --finTitleF apart, 29px at 1920. `body.ar` and
   not `[dir=rtl]`, for the reason set out in section 1. */
body.ot-fin.ar .ofin-title{ margin-bottom:calc(var(--finTitleF) * -.0220) }

/* the placing line stays white and small — its only change is a tighter
   leading, which hands ~4px back to the list at every size */
body.ot-fin .ofin-head{ line-height:1.16 }
body.ot-fin.ot-tight .ofin-title{ display:none }


/* ==========================================================================
   3 · THE PODIUM — circles, and a champion you cannot mistake
   ========================================================================== */

/* THE PODIUM FITS THE BOX IT WAS GIVEN. ⚠️ READ THIS BEFORE CHANGING A SIZE.
   `.ofin-pod` is `height:var(--podH)` with no `overflow`, and `.ofin-step` is
   `position:absolute;bottom:6px` (felt-final.css:67-68). felt.js's height
   give-back (felt.js:2055-2085) takes pixels off --podH to buy the list its
   rows — but nothing takes them off what is INSIDE the podium, so the stack
   keeps its intrinsic height and grows UPWARD, out of the top of the box, over
   the placing line. Measured before this block existed: the champion's step
   escaped the pod's top edge by 8.5px at 1920x1080 and by 40.69px at 844x390,
   where its score and its gold chip landed unreadably on top of «انتهيت في
   المركز 6». `overflow:hidden` is NOT the answer — it would slice the
   champion's score off. What is inside has to get smaller instead.

   So it is measured in CSS, from felt.js's OWN numbers, and scaled to fit:

     --finPodNeed  what the champion's stack adds up to — the exact sum of the
                   six boxes felt-final.css stacks in `.ofin-step.s1`, in the
                   variables felt.js publishes. Verified against the real pages
                   at every viewport: 332.5px at 1920, 266.6 at 1366, 126.7 at
                   844x390 — the rendered heights to the pixel.
     --finPodRoom  what `.ofin-step` actually has: the box felt.js sized, less
                   the 6px it is lifted off the floor by.
     --finPodK     room ÷ need, capped at 1. Every height and every type size
                   inside the podium is then multiplied by it, so the stack
                   lands EXACTLY on the box's own top edge (a +6px gap to the
                   placing line, which is `.ofin-pod`'s own margin-top) and the
                   three steps keep the proportions the design drew — the
                   champion stays the tallest of the three at every size.

   IT IS DERIVED, NOT A LADDER: change any clamp in paintFinal and both sides
   of the ratio move with it. K is 1 whenever the podium already fits, so a
   tall screen is bit-for-bit what it was (measured: 0.974 at 1920, 0.991 at
   1366 — the 2.6% and 0.9% those screens were over budget by, and nothing
   more).

   THE DIVISION IS `tan(atan2(a,b))` — the standard way to divide two lengths
   in CSS, and the widely-supported one (trig functions: Safari 15.4, Firefox
   108, Chrome 111; bare `<length>/<length>` in calc() is years younger). A
   browser without it keeps K=1 and therefore exactly today's rendering, which
   is why the fallback is declared FIRST and the real value only inside
   @supports: a custom property whose value fails at substitution time poisons
   every property that reads it, and `height:invalid` on a pedestal is a far
   worse screen than a 2px overlap. */
body.ot-fin{
  --finPodLift:6px;                          /* = .ofin-step's own `bottom` (felt-final.css:68) */
  --finPodTopUp:8px;                         /* = .ofin-top's own negative margin (:94) */
  --finPodSubM:6px;                          /* = .ofin-sub's `margin:2px 0 4px` (:82) — 0 under ot-tight */
  --finPodBaseH:var(--bsH,15px);             /* the base plate — 0 under ot-tight, where it is display:none */
  /* the six boxes, in the order .ofin-step.s1 stacks them, and NOT ONE COMMENT
     INSIDE THE VALUE: a custom property's value is a raw token stream that gets
     re-parsed wherever it is substituted, and Chrome hands the original text
     back verbatim (comments and all) when you read it. It happens to work here;
     relying on every engine to drop a comment mid-`calc()` on the owner's phone
     is a bet with nothing to win. So the legend lives out here:
       1 .ofin-sc    the taller of the score and the medal beside it
       2 .ofin-sub   its line box (leading 1.3) plus its own margins
       3 .ofin-av    the portrait
       4 .ofin-top   the cap, less the overlap it is pulled up by
       5 .ofin-body  the pedestal
       6 .ofin-base  the plate under it */
  --finPodNeed:calc(max(var(--sc1F,66px),var(--medal1,52px))
                  + var(--subF,14px) * 1.3 + var(--finPodSubM)
                  + var(--avS,96px)
                  + var(--t1H,24px) - var(--finPodTopUp)
                  + var(--b1H,102px)
                  + var(--finPodBaseH));
  --finPodRoom:calc(var(--podH,322px) - var(--finPodLift));
  --finPodK:1;
}
@supports (width:calc(1px * tan(atan2(2px,1px)))){
  body.ot-fin{ --finPodK:min(1,tan(atan2(var(--finPodRoom),var(--finPodNeed)))) }
}

/* …and every interior size reads through it. Nothing here changes a ratio the
   design chose — each declaration is felt.js's own variable times K, so at
   K=1 it re-states exactly what felt-final.css already said. Widths are
   deliberately NOT scaled: felt.js resolves the three steps' widths against
   each other (felt.js:1964-1968) and a podium that is narrower than it planned
   for is a podium with a hole in the middle. Only height comes out.
   Each selector carries `body.ot-fin` plus the same classes the frozen file
   uses, so it out-specifies the rule it answers without an `!important`. */
body.ot-fin .ofin-sc .v{ font-size:calc(var(--sc1F,66px) * var(--finPodK)) }
body.ot-fin .ofin-step.s2 .ofin-sc .v,
body.ot-fin .ofin-step.s3 .ofin-sc .v{ font-size:calc(var(--scF,54px) * var(--finPodK)) }
body.ot-fin .ofin-sc{ gap:calc(var(--scGap,12px) * var(--finPodK)) }
body.ot-fin .ofin-sc img{
  width:calc(var(--medal1,52px) * var(--finPodK));
  height:calc(var(--medal1,52px) * var(--finPodK));
}
body.ot-fin .ofin-step.s2 .ofin-sc img,
body.ot-fin .ofin-step.s3 .ofin-sc img{
  width:calc(var(--medal,38px) * var(--finPodK));
  height:calc(var(--medal,38px) * var(--finPodK));
}
/* THE MEDAL SLOT — and it is deliberately sized by its HEIGHT, not its width.
   felt.js used to drop a SQUARE <img> straight into `.ofin-sc`; since 2026-08-17
   it emits `.ofin-medal` wrapping the game's own struck rosette from
   `overtake/medals.js`, whose viewBox is 34:44 — taller than wide. The two `img`
   rules above are kept, unchanged, for `medalHtml()`'s fallback path: if
   `medals.js` fails to fetch, an <img> is what renders and it needs the square
   numbers back.
   Pinning the HEIGHT to the number the image had is what makes this a free
   change: row 1 of `--finPodNeed` (see §2 above) is `max(--sc1F,--medal1)`, so
   as long as the medal is no taller than the score beside it, `--finPodK` comes
   out at exactly the value it had before the medal changed and nothing else on
   the podium moves by a pixel. The width then falls out of the ratio — 40px
   against the image's 52 for the champion, 29 against 38 for a runner-up — so
   the new medal is strictly NARROWER too, and cannot crowd a step that fitted.
   The glow follows the metal rather than staying gold for all three: the frozen
   file's `.ofin-sc img` filter (felt-final.css:78-81) glowed gold under a SILVER
   star and a COPPER shield, which is part of why the old three read as one
   undifferentiated warm blob. */
body.ot-fin .ofin-sc .ofin-medal{
  display:block;flex:none;
  height:calc(var(--medal1,52px) * var(--finPodK));
  width:calc(var(--medal1,52px) * var(--finPodK) * 34 / 44);
}
body.ot-fin .ofin-step.s2 .ofin-sc .ofin-medal,
body.ot-fin .ofin-step.s3 .ofin-sc .ofin-medal{
  height:calc(var(--medal,38px) * var(--finPodK));
  width:calc(var(--medal,38px) * var(--finPodK) * 34 / 44);
}
body.ot-fin .ofin-medal svg{display:block;width:100%;height:100%}
body.ot-fin .ofin-medal[data-r="1"]{filter:drop-shadow(0 0 10px rgba(240,196,79,.6))}
body.ot-fin .ofin-medal[data-r="2"]{filter:drop-shadow(0 0 9px rgba(214,230,240,.5))}
body.ot-fin .ofin-medal[data-r="3"]{filter:drop-shadow(0 0 9px rgba(208,143,90,.5))}
/* the label's leading is 1.3 and its margins are the design's 2px/4px — both
   scale with the type so the line it occupies is exactly its share of `need` */
body.ot-fin .ofin-sub{
  font-size:calc(var(--subF,14px) * var(--finPodK));
  margin:calc(2px * var(--finPodK)) 0 calc(4px * var(--finPodK));
}
body.ot-fin .ofin-step.s3 .ofin-sub{ font-size:calc(var(--sub3F,13px) * var(--finPodK)) }
body.ot-fin .ofin-av{
  width:calc(var(--avS,96px) * var(--finPodK));
  height:calc(var(--avS,96px) * var(--finPodK));
}
body.ot-fin .ofin-step.s2 .ofin-av,
body.ot-fin .ofin-step.s3 .ofin-av{
  width:calc(var(--avS2,80px) * var(--finPodK));
  height:calc(var(--avS2,80px) * var(--finPodK));
}
/* the cap keeps its overlap PROPORTIONAL. A fixed -8px against a scaled height
   would go net-negative on a short screen and walk the pedestal up into the
   avatar's rim, which is where the rank badge sits. */
body.ot-fin .ofin-top{ margin-top:calc(var(--finPodTopUp) * var(--finPodK) * -1) }
body.ot-fin .ofin-step.s1 .ofin-top{ height:calc(var(--t1H,24px) * var(--finPodK)) }
body.ot-fin .ofin-step.s2 .ofin-top{ height:calc(var(--t2H,22px) * var(--finPodK)) }
body.ot-fin .ofin-step.s3 .ofin-top{ height:calc(var(--t3H,20px) * var(--finPodK)) }
body.ot-fin .ofin-body{ gap:calc(2px * var(--finPodK)) }
body.ot-fin .ofin-step.s1 .ofin-body{ height:calc(var(--b1H,102px) * var(--finPodK)) }
body.ot-fin .ofin-step.s2 .ofin-body{ height:calc(var(--b2H,78px) * var(--finPodK)) }
body.ot-fin .ofin-step.s3 .ofin-body{ height:calc(var(--b3H,62px) * var(--finPodK)) }
/* the pedestal's name and its rank digit shrink WITH their plate, so the ~4px
   the plate has always clipped off them stays exactly ~4px × K and no digit
   starts disappearing that was not disappearing before */
body.ot-fin .ofin-body .nm{ font-size:calc(var(--podNmF,19px) * var(--finPodK) * var(--podNmFit,1)) }
body.ot-fin .ofin-step.s2 .nm{ font-size:calc(var(--podNm2F,17px) * var(--finPodK) * var(--podNmFit,1)) }
body.ot-fin .ofin-step.s3 .nm{ font-size:calc(var(--podNm3F,17px) * var(--finPodK) * var(--podNmFit,1)) }
body.ot-fin .ofin-body .no{ font-size:calc(var(--podNoF,44px) * var(--finPodK)) }
body.ot-fin .ofin-step.s2 .no{ font-size:calc(var(--podNo2F,38px) * var(--finPodK)) }
body.ot-fin .ofin-step.s3 .no{ font-size:calc(var(--podNo3F,34px) * var(--finPodK)) }
body.ot-fin .ofin-step.s1 .ofin-base,
body.ot-fin .ofin-step.s2 .ofin-base,
body.ot-fin .ofin-step.s3 .ofin-base{ height:calc(var(--bsH,15px) * var(--finPodK)) }

/* All three avatars are circles. felt-final.css:85 makes them 14px rounded
   squares and :89 makes the runners-up 12px, so both have to be answered —
   the second selector carries 4 classes to out-specify :89's 3. `overflow`
   is left as it is (see the badge note below) so the ring, the portrait and
   the `.mock` vignette all clip to the circle for free. */
body.ot-fin .ofin-av{ border-radius:50% }
body.ot-fin .ofin-step.s2 .ofin-av,
body.ot-fin .ofin-step.s3 .ofin-av{ border-radius:50% }
/* the ring colour and its glow are felt.js's `--rc`, untouched */

/* THE RANK BADGE.
   The contract puts `.ofin-rank` INSIDE `.ofin-av`, and `.ofin-av` is
   `overflow:hidden` (felt-final.css:85) — so a badge overhanging the rim
   would be sliced off, and lifting the clip would un-round the portrait. It
   therefore sits ON the avatar's lower edge from the inside: a circle
   centred on the rim, 3% clear of the bottom, where the parent circle is
   still ~2.3× wider than the badge, so nothing clips at any R (checked
   geometrically for both --avS and --avS2, and measured).
   `left:50%` + translateX is used rather than `inset-inline-start`: centring
   is direction-agnostic, and mixing a logical inset with a physical
   translate is what mirrors a centred element off-centre in RTL.
   THE METALS ARE THE PEDESTALS' OWN, so the screen keeps one palette —
   gold from felt-final.css:96 + :133-134, silver from :98 + :135-136,
   bronze from :100 + :137-138, and the dark-on-metal text from :127-131. */
body.ot-fin .ofin-step .ofin-rank{
  --finRankS:calc(var(--avS2,80px) * var(--finPodK) * .33);
  position:absolute;z-index:3;
  left:50%;transform:translateX(-50%);
  bottom:3%;
  width:var(--finRankS);height:var(--finRankS);
  border-radius:50%;box-sizing:border-box;
  display:flex;align-items:center;justify-content:center;
  font:900 calc(var(--finRankS) * .56)/1 Cairo,sans-serif;
  font-variant-numeric:tabular-nums;
  border:1px solid rgba(10,16,22,.4);
  box-shadow:0 2px 7px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.5);
}
body.ot-fin .ofin-rank[data-r="1"]{
  background:linear-gradient(180deg,#ffedb0,#dcae40 62%,#c2933a);color:#241a04;
}
body.ot-fin .ofin-rank[data-r="2"]{
  background:linear-gradient(180deg,#f7fafc,#b6c1ca 62%,#aab4bc);color:#10161c;
}
body.ot-fin .ofin-rank[data-r="3"]{
  background:linear-gradient(180deg,#f3c79b,#b07840 62%,#b0783f);color:#2a1607;
}

/* THE CHAMPION CARRIES FOUR SIGNALS NEITHER RUNNER-UP HAS, all added here:
     (1) a second, concentric GOLD ring outside the `--rc` ring
     (2) that gold ring BREATHES — a ring pulse, which s2/s3 do not have
         (their gr-aura2/3 is a drop-shadow on the whole step, not a ring)
     (3) a bigger badge — .36 of the champion avatar against .33 of a smaller
         one, so the numeral 1 is physically the largest of the three
     (4) the "Champion" sub-label becomes a gold CHIP; the runners-up keep
         plain text (and are pinned plain below, because s3's `.ofin-sub` is
         re-used for "Your best:" when you finish third)
   On top of six the frozen file already gave it: the largest avatar, the
   largest score in gold with a gold glow, the largest medal, the gold
   pedestal, the tallest pedestal, and six sparks against four and three. */
body.ot-fin .ofin-step.s1 .ofin-av{
  box-shadow:0 8px 18px rgba(0,0,0,.5),
             0 0 16px var(--rc,#45eded),
             0 0 0 var(--finGoldRing,3px) rgba(240,196,79,.9),
             0 0 30px rgba(240,196,79,.45);
  animation:gr-finCrownRing 2.8s ease-in-out infinite;
}
@keyframes gr-finCrownRing{
  0%,100%{ box-shadow:0 8px 18px rgba(0,0,0,.5),0 0 16px var(--rc,#45eded),
                      0 0 0 var(--finGoldRing,3px) rgba(240,196,79,.75),
                      0 0 22px rgba(240,196,79,.35) }
  50%    { box-shadow:0 8px 18px rgba(0,0,0,.5),0 0 16px var(--rc,#45eded),
                      0 0 0 calc(var(--finGoldRing,3px) + 1.5px) rgba(255,232,150,1),
                      0 0 40px rgba(240,196,79,.62) }
}
body.ot-fin .ofin-step.s1 .ofin-rank{ --finRankS:calc(var(--avS,96px) * var(--finPodK) * .36) }
/* MEASURED, AND IT COSTS ZERO HEIGHT. The chip's outline is an INSET SHADOW
   and its padding is horizontal only — a 1px border plus 2px of vertical
   padding would read as 6px more podium, and every px of intrinsic podium
   height is now a px the fit factor above has to take back out of the
   champion's own type (it was 14.5px of upward overflow at 1920×1080 with a
   bordered chip against 8.5 with this one). Horizontal padding scales with
   K too, so the chip stays the same shape around its label at every size. */
body.ot-fin .ofin-step.s1 .ofin-sub{
  padding:0 calc(var(--subF,14px) * var(--finPodK) * .78);
  border-radius:999px;
  background:linear-gradient(180deg,rgba(240,196,79,.2),rgba(120,86,20,.24));
  box-shadow:inset 0 0 0 1px rgba(240,196,79,.62),
             inset 0 1px 0 rgba(255,244,205,.3),
             0 0 14px rgba(240,196,79,.28);
  text-shadow:0 0 10px rgba(240,196,79,.45);
}
body.ot-fin .ofin-step.s2 .ofin-sub,
body.ot-fin .ofin-step.s3 .ofin-sub{
  padding:0;border:0;background:none;box-shadow:none;
}


/* ==========================================================================
   4 · THE FEATS — three cards become ONE continuous bar
   ==========================================================================
   felt-final.css:146-153 sets `--featGap:14px` between three separately
   bordered, separately shadowed cards. The gap goes to zero, the border and
   the radius move OUT to the container, and the cells lose every edge of
   their own. */
body.ot-fin .ofin-feats{
  position:relative;overflow:hidden;
  gap:0;
  border-radius:var(--finFeatRad,14px);
  border:1px solid rgba(69,237,237,.34);          /* the reference's cyan outline */
  background:linear-gradient(160deg,rgba(90,110,122,.26),rgba(16,26,34,.74));
  box-shadow:inset 0 1px 0 rgba(235,250,255,.16),
             0 10px 24px rgba(0,0,0,.42),
             0 0 16px rgba(69,237,237,.1);
}
body.ot-fin{ --finFeatRad:calc(var(--featPad,11px) * 1.28) }
body.ot-fin .ofin-feat{
  border:0;border-radius:0;background:none;box-shadow:none;
  /* the cells stay EXACTLY equal: flex-basis 0 with nothing of their own to
     add to the base size, so the free space divides three ways evenly */
  flex:1 1 0;min-width:0;
  gap:calc(var(--featPad,11px) * .38);
}
/* THE DIVIDERS ARE PSEUDO-ELEMENTS, NOT BORDERS. A `border-inline-start` on
   two of three cells would make those two exactly 1px wider than the first
   (box-sizing:border-box adds the border to the flex base size). A hairline
   drawn inside costs no width at all, and `inset-inline-start` puts it on the
   correct side of an RTL bar for free. */
body.ot-fin .ofin-feat + .ofin-feat::before{
  content:'';position:absolute;z-index:1;
  inset-inline-start:0;inset-block:18%;
  width:1px;
  background:linear-gradient(180deg,rgba(150,220,230,0),rgba(150,220,230,.34),rgba(150,220,230,0));
}
/* ONE bar means ONE sheen. Three cell-sheens sweeping out of step is what
   gave the strip away as three cards; the per-cell sheen stands down and the
   container runs a single pass. Absolute, so it is not a flex item. */
body.ot-fin .ofin-feat .shn{ display:none }
body.ot-fin .ofin-feats::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  background-size:240% 100%;background-repeat:no-repeat;
  background:linear-gradient(112deg,rgba(0,0,0,0) 44%,rgba(200,250,255,.13) 50%,rgba(0,0,0,0) 58%);
  animation:gr-sheenP 7s ease-in-out infinite;                /* felt-results.css's keyframe, already on both pages */
}
/* the rhythm the reference reads: grey label · bright value · cyan name, one
   size each across all three cells (they already share their classes, so the
   sizes cannot diverge — only the value's colour moves, gold → the
   reference's white, since gold now belongs to the champion alone) */
body.ot-fin .ofin-feat .l{ color:rgba(190,232,240,.72) }
body.ot-fin .ofin-feat .v{ color:#eafcff;text-shadow:0 0 12px rgba(69,237,237,.34) }
body.ot-fin .ofin-feat .b{ color:#45eded }


/* ==========================================================================
   5 · THE PLACES — three columns, a score bar, and a glyph that means "see"
   ==========================================================================
   felt-final.css:163 is `1fr 1fr` with a `@media(max-width:720px)` drop to one
   column at :257. Both are answered WITHOUT a viewport query, because the
   thing that decides how many names fit is the PANEL's width, not the
   window's: `--finW` is already the panel, and `--restNmF`/`--restAv`/
   `--restRkF` are already the row's own type. `auto-fill` + `minmax()` then
   lets the browser count the columns from the room it actually has.

   THE FLOOR IS THE ROW'S OWN CONTENT, measured off it rather than guessed:
   about nine name characters, the avatar, and the rank column. It resolves to
   205px at 1366, 236px at 1600, 252px at 1920 — under the 237/278/314px a
   third column gets at those sizes and over the 175/205/232px a fourth would
   need, so all three wide sizes land on exactly 3 tracks and none of them can
   slip to 4. On a 403px panel (844×390) three columns would be 120px each,
   under the 146px floor, so it steps back to 2 rather than crushing a name to
   an ellipsis. `auto-fill`, not `auto-fit`: a five-player match has two
   also-rans, and auto-fit would collapse the empty tracks and stretch those
   two rows across the whole panel.

   THIS ALSO PAYS FOR ITSELF IN HEIGHT: felt.js:2045 reads the COMPUTED
   `grid-template-columns` back to derive its row count, so 10 seats at 3
   columns is 3 grid rows where 2 columns was 4 — one whole row of shortfall
   removed from the give-back's bill, which is what buys the new title its
   room. */
body.ot-fin{
  --finRestColMin:calc(var(--restNmF,16px) * 9
                     + var(--restAv,28px) * 1.9
                     + var(--restRkF,15px) * 2.6);
}
body.ot-fin .ofin-rest{
  grid-template-columns:repeat(auto-fill,minmax(var(--finRestColMin),1fr));
}

/* the row's own padding, gap and rank column were hardcoded px in a file
   where everything else scales, so on a small screen they ate the name they
   were supposed to be framing. They now scale with the row itself. */
body.ot-fin{
  --finRestPadX:calc(var(--restH,46px) * .25);
  --finRestGap:calc(var(--restH,46px) * .18);
  --finRestRkW:calc(var(--restRkF,15px) * 1.4);
  --finSeeS:calc(var(--restAv,28px) * .64);
  --finBarH:max(2px,calc(var(--restH,46px) * .055));
}
body.ot-fin .ofin-row{
  position:relative;                                  /* the bar and the "you" spine hang off this */
  padding:0 var(--finRestPadX);
  gap:var(--finRestGap);
}
body.ot-fin .ofin-row .rk{ width:var(--finRestRkW) }
/* circular in the places too, as the reference draws them (felt-final.css:183
   makes them 7px rounded squares) */
body.ot-fin .ofin-row .av{ border-radius:50% }

/* THE SCORE BAR — under the NAME, from the reading-start edge, in both scripts.
   `.ofin-bar` is inserted after `.nm` — as a flex item it would sit BESIDE
   the name, so it is lifted out of the flow and pinned under the name's own
   column instead, its two ends computed from the same variables the row's
   flex items use (start = padding + rank + 2 gaps + avatar; end = padding +
   2 gaps + three tabular digits of score + the glyph). Out of flow means it
   adds nothing to the row's height, which matters on the short screens.

   ⚠️ BOTH ENDS ARE LOGICAL, AND THAT IS THE WHOLE POINT OF THIS RULE. A
   physical `left`/`right` pair reads the same in both languages, so in Arabic
   the bar started under the SCORE and the eye and grew backwards toward the
   name while the row itself mirrored around it. `inset-inline-*` resolves
   against the row's own computed `direction` — which is `rtl` on an Arabic
   screen even though no `dir` attribute is ever written (see the note in
   section 1) — so the bar starts where the reader starts, on its own.
   MEASURED IN BOTH DIRECTIONS, not reasoned about: English 1920, row 4,
   bar 558.67→704.98 with the name's own box starting at 558.64 (fill hugs the
   LEFT); Arabic 1920, same row, bar 1215.02→1361.33 against a name box ending
   at 1361.36 (fill hugs the RIGHT). Both start within 0.03px of the name's
   own start edge and grow away from it, toward the score, and in neither
   language does the bar reach the score's box.

   THE TRACK IS PAINTED WHETHER OR NOT THERE IS ANYTHING IN IT, so a score of 0
   reads as "none of the best" instead of as a missing element (measured: 36px
   of empty track behind a 75.4% fill, 74.33px behind a 49.2% one, and the full
   146.31px on a 0% row). No horizontal gradient anywhere on it: `to right`
   would fill from the wrong end in Arabic, so the fill is flat `--rc` with a
   vertical sheen and grows from the inline start on its own. */
body.ot-fin .ofin-bar{
  position:absolute;pointer-events:none;
  inset-inline-start:calc(var(--finRestPadX) + var(--finRestRkW) + var(--restAv,28px) + var(--finRestGap) * 2);
  inset-inline-end:calc(var(--finRestPadX) + var(--restPtF,17px) * 1.9 + var(--finSeeS) + var(--finRestGap) * 2);
  bottom:calc(var(--restH,46px) * .1);
  height:var(--finBarH);
  border-radius:999px;overflow:hidden;
  background:rgba(190,232,240,.15);
  box-shadow:inset 0 0 0 1px rgba(190,232,240,.1);
}
body.ot-fin .ofin-bar > i{
  display:block;height:100%;
  width:var(--pc,0%);
  min-width:0;
  border-radius:inherit;
  background:var(--rc,#45eded);
  background-image:linear-gradient(180deg,rgba(255,255,255,.34),rgba(0,0,0,.14));
  box-shadow:0 0 7px var(--rc,#45eded);
}

/* THE GLYPH.
   `svg.ofin-see` replaces felt.js's literal pencil and, per the contract,
   arrives with NO stroke attribute at all — without this rule it renders
   invisible. Brand cyan, the file's own #45eded. It is a direction-carrying
   mark ("open the words"), so it mirrors in Arabic like every other chevron
   on these pages. */
body.ot-fin .ofin-row .ofin-see{
  flex:none;
  width:var(--finSeeS);height:var(--finSeeS);
  stroke:#45eded;fill:none;opacity:.85;
}
body.ot-fin .ofin-row:hover .ofin-see{ opacity:1 }
/* `body.ar` for the same measured reason as the exit arrow in section 1 — the
   `dir` attribute is never written on these pages, so the `[dir=rtl]` form
   never matched. Kept beside it in case one ever starts writing it. (The eye's
   own path happens to be symmetric about its centre, so this transform is
   currently a no-op either way; it is here so the day the glyph gains a
   direction, the glyph does not have to remember to bring a rule with it.) */
body.ar.ot-fin .ofin-row .ofin-see,
[dir=rtl] .ofin .ofin-row .ofin-see{ transform:scaleX(-1) }


/* ==========================================================================
   6 · YOUR OWN ROW — findable without reading it
   ==========================================================================
   felt-final.css:178-179 separates it by a background tint and a 55%-alpha
   border, which is invisible in a grid of nine. The treatment is lifted from
   the approved mockup's `.callout` — the block that says "this one is you" —
   a full-strength cyan edge, a cyan glow, and cyan numbers (`--cyan #0AB6D4`
   border/glow, `--cyan2 #8DEBF7` numerals), plus a solid cyan spine on the
   reading edge. Four signals, and the spine is a pseudo-element so the row
   keeps exactly the width of its neighbours in the grid track. */
body.ot-fin .ofin-row.you{
  border-color:#45eded;
  background:linear-gradient(180deg,rgba(24,64,72,.92),rgba(12,32,39,.94));
  box-shadow:0 0 0 1px rgba(69,237,237,.4),
             0 0 20px rgba(69,237,237,.26),
             inset 0 1px 0 rgba(230,250,255,.14);
}
body.ot-fin .ofin-row.you::before{
  content:'';position:absolute;z-index:1;
  inset-inline-start:0;inset-block:20%;
  width:3px;border-radius:999px;
  background:#45eded;box-shadow:0 0 10px #45eded;
}
body.ot-fin .ofin-row.you .rk{ color:#bff3f7 }
body.ot-fin .ofin-row.you .nm{ color:#eafcff;text-shadow:0 0 10px rgba(69,237,237,.4) }
body.ot-fin .ofin-row.you .pt{ color:#8debf7;text-shadow:0 0 10px rgba(69,237,237,.45) }
body.ot-fin .ofin-row.you .ofin-see{ opacity:1 }

/* a couple of px of leading and padding handed back to the list at every
   size, which is where the title's height comes from */
body.ot-fin .ofin-notes{ padding-top:calc(var(--noteF,11.5px) * .55) }


/* ==========================================================================
   7 · THE CARD IS AS TALL AS WHAT IS IN IT — the 130px of nothing
   ==========================================================================

   THE DEFECT, MEASURED at 1920x1080 with ten seats, identical on both pages in
   both languages: `.ofin-rest` renders **296.33px** tall while its seven rows in
   three columns occupy **166px** — three row-lines at y622 / y681 / y740, the last
   ending at y788. From there to the primary's top edge at y958 lies a **169.66px**
   band of which a pixel scan finds **152-156 rows carrying no ink at all**, across
   the panel's whole width. It is worst at the size he is most likely to open:
   42.58px / 28-31 blank at 1600, 37.94 / 24-26 at 1366, 18.44 / 5-8 at 844x390.

   ⚠️ NOTHING IS "RESERVED FOR ROW-LINES THAT DO NOT EXIST", and getting that wrong
   would have sent this fix at the wrong element. felt.js does not size this box at
   all — grep it: the only things it writes for the list are the `--rest*` variables
   on `body`. What it computes (felt.js:2085-2088) is `colsN` off the COMPUTED
   `grid-template-columns` this file sets, then `rowsN = ceil(rows/colsN)` and
   `want = rowsN*restH + (rowsN-1)*gapY` — and at 1920 that is `3*48 + 2*11 = 166`,
   exactly the rendered content. The row-lines are correct and correctly sized. The
   296 is `flex:1` (felt-final.css:163) absorbing every pixel the six `flex:none`
   siblings leave, in a panel whose height is pinned by `top:var(--finPad)` /
   `bottom:var(--finPad)` (felt-final.css:53-54) and therefore has nothing to do
   with what is in it. `align-content:start` (:164) then packs the three lines to
   the top, so the slack shows up as one band under the last row.

   ⚠️ AND `flex:1` IS THE GIVE-BACK'S ONLY SENSOR. READ THIS BEFORE TOUCHING IT.
   felt.js:2110 is `short = want - rest.clientHeight`, and it only ever reports a
   shortfall because `flex:1` forces this box to report the panel's real remaining
   room. Take the grow away — `flex:none`, `flex:0 0 auto` — and `clientHeight`
   converges on `want` at EVERY viewport, `short` converges on 0, the give-back
   never fires, and 844x390 regresses to the state felt.js:2064-2078 describes:
   places four and below not on the screen at all and "Play again" pushed out under
   the panel's own edge. That is felt-results.css:163's lesson one level up — the
   same lesson felt-final.css:170-172 records for the ROWS, where twelve shrinkable
   rows in a capped list squeezed themselves to 4px each and the screen went blank
   rather than admitting it needed to scroll.

   SO ONLY THE BASIS MOVES: `0%` -> `auto`. Grow and shrink both stay 1.
     · `flex-shrink:1` plus felt-final.css:163's own `min-height:0` is the sensor.
       When the panel genuinely cannot hold the list, the box is still shrunk below
       its content, `clientHeight` is still less than `want`, `short` is still
       positive, and the give-back still spends --podH exactly as it does today.
       Verified: at 844x390 every number in this file's fit model is unchanged.
     · `flex-basis:auto` is what the panel below needs. With basis `0%` a flex item
       contributes NOTHING to an auto-height column's intrinsic size, so a card
       sized to its content would crush the list to zero and then report that
       everything fits — the exact failure the frozen file warns about. With basis
       `auto` the list's own content is what the card measures.
     · The rows are untouched: `height` + `min-height` of `--restH`
       (felt-final.css:173) is the load-bearing half of that lesson and no row can
       be crushed below it. `overflow-y:auto` (:165) is untouched, so a roster that
       genuinely overflows still SCROLLS and never clips — checked at twelve seats,
       above the shipped cap of ten.

   AND THE PANEL SPENDS THE SLACK BY NOT ASKING FOR IT.
   ⚠️ There was nothing to hand the 130px back TO. The give-back never fired at
   1920 (`short` was -130, so felt.js:2111 breaks on its first pass), so no giver is
   holding reduced pixels — `--podH` is already at its 330px ceiling. The slack is
   unclaimed, and content-sizing the list alone would only MOVE it below the action
   row, where it would read worse: a primary floating 130px above the card's floor.

   WHY THE CARD AND NOT THE PODIUM. The critic offered both. The podium is ruled out
   on two counts, neither of them taste:
     1. Every size felt.js publishes — `--podH` included — is an INLINE style on
        `body`, which outranks every rule in this file. Raising it would take an
        `!important`, and this file ships zero. Growing `.ofin-pod`'s box instead,
        without also growing `--finPodRoom`, would leave `--finPodK` at 0.974 and
        the stack at its old size inside a taller box — the champion's top would
        drop away from the placing line and the measured +6.0px gap would become
        +136px at 1920 while staying +6.0 at 844. That is not a bigger ceremony,
        it is the same ceremony with a hole above it.
     2. Scaling the interior UP instead (letting K exceed 1) would need K = 1.34 at
        1920, and section 3 deliberately does not scale WIDTHS — felt.js resolves
        the three steps' widths against each other (felt.js:1964-1968). A pedestal
        34% taller at the same width is not the shape the design drew.
   Whereas the owner's own reference settles the card: `reference/owner-screenshot
   .md` row 14 reads "the content floats on it with no panel frame around it". A
   card that stops where its content stops is a step TOWARD that, not away from it.

   So `bottom:auto` and the height goes `auto`: shrink-to-fit, capped at exactly the
   box it used to fill, re-centred on the axis it no longer spans. The band is gone
   rather than relocated, and NOTHING INSIDE THE CARD MOVES RELATIVE TO ANYTHING
   ELSE — same podium, same K, same +6.0px gap, same feats bar, same three columns,
   same two pills, same bars. Only the card's own top and bottom edges come in.

   MEASURED AFTER, and every pixel is accounted for at all four viewports:

     viewport   band          card           residual band
     1920x1080  169.66 -> 39.33  1006 -> 875.67  26.33 notes + 13 gap = 39.33
     1600x900    42.58 -> 34.44   832 -> 823.86  22.44 notes + 12 gap = 34.44
     1366x768    37.94 -> 30.50   708 -> 700.56  20.50 notes + 10 gap = 30.50
     844x390     18.44 -> 18.44   386 -> 386     15.39 notes +  3 gap = 18.39

   The band the card gives up and the height the card gives up are the SAME number
   to two decimals — 130.33 / 8.14 / 7.44 / 0 — which is the check that it went out
   of the card rather than moving inside it. What is left of the band is the notes
   row and the action row's own gap, exactly, with nothing unaccounted: the pixel
   scan drops from 152-156 ink-free rows to 21-25 at 1920, and those are the notes
   line's own leading. The list box is now its content to the pixel (166 / 155 / 130
   against a content of 166 / 155 / 130), and `.ofin-rest` still reports
   scrollHeight == clientHeight in all 16 combos.
   AND IT STILL SCROLLS WHEN IT MUST, which is the half that matters: forced at
   1000x560, 1024x540 and 1366x600 — all non-tight, all with the card at its cap —
   the list overflows by 6-9px, `overflow-y:auto` shows it, and the shortest row is
   28 / 27 / 30px against a `--restH` of 28 / 27 / 30. Not one row crushed, at ten
   seats and at twelve.

   ⚠️ NOT UNDER `ot-tight`, AND THAT IS EXPLICIT RATHER THAN LEFT TO SPECIFICITY.
   felt-final.css:255 deliberately makes the phone's card full-bleed (`top:0;
   bottom:0;border-radius:0`) and its `top`/`bottom` would win here on specificity —
   but `transform` would NOT, so the card would keep a `translateY(-50%)` with
   nothing to centre and ride half its own height off the top of the screen. The
   `:not(.ot-tight)` is load-bearing. It is also where the give-back actually lives,
   and the one viewport whose band (18.44px, 5-8 blank rows) is already the width of
   a phone's own margin. */
body.ot-fin .ofin-rest{ flex-basis:auto }
body.ot-fin:not(.ot-tight) .ofin-panel{
  top:50%;bottom:auto;
  transform:translate(-50%,-50%);
  max-height:calc(100% - var(--finPad,26px) * 2);
}


/* ==========================================================================
   WHAT A SHORT SCREEN GIVES UP — ornament, never a row or a rank
   ==========================================================================
   `body.ot-tight` is felt.js's own (felt.js:1869, stage under 520px tall); it
   already drops the drifting cards, the confetti and the sparks. On top of
   that, in the order the brief asks for — ornament first:
     · the TITLE goes entirely (declared in section 2) — it is the one thing
       on this screen whose job is decoration, and felt.js cannot budget for
       a child it does not know about;
     · the podium's BASE PLATES go — pure ornament, and worth ~7px of the
       podium's intrinsic height, which section 3's fit factor would otherwise
       have to take back out of the champion's own type;
     · the exit's DOOR GLYPH goes, and the pill's own padding comes in with it
       (below) — the phone is the one screen where the leave pill's gutter is
       genuinely tight, and if something has to give it is the ornament, never
       the label that says what the button does;
     · the champion's ring stops breathing, and the single feats sheen stops
       sweeping — motion is the cheapest thing on the screen to lose.
   NOTHING here removes a row, a rank, a name or a number.

   ⚠️ THE SCORE BARS USED TO GO HERE AND NO LONGER DO. The reason given was the
   fit pass's 18px row floor (felt.js:2094), where a 2px bar and the name's own
   descenders would want the same pixels — but that floor is not what a phone
   actually gets: measured at 844x390 with ten seats, `--restH` resolves to 26px
   and the bar occupies 2.6px→4.6px off the row's bottom edge, clear of a 10px
   name's line box. The bar is also the one thing the reference gives EVERY row
   (`reference/owner-screenshot.md` row 8), it is absolutely positioned so it
   costs the row no height at all, and dropping it made the phone the one
   viewport where a rank carried no score bar. Ornament is what a short screen
   gives up; this is not ornament. */
body.ot-fin.ot-tight .ofin-base{ display:none }
body.ot-fin.ot-tight .ofin-exit svg{ display:none }
body.ot-fin.ot-tight{
  --finExitPadX:calc(var(--exitF,14px) * .7);
  --finExitGap:calc(var(--exitF,14px) * .5);
}
body.ot-fin.ot-tight .ofin-step.s1 .ofin-av{ animation:none }
body.ot-fin.ot-tight .ofin-feats::after{ display:none }
body.ot-fin.ot-tight .ofin-feat{ gap:calc(var(--featPad,11px) * .3) }
body.ot-fin.ot-tight .ofin-head{ line-height:1.1 }
/* 6px of the podium's own intrinsic height; the sub-label itself is kept */
body.ot-fin.ot-tight .ofin-sub{ margin:0 }
/* …and BOTH of those have to be said again to the fit sum in section 3, or it
   would keep charging the podium for two boxes that are no longer there and
   shrink the champion's score to pay for them. `need` is an arithmetic model of
   the stack: every rule that removes a box from the stack owes it a line. */
body.ot-fin.ot-tight{ --finPodSubM:0px; --finPodBaseH:0px }

/* WHITESPACE GOES BEFORE TYPE DOES — and on this screen it goes before the
   podium does, because felt.js spends the podium FIRST.
   The give-back's order (felt.js:2055) is --podH, then --goH, then the panel's
   padding, then the honours block. --podH alone can cover 66px, so on a
   844×390 phone it covers the WHOLE 58px shortfall on its own and every other
   giver keeps its full size: the podium loses 39% of its height while six
   hardcoded gaps around it — `.ofin-rest`'s 8px margin (felt-final.css:164),
   the feats' 4px (:147), the feat cells' own padding (:148) — sit untouched at
   the size they were written for on a 900px board. Handing those pixels back
   BEFORE felt.js measures means the list needs less, the give-back takes less,
   and the fit factor above has more room to keep the champion legible: K goes
   from 0.68 to 0.91 at 844×390 — a podium 9% smaller than the design's instead
   of 32% smaller, and a score at 25px instead of 19.
   Every one of these is spacing. Not one is a glyph, a row or a number. */
/* THE RANK BADGE IS THE ONE THING THAT GETS BIGGER, because it is the only
   required element that costs the fit nothing: it is absolute inside
   `.ofin-av`, so its size is not in `need` at all. Scaled by K alone it would
   have come out SMALLER than it is today on the screen where it is already
   smallest — 5.70px of digit on a runner-up against 6.28 before — and the
   brief's floor is that all three places keep a readable badge. .33→.40 and
   .36→.42 put it back above where it started (6.9px and 8.75px of digit).
   THE CLIP IS WHY `bottom` MOVES WITH IT. `.ofin-av` is `overflow:hidden` and
   circular, so a badge sitting b of the diameter off the bottom can only be as
   wide as the chord there, 2·√(.25−(.5−b)²): at b=3% that is .341d, which is
   exactly what set the .33 in section 3 — a .42 badge at 3% would have its
   bottom corners sliced off. At b=7% the chord is .51d, clear of .42 with room
   to spare. Checked for --avS and --avS2 alike, since both are circles. */
body.ot-fin.ot-tight .ofin-step .ofin-rank{
  --finRankS:calc(var(--avS2,80px) * var(--finPodK) * .40);
  bottom:7%;
}
body.ot-fin.ot-tight .ofin-step.s1 .ofin-rank{ --finRankS:calc(var(--avS,96px) * var(--finPodK) * .42) }

body.ot-fin.ot-tight .ofin-panel{ padding-top:calc(var(--finPadIn,26px) * .62) }
body.ot-fin.ot-tight{ --finPadB2:calc(var(--finPadB,24px) * .7) }
body.ot-fin.ot-tight .ofin-feats{ margin:calc(var(--featMT,14px) * .55) 0 calc(var(--featMT,14px) * .2) }
body.ot-fin.ot-tight .ofin-feat{ padding:calc(var(--featPad,11px) * .55) 12px }
body.ot-fin.ot-tight .ofin-rest{ margin-top:calc(var(--restGapY,10px) * .6) }
body.ot-fin.ot-tight .ofin-notes{ padding-top:calc(var(--noteF,11.5px) * .2) }
/* the gap over the action row, trimmed as it always was — but said as the variable
   section 1 declares, so the tap area's top edge moves with it instead of landing a
   pixel inside `.det` on the one screen where the two are 3px apart */
body.ot-fin.ot-tight{ --finGoGap:calc(var(--goMT,12px) * .6) }

/* felt-final.css:257 drops the list to one column under a 720px WINDOW — a
   viewport query sizing a fluid component, which is exactly what the
   container-relative rule above replaces. It is neutralised rather than left
   to fight: the `minmax()` floor already steps 3 → 2 → 1 on its own, off the
   panel's real width, and it does so correctly on a 720px window that is
   showing a 400px panel. */
@media(max-width:720px){
  body.ot-fin .ofin-rest{
    grid-template-columns:repeat(auto-fill,minmax(var(--finRestColMin),1fr));
  }
}

/* One motion rule, and it is deliberate: the frozen file explains at :246
   why this screen ships no `prefers-reduced-motion` block. Everything added
   here that moves — the gold ring, the one sheen — is slow and ambient, and
   nothing flashes. A reader who has asked for less motion still gets the
   ring and the sheen stilled, which is the whole of what this file animates. */
@media(prefers-reduced-motion:reduce){
  body.ot-fin .ofin-step.s1 .ofin-av{ animation:none }
  body.ot-fin .ofin-feats::after{ animation:none }
}

/* ---- NOTHING FLOATS OVER THE CEREMONY -----------------------------------
   `felt-final.css` already stands down the game's floating furniture on this
   screen — `.gx-btn.gx-float`, `#fsbtn`, `#fsbtnhint-float` — under the owner's
   standing rule that outside a live round and the final results, nothing floats.
   Two newer chips were never added to that list: the card-skin picker
   (`skinpick.js`, `.skp-chip`) and the table picker (`tablepick.js`, `.tbp-chip`).

   Both DO try to hide themselves here. Each polls `SCREEN` every 600ms and drops
   its `*-on` body class while the screen is 'play' or 'over' (tablepick.js:99-107,
   and skinpick.js does the same for the same stated reason). But a poll is not a
   gate: arriving at the standings from a state where the class was still on —
   a reconnect straight onto the results, or any repaint inside the tick — leaves
   up to 600ms with two shop chips sitting on top of the podium. Captured at
   1920 in `rounds/r06/online-ar-1920x1080.png`: «مظهر البطاقات» and «الطاولة»
   both parked over the top-right of the ceremony. It is a narrow window, which is
   exactly why it is flaky across capture rounds rather than absent.

   So the class does the same thing the poll intends, without waiting for it. This
   hides only while `body.ot-fin` is set, so the pickers are untouched on every
   screen they belong on, and nothing about their own logic changes. */
body.ot-fin .tbp-chip,
body.ot-fin .skp-chip{ display:none }

/* AND THE WIN EMOJI, WHICH IS THE BIGGEST THING THAT LANDS ON THE CEREMONY.
   ⚠️ IT IS NOT ANY `.ofin-*` ELEMENT, and naming it wrong would have sent a fix at
   the wrong file. It is `div.emo.big` — the celebration glyph `floatEmoji()` throws
   from `endGame()` on BOTH pages, identically: CSS at overtake.html:657-659 /
   overtake-online.html:434-436, the glyph set `WIN_EMOJI=['🏆','🎉','🔥','⚡','👑',
   '💥','🌟','🚀']` at :1174 / :1386, the call at :1964 / :1907, the function at
   :2634 / :2122. It is `position:fixed;left:50%;top:42%;font-size:96px;z-index:60`
   appended straight to `document.body` — so it is OUTSIDE `.ofin` entirely, in the
   root stacking context, and 60 clears the whole finale subtree (`.ofin` is 2,
   `.ofin-panel` 4, the rank badge 3). No z-index anywhere inside this screen can
   reach it, and `.ofin-card` — the drifting glass tiles a critic suspected — cannot
   be it: at z-index 1 it is a SIBLING of the panel's 4 in the same context and can
   never paint over it. Nor confetti (`.ofin-cf`, 3-5px rectangles) or the sparks
   (`.ofin-spk`, 3-5px dots), which are two orders of magnitude too small.

   WHAT IT COVERS, measured rather than assumed: `endGame()` fires two of them
   350ms apart when the local player tops the sort with a non-zero score — always
   🏆, plus one at random — and `left:50%` pins them dead centre over the middle
   step for the 2300ms they live. Ink bbox at 1920x1080 is x917-1003 / y410-517,
   centre (960,463); `.ofin-step.s1 .ofin-body .no` — the champion's own 44px place
   digit — centres at (963,442). It lands on the bullseye. (Correcting the finding
   as reported: what is buried is the PEDESTAL digit, not the `.ofin-rank` badge,
   which reads its gold 1 cleanly in all four tie captures. And it is in all FOUR
   1920 tie captures, not two of eight — the tie fixture is simply the only one
   where the local player wins, and the 844 captures miss it only because the node
   has expired by the time the rig resizes.)

   THE OWNING FILES ARE THE TWO PAGES, WHICH THIS ROUND DOES NOT TOUCH. This is the
   same answer the two picker chips above got, for the same reason and by the same
   means: one rule in this file, scoped to this screen, out-specifying the page's
   own bare `.emo` (0,2,1 against 0,1,0, so stylesheet order does not matter). It
   reaches across because `.emo` is a plain `document.body` child with no stacking
   context or clip between it and the root. Nothing of the celebration is lost that
   the player can see: this screen already carries its own confetti, per-step
   sparks, the breathing gold ring, the gold pedestal and the laurel trophy asset
   beside the champion's score — and a 96px OS emoji is also the one thing on a
   screen being raised to a design reference that the project's own no-emoji-icons
   rule bans outright.
   SCOPED TO `ot-fin` ON PURPOSE, not to the body: `floatEmoji()` also fires on each
   round win during play (overtake.html:1939 / online:1902, 46px, no `.big`) and
   that one is intended, and felt.js strips `ot-fin` when the finale bails, on which
   path the legacy end screen shows and the emoji is the only celebration there is. */
body.ot-fin .emo{ display:none }

/* ---- AND THE PRIMARY NEEDS THE SAME COURTESY ----------------------------
   A blind critic measured every target on the 844x390 screen and found the one
   the earlier pass missed: the leave pill's hit area was grown to 45x44, but the
   PRIMARY — «العودة إلى الغرفة» online, «العب مجدداً» solo, the action almost
   every player takes — renders **175x29**, which made it SMALLER than the pill
   beside it. The visible plate cannot grow: `--goH` is 34px there and the panel's
   height budget at that size is already paid for in shrunk podium type.

   So the same answer as the leave pill: an invisible area, no visible change.
   It grows ASYMMETRICALLY — 3px up, the rest down — because the footer's
   «كل الكلمات، لاعبًا لاعبًا» link sits only ~3.4px above the action row at 844,
   and stealing its hit area to give to this one would just move the problem onto
   a smaller target. Below the row is the panel's own bottom padding: dead space.
   Absolutely positioned, so it adds nothing to any scrollHeight.

   Not fixed here, and carded instead: the standings rows are 180.5x26 at 844 and
   their eye glyph ~8px. Giving seven rows 44px each does not fit above the tray
   at that height, so it is a layout decision, not a padding one. */
body.ot-fin .ofin-go .btn{ position:relative }
body.ot-fin .ofin-go .btn::after{
  content:'';position:absolute;inset-inline:0;inset-block:-3px;
  min-height:var(--finTap,44px);
}
/* ⚠️ AND THE GROWTH STOPS AT THE PANEL'S FLOOR AT 844. `min-height:44px` on a
   34px row overshoots the panel's bottom padding by ~10px, and an absolutely
   positioned box that passes the padding box DOES count toward scrollHeight — it
   put `.ofin-rest` into a scroll on all four 844x390 combos, which is a criterion.
   (The same trap caught the previous pass, from the other direction.) So at tight
   the target grows only into the padding that is actually there: 29 -> 37px. That
   is honestly still under the 44px guideline, and it is recorded as such rather
   than rounded up — the rest needs the action row to be taller, which at 390px of
   stage height costs the podium type that was just fitted. */
body.ot-fin.ot-tight .ofin-go .btn::after{ inset-block:-4px;min-height:0 }

/* ==========================================================================
   THE PORTRAIT RING STOPS SAYING WHO AND STARTS SAYING WHICH PLACE
   ==========================================================================
   Owner ask, 2026-08-17, after this screen shipped: the podium's three place
   symbols had to become ONE ranked medal family. `felt.js` now strikes that
   family (`overtake/medals.js`), and this is the other half of the same fix.

   WHAT WAS WRONG. `felt-final.css:86` rings every portrait in `--rc`, which
   `felt.js:234 accent()` resolves to THE PLAYER'S OWN avatar colour. So the ring
   answered "who is this", and it answered it loudly enough to break the gold /
   silver / bronze ladder standing right behind it: a runner-up whose avatar
   accent happens to be warm read as more first-place than a champion whose
   accent is cyan, and the only thing marking the actual champion was a thin
   extra gold ring OUTSIDE his own colour (declared in section 4 above).

   WHAT IT DOES NOW. On this screen — and only on this screen, where a place is
   the entire subject — the ring is the rank's metal, taken from the same three
   ramps the medal, the badge and the pedestal already use. Identity does not
   disappear: `--rc` still throws the coloured halo around the portrait
   (`0 0 16px var(--rc)` in the frozen file's own box-shadow, and in section 4's
   champion override), so the player's colour still glows off the plate — it just
   no longer competes with the metal for the same 3px of rim.

   KEYED ON `data-rank`, NOT ON `.s1/.s2/.s3`. felt.js publishes the real rank as
   an attribute (`felt.js:finStepsHtml`), and since the owner's tie decision the
   two are no longer the same thing: level scores SHARE a place, so a shared first
   is `data-rank` 1, 1, 3 across steps s1, s2, s3. Reading the attribute means
   both golds get the gold rim and the crown ring, with no second place invented
   to fill the silver slot.

   AND THE PEDESTAL IS LEFT ALONE, ON PURPOSE. Its plate stays keyed to s1/s2/s3
   because the plinth is the POSITION ladder, not the rank ladder — tallest in the
   middle, and on a shared first the two champions still stand on differently
   sized boxes, exactly as they would on a real podium. The medal, the badge, the
   ring and the struck digit are what carry the rank, and all four now agree. If
   the owner would rather the plate followed the rank too, that is a paint change
   to the frozen file's six metal surfaces and is carded, not smuggled in here. */
body.ot-fin .ofin-step[data-rank="1"] .ofin-av{ border-color:#f0c04f }
body.ot-fin .ofin-step[data-rank="2"] .ofin-av{ border-color:#dfe6eb }
body.ot-fin .ofin-step[data-rank="3"] .ofin-av{ border-color:#e8a76a }

/* THE CROWN RING FOLLOWS RANK 1 TOO — same reason. Section 4 gave it to `.s1`,
   the centre plinth, which is right in every match that has no tie and wrong in
   the one case the owner just legislated for. Equal specificity to the rule it
   replaces (`body.ot-fin` + two classes + one class, against this one's
   `body.ot-fin` + class + attribute + class), so it wins by coming later in the
   file rather than by an `!important`. The declaration is section 4's, verbatim,
   with only the selector changed. */
body.ot-fin .ofin-step[data-rank="1"] .ofin-av{
  box-shadow:0 8px 18px rgba(0,0,0,.5),
             0 0 16px var(--rc,#45eded),
             0 0 0 var(--finGoldRing,3px) rgba(240,196,79,.9),
             0 0 30px rgba(240,196,79,.45);
  animation:gr-finCrownRing 2.8s ease-in-out infinite;
}
/* …and it stops breathing on a short screen, exactly as section 6 already stops
   the `.s1` one — an animation the owner cannot see is battery he can. */
body.ot-fin.ot-tight .ofin-step[data-rank="1"] .ofin-av{ animation:none }

/* ==========================================================================
   THE INSTALL OFFER GETS A CORNER INSTEAD OF THE PRIMARY BUTTON
   ==========================================================================
   Owner ask, 2026-08-17: «أريد منك أن تقوم بإيجاد مكان مناسب لزر التثبيت» — find
   the install button a proper place. What it had before was no place at all:
   `site/pwa.js` floated a fixed card at `bottom:16px` with no screen gate, and
   the bottom is where every screen in this game keeps its action row, so on a
   landscape phone it sat on top of THIS screen's «العودة إلى الغرفة». pwa.js now
   gates itself out of every live game screen and docks at the top when it does
   open; `finale.js dressInstall()` gives the offer this corner, and the reasoning
   for choosing the end of a match is written up there.

   ABSOLUTE, so it cannot re-enter the panel's height arithmetic — the fit at
   844x390 was measured pixel by pixel and an offer does not get to cost a
   scrollbar on the ceremony. `.ofin-panel` is already `position:absolute`
   (felt-final.css:53), so it is the containing block for free.

   `inset-inline-end` MIRRORS CORRECTLY HERE, unlike a `[dir=rtl]` selector — see
   the note at section 1: these pages ship `dir="ltr"` on <body> and switch on
   `body.ar`, which gives the body a computed `direction:rtl`, and logical
   properties read the computed direction rather than the attribute. So the chip
   crosses to the left in Arabic with no second rule.

   AND IT IS DRESSED DOWN FROM WHAT pwa.js SHIPS. `.gr-pwabtn` is a solid cyan
   gradient CTA, which is right in a settings row and wrong two inches from this
   screen's own solid cyan primary — two equal-weight buttons, one of which is an
   advert. Scoped to this corner it becomes the brand's glass treatment instead:
   translucent, blurred, cyan-rimmed, cyan text. Nothing outside `.ofin-inst` is
   affected, so the button keeps its normal look everywhere else it is mounted. */
body.ot-fin .ofin-inst{
  position:absolute;z-index:6;
  top:calc(var(--finPadIn,26px) * .3);
  inset-inline-end:calc(var(--finPadX,34px) * .38);
  line-height:0;
}
/* ⚠️ AND IT HAS A FLOOR, because scaling it off `--finHeadF` alone made it
   UNTAPPABLE. `--finHeadF` collapses on a short screen — at 844x390 the chip
   measured 17.6 x 17.6px, an eighteen-pixel target on a touch screen, which is
   not an offer, it is a trap. The floor costs nothing: the chip is absolutely
   positioned and sits inside the panel's own padding, so growing it does not
   touch any scrollHeight (the same reason the tap targets in section 5 grow with
   an absolute `::after`). Still honestly under the 44px guideline — 40px wide,
   36px at tight — because at 390px of stage height a 44px chip starts reaching
   for the title's line, and this is a tertiary offer sharing a screen with a
   primary that has the stronger claim on the space. Recorded, not rounded up. */
body.ot-fin .ofin-inst .gr-pwabtn{
  background:rgba(13,20,27,.55);
  -webkit-backdrop-filter:blur(9px);backdrop-filter:blur(9px);
  border:1px solid rgba(141,235,247,.26);
  color:#bfe6ec;
  box-shadow:0 6px 16px rgba(0,0,0,.34);
  font-size:max(12px,calc(var(--finHeadF,26px) * .44));
  padding:calc(var(--finHeadF,26px) * .26) calc(var(--finHeadF,26px) * .38);
  border-radius:calc(var(--finHeadF,26px) * .38);
  gap:calc(var(--finHeadF,26px) * .22);
  min-height:var(--finInstMin,40px);
  min-width:var(--finInstMin,40px);
  box-sizing:border-box;
}
body.ot-fin .ofin-inst .gr-pwabtn svg{
  width:calc(var(--finHeadF,26px) * .6);
  height:calc(var(--finHeadF,26px) * .6);
}
body.ot-fin .ofin-inst .gr-pwabtn:hover{
  transform:none;
  border-color:rgba(141,235,247,.55);
  color:#eaf4f5;
  box-shadow:0 8px 20px rgba(0,0,0,.4);
}
/* A SHORT SCREEN GETS THE ICON ALONE, and it moves into the corner the panel
   gives up at `ot-tight` — the radius goes to 0 and the padding is cut to .62
   there (section 6), so the chip follows the padding rather than sitting in the
   space it used to have. `.compact` is finale.js's call, not a media query. */
body.ot-fin.ot-tight .ofin-inst{
  top:calc(var(--finPadIn,26px) * .18);
  inset-inline-end:calc(var(--finPadX,34px) * .24);
}
body.ot-fin.ot-tight .ofin-inst .gr-pwabtn{
  --finInstMin:36px;
  padding:calc(var(--finHeadF,26px) * .22);
  border-radius:calc(var(--finHeadF,26px) * .3);
}
body.ot-fin.ot-tight .ofin-inst .gr-pwabtn svg{ width:17px;height:17px }

/* ==========================================================================
   THE PEDESTAL FOLLOWS THE RANK TOO — the last surface that still said POSITION
   ==========================================================================
   Owner, 2026-08-18, asked for it plainly: «نعم بالنسبة لرقم اثنين أريدك أن
   توحدها» — unify it. The previous pass moved the medal, the badge, the ring and
   the struck digit onto `data-rank` and deliberately left the plinth keyed to
   `.s1/.s2/.s3`, on the argument that the plinth is the POSITION ladder. He has
   overruled that, and he is right for the case it exists for: with a shared first
   place, a gold medal, a gold rim and a gold "1" standing on a SILVER plate is
   not a podium, it is a mismatch.

   ⚠️ THIS CHANGES NOTHING IN A NORMAL MATCH, AND THAT IS THE POINT. Every value
   below is copied verbatim from `felt-final.css` — gold from :95-96, :104-108,
   :127-128, :133-134; silver from :97-98, :109-113, :129-130, :135-136; bronze
   from :99-100, :114-118, :131-132, :137-138. Without a tie `data-rank` is 1, 2, 3
   in the same order as s1, s2, s3, so these rules re-state exactly what was
   already painted. They only DO anything when a place is shared.

   AND ONLY THE PAINT MOVES. Widths, heights, radii, the box-shadows and the type
   sizes stay keyed to the step, because those are the plinth's PHYSICAL ladder —
   tallest in the middle — and two players sharing gold still stand on differently
   sized boxes, exactly as they would on a real podium. `background-image` rather
   than the `background` shorthand for the same reason: it swaps the metal without
   touching anything else the shorthand would reset.

   Specificity: `body.ot-fin` + `.ofin-step[data-rank]` + the child = four
   class-level selectors against the frozen file's three, so it wins on weight
   rather than with an `!important`. This file still ships zero. */
body.ot-fin .ofin-step[data-rank="1"] .ofin-top{
  background-image:linear-gradient(180deg,#ffedb0,#dcae40);
}
body.ot-fin .ofin-step[data-rank="2"] .ofin-top{
  background-image:linear-gradient(180deg,#f7fafc,#b6c1ca);
}
body.ot-fin .ofin-step[data-rank="3"] .ofin-top{
  background-image:linear-gradient(180deg,#f3c79b,#b07840);
}
body.ot-fin .ofin-step[data-rank="1"] .ofin-body{
  background-image:linear-gradient(180deg,rgba(255,255,255,.3),rgba(0,0,0,0) 30%,rgba(0,0,0,.28) 100%),
                   linear-gradient(90deg,#7a5a1c,#f0c04f 15% 85%,#7a5a1c);
  border-color:#ffe9a8;
}
body.ot-fin .ofin-step[data-rank="2"] .ofin-body{
  background-image:linear-gradient(180deg,rgba(255,255,255,.32),rgba(0,0,0,0) 30%,rgba(0,0,0,.26) 100%),
                   linear-gradient(90deg,#5d6870,#dfe6eb 15% 85%,#5d6870);
  border-color:#f4f8fb;
}
body.ot-fin .ofin-step[data-rank="3"] .ofin-body{
  background-image:linear-gradient(180deg,rgba(255,255,255,.28),rgba(0,0,0,0) 30%,rgba(0,0,0,.26) 100%),
                   linear-gradient(90deg,#6e431c,#e8a76a 15% 85%,#6e431c);
  border-color:#f3c79b;
}
body.ot-fin .ofin-step[data-rank="1"] .ofin-base{
  background-image:linear-gradient(180deg,#c2933a,#6e4f14);
}
body.ot-fin .ofin-step[data-rank="2"] .ofin-base{
  background-image:linear-gradient(180deg,#aab4bc,#5c666e);
}
body.ot-fin .ofin-step[data-rank="3"] .ofin-base{
  background-image:linear-gradient(180deg,#b0783f,#5f3d18);
}
/* the ink cut into the plate, which has to move with the metal it is cut into —
   dark-on-gold is legible, dark-on-gold-with-a-silver-halo is not */
body.ot-fin .ofin-step[data-rank="1"] .nm{ color:#241a04 }
body.ot-fin .ofin-step[data-rank="1"] .no{
  color:rgba(36,26,4,.85);text-shadow:0 1px 0 rgba(255,240,190,.5);
}
body.ot-fin .ofin-step[data-rank="2"] .nm{ color:#10161c }
body.ot-fin .ofin-step[data-rank="2"] .no{
  color:rgba(16,22,28,.85);text-shadow:0 1px 0 rgba(245,250,255,.5);
}
body.ot-fin .ofin-step[data-rank="3"] .nm{ color:#2a1607 }
body.ot-fin .ofin-step[data-rank="3"] .no{
  color:rgba(42,22,7,.85);text-shadow:0 1px 0 rgba(250,225,190,.5);
}
/* …and so does the score standing above it, for the same reason: the frozen file
   colours it off the step (felt-final.css:75-77), so a shared first showed one
   champion's 120 in gold and the other's in silver. Only the COLOUR moves; the
   size stays the step's, since that is the physical ladder again. */
body.ot-fin .ofin-step[data-rank="1"] .ofin-sc .v{
  color:#f0c04f;text-shadow:0 0 26px rgba(240,196,79,.6);
}
body.ot-fin .ofin-step[data-rank="2"] .ofin-sc .v{
  color:#dfe6eb;text-shadow:0 0 20px rgba(210,228,240,.55);
}
body.ot-fin .ofin-step[data-rank="3"] .ofin-sc .v{
  color:#f0a44f;text-shadow:0 0 20px rgba(240,164,79,.55);
}

/* ==========================================================================
   A PODIUM WITH FEWER THAN THREE SCORERS
   ==========================================================================
   The owner's rule is that a place is EARNED, so `finScorers()` hands the podium
   only players who actually banked points and `finStepsHtml` walks order [1,0,2]
   and `continue`s past a missing one (felt.js). With two scorers that leaves s2
   and s1 and no s3 — but the two survivors keep the absolute offsets the
   three-step ladder gave them, s2 pinned to `--sideX` and s1 on the pod's centre
   line (felt-final.css:69-71). The pair therefore sits LEFT of centre and the
   whole right gutter is dead. Measured, both languages, identical to the pixel
   because the podium is laid out in physical left/right:
     1920  band 339px   group off-centre -147.5
     1600  band 285px   group off-centre -123.0
     1366  band 242.5px group off-centre -104.8
     844   band 123.5px group off-centre  -53.3
   Both steps move right by exactly half that offset, which puts the pair on the
   pod's centre line without touching the gap between them.

   ⚠️ WIDTHS ARE NOT TOUCHED. felt.js resolves the three step widths against each
   other and section 3 already refuses to scale them; only the two `left` offsets
   move. The percentages resolve against `.ofin-pod`'s own width, so nothing here
   has to know the pod in pixels and no new variable is needed.

   ⚠️ `:has(.ofin-step.s2)` IS LOAD-BEARING, NOT DECORATION. With ONE scorer only
   s1 renders and it is ALREADY centred — measured off-centre 0.0 at all four
   viewports — so a rule keyed on "no s3" alone would shove a correct lone plinth
   off centre. A browser without `:has()` drops these two selectors whole and
   keeps today's rendering, which is the right way for this to fail. */
body.ot-fin .ofin-pod:has(.ofin-step.s2):not(:has(.ofin-step.s3)) .ofin-step.s1{
  left:calc(75% - var(--sideX,40px) / 2 - var(--s1W,280px) / 4);
}
body.ot-fin .ofin-pod:has(.ofin-step.s2):not(:has(.ofin-step.s3)) .ofin-step.s2{
  left:calc(25% + var(--sideX,40px) / 2 - var(--s1W,280px) / 4);
}
/* NOBODY SCORED AT ALL — nobody had ever looked at this case. felt.js emits no
   steps whatsoever and `.ofin-pod` keeps its entire `--podH`: 968x330px of empty
   box at 1920 with the confetti still falling through it, and the ten also-rans
   pushed to the bottom of the panel. It collapses, and `.ofin-rest` (flex:1)
   takes the room back.

   ⚠️ NOT UNDER `ot-tight`, AND THAT IS MEASURED, NOT CAUTION. At 844x390 the
   panel's height is pinned, so collapsing the pod hands `.ofin-rest` 153 extra
   px — and felt.js's rest-fit pass then grows `--restH` toward its 48 cap, which
   drags `--restNmF` up, which drags `--finRestColMin` (section 4) past half the
   panel, which drops the grid from 2 columns to 1. That pass computes `rowsN`
   from the column count BEFORE it changes the row height and never re-derives it,
   so ten 48px rows land in a 299px box: scrollHeight 516 against clientHeight
   299. That is a latent felt.js bug this rule would merely be the first thing to
   trigger, so the collapse stays off the one screen where the panel is pinned.
   The bug is carded in its own right. */
body.ot-fin:not(.ot-tight) .ofin-pod:not(:has(.ofin-step)){ display:none }
