/* ==========================================================================
   GRIDROP · OVERTAKE — THE GALLERY: the two cases that frame the ceremony.

   The owner's approved render (`.claude/gauntlet-gallery/reference/owner-
   screenshot.md`) puts THREE vertical zones on the felt: a tall rounded case
   on each gutter and the ceremony between them. This file is the two cases.
   The ceremony is not touched by one declaration in here.

   Loaded AFTER `finale.css`, which is itself after `felt-final.css`, so at
   equal specificity this file wins and no `!important` is needed anywhere in
   it — the same discipline finale.css states in its own header.

   ⚠️ EVERY SELECTOR IS SCOPED UNDER `.ofin`. A bare `.ogal-nm` or `.ogal-av`
   rule would leak onto the lobby, the browse list, the invite row and the
   racebar on BOTH pages — those pages carry `.nm`, `.av`, `.sav` and `.st`
   nodes of their own, and a wing class is only one careless rename away from
   colliding with them. `.ofin` exists on the end screen and nowhere else
   (`felt-final.css:37`), so it is the cheapest possible fence.

   WHAT THIS FILE MAY STYLE is fixed by `.claude/gauntlet-gallery/CONTRACT-DOM
   .md` §2-§3 — `.ogal{,-h,-tiles,-tile,-prev,-meta,-nm,-pill,-more,-rows,-row,
   -av,-dot,-who,-st,-inv,-empty,-foot}` plus the state attributes `data-ogal`,
   `data-ogal-fit`, `data-side`, `data-state`, `data-kind`, `data-pill`,
   `data-online` and the measured `--ogalGut`. Nothing else. The module builder
   owns `gallery.js`; neither of us may change that contract alone.

   SIZES COME FROM THE VARIABLES felt.js ALREADY PUBLISHES on `<body>`, and from
   exactly SIX of them — `--finPad` (:2029), `--finPadIn` (:2033), `--finPadX`
   (:2035), `--restGapY` (:2134), `--restAv` (:2136) and `--restNmF` (:2137) —
   each already clamped off felt.js's one R = clamp(min(W/1600,H/900),.34,1.12).
   Every other size in this file is a named fraction of one of those, published
   here as its own variable with the same `var(…, <default>)` fallback style the
   two older files use. The list is short on purpose: the fewer inputs, the fewer
   ways a wing can disagree with the panel it is standing beside.

   ⚠️ NO VIEWPORT MEDIA QUERY SIZES ANYTHING HERE. Standing project rule, and
   both sibling files obey it. The collapse rides on the `data-ogal-fit`
   attribute gallery.js writes (CONTRACT-DOM §5) and on felt.js's own
   `body.ot-tight` (`felt.js:1932`, stage under 520px tall) — never on a width
   or height breakpoint of my invention. The only bare px in this file are
   PHYSICAL minimums (a fingertip, a readable glyph, a device hairline); each
   one is named where it appears and says why it must not scale.

   THE PALETTE IS THE TABLE'S, not the site's: felt #143440 / #0d2530, neon
   teal #45eded, gold #f0c04f, ink #eafcff, muted #9fb6bf, presence green
   #3ad29f (the same green `tables/skin.css:399` strikes the equipped tick in).

   ⚠️ BUT THE MATERIAL IS `.ofin-panel`'S, and that is a different question from
   the palette. The reference draws one felt with no card behind the screen; the
   live screen has a wallpaper and a bordered panel, and the owner has ruled that
   both stay. A case therefore has to be the same KIND OF OBJECT as the ceremony
   rather than a different material standing beside it — so its edge, its blur,
   its three shadows and its corner all come from the panel's own recipe
   (felt-final.css:56-62), restated as the `--ogalEdge/Cast/Bloom/Lift/Blur`
   tokens in section 1 and spent at `--ogalSec` of the panel's weight. The one
   thing that is NOT copied is how dark the panel's fill is: the owner ruled a
   case may not end up darker than the felt it sits on, and section 2 records
   why the fill inverts while the material does not.
   ⚠️ THERE IS NO RED IN THIS FILE, deliberately. `--red:#ff6b6b` belongs to the
   leave button alone on this screen (finale.css section 1), and a second red
   object in the gutters would compete with the one destructive action the
   player has.
   ========================================================================== */


/* ==========================================================================
   1 · THE TOKENS, AND THE GATE
   ==========================================================================

   ⚠️ THE WINGS DO NOT EXIST UNTIL THE JS HAS MEASURED. `--ogalGut` is written
   INLINE on `.ofin` by gallery.js (CONTRACT-DOM §3) and inline always beats a
   stylesheet, so the fallbacks below only ever apply in the window before the
   first pass — and in that window the honest width is NONE. `data-ogal="on"`
   is the JS saying "I have measured and both wings fit"; without it nothing
   paints. This is also the whole of `data-ogal="off"`: one gate, two jobs, and
   no chance of a 300px case flashing into a 190px gutter on first paint.

   Declared on `.ofin` rather than on `body` so the tokens die with the stage.
   `.ofin` is destroyed and rebuilt wholesale on every re-render
   (`felt.js:1858-1874` → paintFinal), which is exactly the lifetime we want. */
.ofin{
  /* ---- paint ------------------------------------------------------------ */
  --ogalInk:#eafcff;                    /* names, values — the reference's white  */
  --ogalMute:#9fb6bf;                   /* headers, status lines, the foot        */
  --ogalTeal:#45eded;                   /* the pills and the chevron              */
  --ogalGold:#f0c04f;                   /* the champion's tint, used once         */
  --ogalLive:#3ad29f;                   /* the presence dot = skin.css:399's green */

  /* ---- THE PANEL'S OWN MATERIAL, so the three boxes read as one kit -------
     The reference puts everything on ONE felt with no card behind the screen.
     The live screen has a wallpaper AND `.ofin-panel`, and the owner has ruled
     that both stay — so the only way to stop a case reading as "a sidebar
     parked next to a modal dialog" is for the case to be made of the SAME
     STUFF as the panel: same edge, same pigments, same light, same shadow
     logic, same corner-radius token, at less weight.

     ⚠️ THESE ARE RESTATED HERE BECAUSE THERE IS NOTHING TO `var()`.
     `.ofin-panel` writes its material as LITERALS (felt-final.css:58-62) and
     declares no tokens for it, so a case cannot read them at run time. Naming
     them here with the line they came from is the next best thing: one place to
     re-sync, and a `git grep` from felt-final.css lands on it. The one part of
     the panel's material that IS a token — its radius — is read as a token
     below (`--ogalRad`) rather than copied. */
  --ogalEdge:rgba(120,214,226,.22);     /* = .ofin-panel's border    :61 */
  --ogalCast:rgba(0,0,0,.65);           /* = its drop shadow's ink   :62 */
  --ogalBloom:rgba(69,237,237,.08);     /* = its cyan bloom          :62 */
  --ogalLift:rgba(230,245,250,.1);      /* = its inset top light     :62 */
  --ogalBlur:7px;                       /* = its backdrop blur       :59 */

  /* ⚠️ HOW MUCH OF THE PANEL'S WEIGHT A CASE IS ALLOWED TO CARRY, in one place.
     Criterion 2 requires the cases to stay VISIBLY SECONDARY and it already
     passes — matching the panel's material is not permission to match its
     presence. So the case takes the panel's PIGMENTS unchanged (same ink, same
     cyan, same light: that is what makes it the same object) and .6 of its
     shadow GEOMETRY (a smaller cast, a tighter bloom: that is what makes it the
     smaller one). Same paint, less of it — rather than a second palette mixed
     to look "like" the first, which is how two objects drift apart. */
  --ogalSec:.6;

  /* ---- rhythm, all derived ---------------------------------------------- */
  /* the breathing room on either side of a case, inside its gutter. .62 of the
     panel's own horizontal padding (`--finPadX`, felt.js:2034-2035) puts it at
     22.3px at 1920 — the reference's own figure — and it collapses with the
     panel's padding on a small screen instead of holding a desktop number. */
  --ogalPad:calc(var(--finPadX,34px) * .62);
  /* the case's own inner padding, and its floor. 8px is a PHYSICAL minimum:
     below it the 1px border and the tile's own edge start reading as one line
     rather than as a container holding something. */
  --ogalPadIn:max(8px,calc(var(--finPadIn,26px) * .62));
  --ogalGap:calc(var(--restGapY,10px) * 1.1);         /* between tiles / rows    */
  /* ⚠️ THE CORNER IS A RELATIONSHIP TO THE PANEL'S CORNER, NOT A NUMBER OF ITS
     OWN. Round 1 derived this off `--finPadIn`, which is the panel's PADDING —
     a coincidence that happened to land near the reference's ≈22px and would
     have drifted the moment felt.js retuned one token and not the other.
     `--finRad` IS the panel's radius (felt-final.css:56, written by felt.js:
     2031 as `tight ? 0 : round(26*R)`), so reading it is what makes the two
     corners one family for good: .78 of the panel's own curve, 22.6px against
     its 29px at 1920 — the reference's figure, now arrived at structurally.
     (`--finRad` is 0 under `ot-tight`; no wing renders there, section 6.) */
  --ogalRad:calc(var(--finRad,26px) * .78);           /* 22.6px at 1920 = REF     */
  --ogalRadIn:calc(var(--ogalRad) * .62);             /* 14.0px at 1920 = REF     */

  /* ---- type, and the 11px floor ----------------------------------------- */
  /* ⚠️ `max(11px, …)` ON EVERY ONE OF THESE, AND IT IS NOT BELT-AND-BRACES.
     `--restNmF` bottoms out at 10px (felt.js:2137's own clamp floor, and
     :2236's give-back floor is 10 too), so a name derived straight off it
     would ship at 10px on a landscape phone — under BAR criterion 12's stated
     floor. 11px is a legibility measurement, not a proportion, so like
     finale.css's `--finTap:44px` it deliberately does NOT scale with R.
     Above the floor every one of these still rides R through --restNmF. */
  --ogalHF:max(11px,calc(var(--restNmF,16px) * .74));   /* the case header      */
  --ogalNmF:max(11px,calc(var(--restNmF,16px) * .95));  /* item / friend name   */
  --ogalStF:max(11px,calc(var(--restNmF,16px) * .74));  /* the status line      */
  --ogalPillF:max(11px,calc(var(--restNmF,16px) * .76));/* Price / Invite       */
  --ogalFootF:max(11px,calc(var(--restNmF,16px) * .74));/* See all / N of M     */

  /* ⚠️ 32px IS A FINGERTIP, NOT A PROPORTION — the same reasoning finale.css
     records for its own `--finTap:44px` at :283. BAR criterion 12 sets this
     floor for whatever survives on a landscape phone; scaling it with R would
     be scaling away the only thing it exists for. It is lower than finale's 44
     because these are TERTIARY actions sharing a screen with a primary that
     has the stronger claim on the space — recorded as such rather than rounded
     up, exactly as finale.css:1408-1417 records its own shortfall. */
  --ogalTap:32px;

  /* the friend portrait: a shade larger than a standings row's, because a row
     here carries two lines of text beside it and a 28px disc reads as a bullet
     rather than as a person. 24px is a second physical floor. */
  --ogalAv:max(24px,calc(var(--restAv,28px) * 1.25));

  /* how tall a preview box wants to be, as a fraction of the case's OWN width
     rather than of the viewport — so a narrow case gets a proportionate
     preview and the tile keeps its shape at every R. ONE value, at every size:
     round 1 also turned this knob down to .30 under `body.ot-tight` to keep a
     kit wing alive on a phone, and section 6 has since established that no wing
     renders there at all — so the second value went with the block that set
     it, and `--ogalPrevK` no longer has a state to disagree with. */
  --ogalPrevK:.52;

  /* THE CASE'S OUTER BOUND — and it is a bound, not a height. `--finPad` is
     `.ofin-panel`'s own inset (felt-final.css:53-54, felt.js:2029), so
     `100% − 2 × --ogalInset` is exactly the box the panel is allowed to fill.
     Section 2 spends it as a `max-height` and never as a size.

     ⚠️ ROUND 1 PINNED `top` AND `bottom` TO THIS, AND THAT WAS THE DEFECT.
     The panel is `top:50%` + `translate(-50%,-50%)` capped at
     `100% − 2 × --finPad` (finale.css:1064-1068) — i.e. it is CONTENT-tall and
     the cap is only its ceiling. A case pinned to that ceiling is therefore
     taller than the panel by however much the panel did not need: 1006px
     against an 876px panel at 1920, which reads as a sidebar parked beside a
     modal rather than as a container beside the ceremony. The case is now
     content-tall too (`--ogalH` below), on the panel's own centre line. */
  --ogalInset:var(--finPad,26px);
}

/* ⚠️ THE ONE EXPRESSION BOTH WINGS TAKE THEIR WIDTH FROM — this is criterion 1.
   `clamp()` is CONTRACT-DOM §4's, verbatim in shape; the outer `min()` is the
   defence.

   THE TRAP IT IS THERE FOR: `clamp(196px, …, 320px)` has a FLOOR, and a floor
   is a promise to return 196px even when the gutter is 180px wide. The case
   would then start at a negative inset and its inner edge would land at
   (gut + W)/2 — INSIDE `.ofin-panel`, which is criterion 11's one hard failure
   ("no case rect intersects the centre column's rect"). gallery.js is supposed
   to have written `data-ogal-fit="none"` long before that, but a CSS file that
   can only be correct while its JS is correct is a CSS file with a bug in it.
   So the width is additionally capped at the gutter less a 1px hairline on
   each side, and the case is centred in whatever gutter it actually got.

   The 2px is a DEVICE HAIRLINE, not a scaled space: it exists so the two rects
   are provably disjoint rather than tangent, because a critic's intersection
   test may or may not treat touching edges as an overlap and the answer must
   not depend on which. */
.ofin{
  --ogalW:min(
    clamp(196px, calc(var(--ogalGut,300px) - var(--ogalPad) * 2), 320px),
    calc(var(--ogalGut,300px) - 2px)
  );
}

/* ⚠️ AND THE ONE EXPRESSION BOTH WINGS TAKE THEIR HEIGHT FROM. This is the other
   half of criterion 1, and the answer to round 1's "the case out-frames the
   ceremony".

   THE REFERENCE draws the two cases with the same top edge and the same bottom
   edge AS EACH OTHER, beside a centre column — never a case that overhangs the
   ceremony top and bottom. `.ofin-panel` is content-tall under a cap
   (finale.css:1064-1068), so the only way a case can sit inside the same band is
   to be content-tall as well, on the same centre line, under the same cap.

   NOTHING BELOW IS MEASURED OR GUESSED: every term is one of this file's own
   tokens, so the sum rides felt.js's R exactly as its parts do, and BOTH wings
   take the SAME expression — which is what keeps criterion 1's top-edge and
   bottom-edge tests structural now that `top`/`bottom` no longer pin them.

   THE SUM IS THE KIT CASE'S, because it is the taller body of the two and the
   one the reference details tile by tile. The friends case takes the identical
   height and spends its slack the way section 5 already asks: below the last
   row, above the foot. Same for a kit case that loses a tile — CONTRACT §6
   REMOVES a tile it cannot render rather than showing a hole, and the freed
   height lands in ONE place under the last tile, never split into two voids. */
.ofin{
  /* the preview box, published once HERE so the height sum, section 4's box and
     the module's own `--fcw` arithmetic all read one number. */
  --ogalPrevH:calc(var(--ogalW,300px) * var(--ogalPrevK));

  /* the meta line is as tall as the TALLER of its two children, and that is the
     PILL, not the name: `1.3` of line, plus `2 × .22` of its own padding, plus
     its 1px border on each side. Written as a `max()` rather than assumed,
     because which of the two wins depends on the `--ogalNmF` / `--ogalPillF`
     ratio in section 1 and that ratio is allowed to change. */
  --ogalMetaH:max(
    calc(var(--ogalNmF) * 1.35),
    calc(var(--ogalPillF) * 1.74 + 2px)
  );
  /* ONE TILE: preview + the tile's own inner gap + the meta line + its padding
     on both sides + its 1px border on both sides. The `max(6px, …)` is section
     3's own padding floor restated rather than re-derived, so the height sum
     and the box it is summing can never disagree about it. */
  --ogalTileH:calc(
    var(--ogalPrevH) + var(--ogalPadIn) * .42 + var(--ogalMetaH)
    + max(6px,calc(var(--ogalPad) * .55)) * 2 + 2px
  );
  /* the header: one line, the gap under it, and the 1px rule itself */
  --ogalHeadH:calc(var(--ogalHF) * 1.35 + var(--ogalPadIn) * .45 + 1px);
  /* the footer is its 32px TARGET, never its 12px label — `.ogal-more` carries
     `min-height:var(--ogalTap)` and a 1px top rule (section 3). */
  --ogalFootH:calc(var(--ogalTap) + 1px);

  /* header + three tiles + the two gaps between them + footer, then the case's
     own two gaps (`--ogalGap * .8` each), its padding on both sides, and its
     1px border on both sides. 813px at 1920 against an 876px panel: inside the
     ceremony's band, and visibly secondary to it — which is what the reference
     asks for in words. */
  --ogalH:calc(
    var(--ogalHeadH)
    + var(--ogalTileH) * 3 + var(--ogalGap) * 2
    + var(--ogalFootH)
    + var(--ogalGap) * 1.6
    + var(--ogalPadIn) * 2 + 2px
  );
}


/* ==========================================================================
   2 · THE CASE — a container on the felt, not a card
   ==========================================================================

   `.ofin` is `position:absolute;inset:0;z-index:2;overflow:hidden`
   (felt-final.css:37) and `.ofin-panel` is ALSO absolute, centred, at
   `width:var(--finW)` (felt-final.css:53-57, and finale.css:1064-1068 re-pins
   it to `top:50%` with a `max-height`). So the stage has two free gutters,
   each `(100% − var(--finW))/2` wide, and an absolutely positioned node placed
   there contributes NOTHING to the panel's height arithmetic. That is the only
   reason a whole new panel can be added to this screen at all — felt.js budgets
   the panel from the five children it knows about, and a sixth flow child it
   has never heard of is a height it cannot give back (the same lesson
   finale.css:2 records for its own title).

   ⚠️ z-index 3, AND THAT NUMBER IS LOAD-BEARING. The stage's own layers are
   `.ofin-card` 1 (the drifting glass), `.ofin` 2, `.ofin-rank` 3,
   `.ofin-panel` 4, `.ofin-exit` 5, `.ofin-det`/`.ofin-inst` 6. At 3 the wings
   sit above the drifting glass — so they read as objects on the felt rather
   than as things the glass slides over — and BELOW the panel, so that even if
   the geometry above were ever wrong, a wing still cannot paint over the
   ceremony. Belt for the width cap's braces. */
.ofin .ogal{
  position:absolute;
  z-index:3;
  box-sizing:border-box;
  width:var(--ogalW);
  /* ⚠️ CENTRED ON THE PANEL'S OWN AXIS AND CONTENT-TALL — not pinned to the
     stage's inset, which is what made the case 1006px tall beside an 876px
     ceremony in round 1. `.ofin-panel` is `top:50%` + `translate(-50%,-50%)`
     with `max-height:100% − 2 × --finPad` (finale.css:1064-1068); the case takes
     the same 50% line, the same cap and its own content height (`--ogalH`), so
     the two objects live in one vertical band and a case can never out-frame
     the ceremony again. Both wings take this identical block, which is what
     keeps criterion 1's top-edge and bottom-edge tests structural rather than
     lucky. The cap still governs a short-but-not-tight stage: there the case
     fills it and the flex construction below does the shrinking, exactly as
     before. */
  top:50%;
  bottom:auto;
  height:var(--ogalH);
  max-height:calc(100% - var(--ogalInset) * 2);
  transform:translateY(-50%);
  padding:var(--ogalPadIn);
  display:flex;
  flex-direction:column;
  gap:calc(var(--ogalGap) * .8);

  /* ---- THE SURFACE: the panel's material, at the panel's own recipe -------
     `.ofin-panel` is a translucent 180° two-stop film over a 7px backdrop blur,
     closed by a 1px cool-cyan hairline, a wide black cast, a faint cyan bloom
     and a 1px inset light along its top edge (felt-final.css:58-62). Every one
     of those five moves is repeated below, in the same order, out of the tokens
     above — so the kit case, the ceremony and the friends case are one object
     seen three times, and a future change to the panel's recipe has ONE place
     to be answered here rather than five scattered literals.

     🔴 THE ONE DELIBERATE DIVERGENCE, AND THE OWNER RULED IT: the FILL's
     DIRECTION. The panel's film DARKENS — rgba(12,22,28,.72) → rgba(8,14,19,.82)
     takes its interior to rgb(9,18,23), far below the ground it sits on. A case
     copying that literal darkness measured ~4 RGB points DARKER than the felt
     60px away, against a reference that says in words "fill barely lighter than
     the felt", and the owner's ruling is that a case may not end up darker than
     its ground. So the case follows the panel's MATERIAL and not its DARKNESS:
     same construction, same blur, same two stops, same 180°, same "present at
     the lit top edge and receding downward" logic — but built from the table's
     own felt highlight #143440 (rgb 20,52,64), which is lighter than every tone
     of the nebula, so the film can only ever LIFT. Over the measured ground
     rgb(13,44,54) that resolves to about rgb(17,49,60) at the top and
     rgb(15,47,57) at the foot: barely lighter, everywhere, by construction.
     Falling alpha rather than the panel's rising alpha is the same optical
     statement with the pigment inverted — a dark film gets heavier as it
     recedes, a light film gets thinner. */
  border-radius:var(--ogalRad);
  background:linear-gradient(180deg,rgba(20,52,64,.62),rgba(20,52,64,.34));
  -webkit-backdrop-filter:blur(var(--ogalBlur));
  backdrop-filter:blur(var(--ogalBlur));
  /* THE EDGE IS THE PANEL'S EDGE, to the value. 1px is checked as 1px by
     criterion 2 and is a device hairline, which is why it is not derived; the
     colour is now `--ogalEdge` = the panel's own rgba(120,214,226,.22) rather
     than round 1's #45eded at .34. Still a teal border, as criterion 2 requires
     — and a QUIETER one than round 1 shipped, which is the right direction for
     an object that has to stay secondary. */
  border:1px solid var(--ogalEdge);
  /* ⚠️ THE PANEL'S THREE SHADOWS, IN THE PANEL'S ORDER, AT `--ogalSec` OF ITS
     GEOMETRY — the pigments are untouched, so the light on all three boxes has
     one source and one colour. The bloom is the part criterion 2 measures:
     `.ofin-title` carries `drop-shadow(0 0 18px rgba(69,237,237,.5))`
     (finale.css §2) and this is 24px at .08 — a sixth of its strength, and
     weaker than round 1's own .10. The inset top light is taken at full value
     rather than scaled: it is a 1px hairline of light, the same class of thing
     as the border, and a hairline at 60% is just a dimmer hairline. */
  box-shadow:0 calc(30px * var(--ogalSec)) calc(80px * var(--ogalSec)) var(--ogalCast),
             0 0 calc(40px * var(--ogalSec)) var(--ogalBloom),
             inset 0 1px 0 var(--ogalLift);

  /* ⚠️ `overflow:hidden` IS NOT WHAT SATISFIES criterion 11's `scrollHeight ==
     clientHeight`, AND ASSUMING IT DOES WOULD HIDE A REAL DEFECT. A clipped box
     still REPORTS its overflowing content in `scrollHeight`; hidden only stops
     you seeing it. What actually guarantees the equality is the flex
     construction below — every child of a case is either `flex:none` (header,
     footer) or shrinkable with `min-height:0` (the list), so the children's
     used sizes can never sum past the case. The clip is here for the second
     half of criterion 12 ("nothing is cut at any edge"): it keeps any single
     misbehaving renderer's ink inside the rounded corner rather than letting it
     spill onto the felt. */
  overflow:hidden;
}

/* ⚠️ THE GATE. Nothing paints until gallery.js has measured the gutter and said
   so. See the note at section 1: `--ogalGut` arrives as an inline style, and
   before it does the fallbacks in this file are guesses. A guess that paints is
   worse than nothing for the ~1 frame it lives. */
.ofin .ogal{ display:none }
.ofin[data-ogal="on"] .ogal{ display:flex }

/* THE TWO SIDES ARE LOGICAL, NOT PHYSICAL — CONTRACT-DOM §2: the kit wing is
   `data-side="start"`, which is the LEFT in English and the RIGHT in Arabic,
   and gallery.js never positions anything.

   ⚠️ `inset-inline-*` MIRRORS AND `[dir=rtl]` DOES NOT. Both pages ship
   `<body dir="ltr">` and switch language by toggling `body.ar` alone — the
   `dir` attribute is never written — so a `[dir=rtl]` SELECTOR matches nothing
   here and has already cost this project a round (finale.css:203-215 records
   the measurement). What IS true is that `tokens.css:16` gives `body.ar` a
   computed `direction:rtl`, and a LOGICAL property reads the computed
   direction rather than the attribute. So these two rules mirror on their own,
   with no Arabic copy of either, exactly as finale.css's `.ofin-bar` does.
   (`tokens.css:17` pins `.topbar/.cols/.grid/.pgrid` back to ltr — none of
   which is an ancestor of `.ofin`, which hangs off `.col`.)

   THE CASE IS CENTRED IN ITS OWN GUTTER rather than pinned to the stage edge:
   half the slack goes outside it and half between it and the panel, so the
   two wings are symmetric about the ceremony without either of them needing to
   know the other's number. */
.ofin .ogal[data-side="start"]{
  inset-inline-start:calc((var(--ogalGut,300px) - var(--ogalW)) / 2);
}
.ofin .ogal[data-side="end"]{
  inset-inline-end:calc((var(--ogalGut,300px) - var(--ogalW)) / 2);
}

/* THE HEADER LINE — «THE CHAMPION WORE» / «FRIENDS ONLINE».
   Small, uppercase, letter-spaced, muted grey-teal, at the case's reading-start
   corner (the reference, both cases). `text-align:start` rather than `left`,
   for the same reason as the insets above.

   ⚠️ CRITERION 2 MEASURES THIS AGAINST THE TITLE: header ≤ 0.35 × the title's
   size. `--finTitleF` is `--finHeadF * 2.2` (finale.css §2), which is 61.6px at
   1920 against this header's 12.6px — a ratio of 0.20, comfortably inside the
   bar at every R because both sides ride the same R. */
.ofin .ogal-h{
  flex:none;
  margin:0;
  text-align:start;
  text-transform:uppercase;                /* a no-op in Arabic, which has no case */
  font:800 var(--ogalHF)/1.35 Cairo,sans-serif;
  letter-spacing:.12em;
  color:var(--ogalMute);
  padding-bottom:calc(var(--ogalPadIn) * .45);
  border-bottom:1px solid rgba(159,182,191,.14);
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
/* ⚠️ ARABIC TAKES NO TRACKING, AND THIS IS ONE OF THE TWO PLACES A `body.ar`
   RULE IS GENUINELY NEEDED (logical properties cannot reach it). Arabic is
   cursive: `letter-spacing` inserts space between glyphs that are drawn joined,
   so a tracked Arabic header reads as a word coming apart. `.12em` on a 12px
   header is 1.4px between every letter of «ما ارتداه البطل». The Latin
   tracking is what makes a small uppercase line read as a label; Arabic gets
   the same job done by the weight and the colour alone. */
body.ar .ofin .ogal-h{ letter-spacing:normal }

/* THE CHAMPION'S CASE IS TINTED, ONCE. CONTRACT-DOM §3: `data-state` on
   `.ogal-kit` is `champion` (online — the owner's decision of 2026-08-22,
   option ب: everybody sees the CHAMPION's kit) or `yours` (solo). Gold is
   already this screen's champion colour — the pedestal, the medal, the badge,
   the rim — so the header rule under the champion's case borrows it and says
   whose kit this is without a second word of copy. One hairline, at .3 alpha:
   a stronger signal here would start competing with the podium for the same
   meaning. */
.ofin .ogal-kit[data-state="champion"] > .ogal-h{
  color:rgba(240,196,79,.82);
  border-bottom-color:rgba(240,196,79,.3);
}
/* `partial` = the champion is remote and only the avatar is knowable
   (CONTRACT-DOM §3). The case still renders and still says whose it is; it is
   simply quieter, because two of its three tiles are absent. Nothing is faked
   to fill them — that is criterion 8's rule for the friends case and it is the
   same rule here. */
.ofin .ogal-kit[data-state="partial"] > .ogal-h{ opacity:.8 }


/* ==========================================================================
   3 · THE KIT CASE — three tiles, each with a preview, a name and a pill
   ==========================================================================

   THE STACK IS A FLEX COLUMN AND EVERY MEMBER OF IT IS DECLARED, because that
   is what makes criterion 11 structural:
     · `.ogal-h`     flex:none        — never gives up a pixel
     · `.ogal-tiles` flex:1, min-h:0  — absorbs every pixel of slack, and is
                                        the only thing that shrinks
     · `.ogal-more`  flex:none        — the footer keeps its tap target
   With that, the children's used sizes cannot sum past the case at any R, in
   any language, at any tile count. It is felt-final.css:170-172's own lesson
   applied one level out: a shrinkable row in a capped list crushes itself to
   nothing and then reports that everything fits — so the things that must not
   crush are pinned, and exactly one box is allowed to absorb.

   ⚠️ `justify-content:flex-start`, AND ROUND 1 HAD IT AS `center` — the other
   half of the same defect. Centring only ever had a job because the case was
   pinned ~1006px tall around ~813px of tiles, and it did not remove that band
   of nothing: it SPLIT it, into ~125px of dead space above the first tile and
   ~120px below the last. Two voids are worse than one, because the reader sees
   the case float rather than fill. The case is content-tall now (`--ogalH`,
   section 1) so normally there is no slack at all; when there is — a tile the
   module could not render, or a case capped on a short stage — it belongs in
   ONE place, below the last tile and above the footer. One gap, one rhythm,
   top to bottom, which is the composition the reference draws. */
.ofin .ogal-tiles{
  flex:1 1 auto;
  min-height:0;
  display:flex;
  flex-direction:column;
  justify-content:flex-start;
  gap:var(--ogalGap);
}

/* THE TILE. Radius ≈14px, a slightly lighter fill than the case, a hairline
   border, generous inner padding — the reference, element for element.

   `flex:0 1 auto` and NOT `1 1 0`: a share-the-box tile would make the tiles
   define themselves from whatever height the case happened to have — a preview
   swimming in a box rather than the reference's compact stack, and a circular
   argument now that the case's height is derived FROM the tile. It is the tile
   that states its size and the case that follows. Natural height — which is now
   the CASE's height (`--ogalH`, section 1), so the two agree by construction —
   and shrink only when the case genuinely cannot hold them, i.e. on a stage
   short enough for `max-height` to bite before `ot-tight` does. `min-height:0`
   is what lets that shrink actually happen — without it the tile's own content
   is its floor and the stack overflows instead. */
.ofin .ogal-tile{
  flex:0 1 auto;
  min-height:0;
  box-sizing:border-box;
  display:flex;
  flex-direction:column;
  gap:calc(var(--ogalPadIn) * .42);
  padding:max(6px,calc(var(--ogalPad) * .55));   /* 6px = a physical floor: below
                                                    it the tile's border and the
                                                    preview's edge become one line */
  border-radius:var(--ogalRadIn);
  /* ⚠️ THE TILE LIFTS OFF THE CASE THE WAY THE CASE LIFTS OFF THE FELT, and for
     the same two reasons. The reference says a tile is "a slightly lighter fill"
     than the case — so it may not be a darker box, and round 1's second stop
     rgba(16,42,54,.5) resolved to roughly the ground's own value, which is what
     let a sample taken inside a case come back darker than the felt beside it.
     The pigment here is `--ogalLift`, the panel's OWN inset highlight
     (felt-final.css:62) at a third of a step and a sixth of a step: one light
     source for the whole kit, one direction of travel — felt → case → tile,
     each a little nearer the light. Same 180°, same falling alpha as the case
     above, so the three surfaces read as one material at three depths. */
  background:linear-gradient(180deg,rgba(230,245,250,.055),rgba(230,245,250,.025));
  border:1px solid rgba(159,182,191,.13);
  overflow:hidden;
}
/* hover is one more step toward the same light, plus the teal edge — never a
   different colour of tile, which would break the family for one interaction. */
.ofin .ogal-tile:hover{
  border-color:var(--ogalEdge);
  background:linear-gradient(180deg,rgba(230,245,250,.085),rgba(230,245,250,.045));
}

/* THE PREVIEW BOX. It is the only shrinkable part of a tile — the name and the
   pill are pinned below — so when the case runs out of room it is the artwork
   that gives, never the words that say what the thing is. The height is a
   fraction of the CASE'S OWN WIDTH, not of the viewport: a narrow case gets a
   proportionate preview and the tile keeps its shape at every R. */
.ofin .ogal-prev{
  flex:1 1 auto;
  min-height:0;
  height:calc(var(--ogalW,300px) * var(--ogalPrevK));
  position:relative;
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:calc(var(--ogalRadIn) * .72);
  overflow:hidden;
}

/* THE META LINE — name on the reading-start side, pill on the reading-end side.
   ⚠️ CRITERION 3 MEASURES THE PILL'S EDGE: "the pill's right edge is within 4px
   of the tile's inner right edge". `margin-inline-start:auto` on the pill is
   what makes that structural — the pill is pushed to the row's end edge, which
   IS the tile's inner end edge because the row has no padding of its own, so
   the delta is 0 by construction and it mirrors in Arabic for free. A
   `justify-content:space-between` would give the same answer with two tiles
   and the wrong one the moment a tile ever carries a third child. */
.ofin .ogal-meta{
  flex:none;
  display:flex;
  align-items:center;
  gap:calc(var(--ogalPadIn) * .4);
  min-width:0;
}
.ofin .ogal-nm{
  min-width:0;
  font:700 var(--ogalNmF)/1.35 Cairo,sans-serif;
  color:var(--ogalInk);
  text-align:start;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.ofin .ogal-meta > .ogal-nm{ flex:1 1 auto }

/* THE PILL. Small, teal-outlined, on the row's end edge (the reference). Three
   things it can say, per CONTRACT-DOM §3's `data-pill`; the DEFAULT below is
   the outline, so a pill that arrives with no attribute at all still reads as a
   pill rather than as bare text. */
.ofin .ogal-pill{
  flex:none;
  box-sizing:border-box;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  padding:calc(var(--ogalPillF) * .22) calc(var(--ogalPillF) * .66);
  border-radius:999px;
  font:700 var(--ogalPillF)/1.3 Cairo,sans-serif;
  font-variant-numeric:tabular-nums;                /* a price is a number, and a
                                                       number must not jitter    */
  white-space:nowrap;
  color:var(--ogalTeal);
  border:1px solid rgba(69,237,237,.45);
  background:rgba(69,237,237,.06);
}
/* `price` is the reference's own case and takes the default outline unchanged;
   it is written out so the attribute is never a silent no-op. */
.ofin .ogal-pill[data-pill="price"]{
  color:var(--ogalTeal);
  border-color:rgba(69,237,237,.45);
  background:rgba(69,237,237,.06);
}
/* `own` is a STATE, not an offer — you already have it, so there is nothing to
   click and nothing to weigh up. Filled and quiet rather than outlined and
   bright, so the eye skips it and lands on the ones that are still for sale. */
.ofin .ogal-pill[data-pill="own"]{
  color:#bff3f7;
  border-color:transparent;
  background:rgba(69,237,237,.16);
}
/* `tier` is a rarity, and rarity on this screen is already gold — the same
   #f0c04f the podium, the medal and the badge use. */
.ofin .ogal-pill[data-pill="tier"]{
  color:var(--ogalGold);
  border-color:rgba(240,196,79,.5);
  background:rgba(240,196,79,.08);
}

/* THE FOOTER — `See all` in muted white with a thin chevron, centred.
   A `<button>`, so it arrives with UA styling that has to be taken off. `all:
   unset` is deliberately NOT used: it would also unset `display`, and every
   property restated below would then be fighting a keyword rather than a value.
   The explicit reset is longer and it is exactly what it says.

   ⚠️ THIS ONE GETS A REAL 32px BOX rather than an invisible expanded area.
   finale.css:283-310 grows its exits with an absolute `::after` because their
   visible pills must not change size (a criterion measures the primary as the
   wider of two). Nothing measures this footer, it spans the case's whole width
   already, and a real 32px band is both the honest target and the composition
   the reference draws. The absolute-`::after` trick also carries a trap here
   that finale.css:1408 records the hard way — an absolutely positioned box that
   passes its ancestor's padding box DOES count toward scrollHeight, and
   criterion 11 measures this case's scrollHeight. A real box cannot do that. */
.ofin .ogal-more{
  flex:none;
  box-sizing:border-box;
  -webkit-appearance:none;
  appearance:none;
  margin:0;
  padding:0 calc(var(--ogalPadIn) * .5);
  width:100%;
  min-height:var(--ogalTap);
  display:flex;
  align-items:center;
  justify-content:center;
  gap:calc(var(--ogalFootF) * .5);
  background:none;
  border:0;
  border-top:1px solid rgba(159,182,191,.14);
  border-radius:0;
  font:700 var(--ogalFootF)/1.3 Cairo,sans-serif;
  color:rgba(234,252,255,.72);
  cursor:pointer;
  text-align:center;
}
.ofin .ogal-more:hover{ color:var(--ogalInk) }
/* the chevron. If gallery.js emits an `<svg>` here it arrives with no stroke of
   its own — the same convention `svg.ofin-see` follows, which finale.css:884
   had to answer or the glyph rendered invisible. `currentColor` means the mark
   and its label brighten together on hover, with one rule instead of two.
   It points DOWN ("there is more below"), which is the one chevron direction
   that is the same in both scripts — so unlike the exit's back-arrow it needs
   no `body.ar` mirror. If the module ships a text glyph instead, this rule
   simply never matches and the label reads normally. */
.ofin .ogal-more svg{
  flex:none;
  width:calc(var(--ogalFootF) * 1.1);
  height:calc(var(--ogalFootF) * 1.1);
  stroke:currentColor;
  fill:none;
  opacity:.85;
}


/* ==========================================================================
   4 · WHAT GOES INSIDE `.ogal-prev` — the game's own renderers, unretouched
   ==========================================================================
   CONTRACT-DOM §6: a preview is built by the game's OWN renderer, never by an
   `<img>` and never by hand-written colour values (criterion 4, and the
   project's standing "previews show the real thing" rule). This file therefore
   gives each renderer a BOX and takes nothing else off it.

   ⚠️ A SIZE THIS FILE CANNOT SET, AND IT IS THE ONE HOOK I AM SHORT.
   The card swatch is sized by `--fcw`, which `cardswatch.js:55-63` reads as a
   var with a 40px fallback and which gallery.js writes INLINE on the wrapper
   (CONTRACT-DOM §3 names it as the one inline style the module may write). An
   inline custom property beats every rule in this file, so I cannot clamp it
   from here. What I can do is publish the number the module should compute it
   from: `--ogalPrevH` is the preview box's own resolved height — declared on
   `.ofin` in section 1 and inherited down to here, so the case's height sum and
   the module read ONE number rather than two copies of one expression — and a
   card that wants to fit inside it wants `--fcw ≈ --ogalPrevH / 1.171` (the
   card's own aspect, cardswatch.js:58). Until the module reads it, the
   `overflow:hidden` on `.ogal-prev` is the guard: an oversized card is clipped
   to the tile rather than escaping onto the felt. */

/* ---- deck: `<span class="otcw otf-sw" style="--fcw:NNpx">` + a `.otcw-c` ----
   ⚠️ THE TWO WRAP CLASSES ARE HANDED OUT AS ONE STRING AND MUST BE APPLIED
   WHOLE (`cardswatch.js:47-49`): `otf-sw` is what admits the card to felt.css's
   paint — its metal, its edge, its ink, its two value chips — and `otcw` is
   what gives it a size. Nothing here touches either. The wrapper is a `<span>`,
   so it is `display:inline` until something says otherwise and its `.otcw-c`
   child (which is `flex:none`, cardswatch.js:57) has no flex container to be an
   item of. One `display:flex` is the whole of what it needs. */
.ofin .ogal-prev .otcw{
  display:flex;
  align-items:center;
  justify-content:center;
  max-width:100%;
  max-height:100%;
}
/* THE FAN — FOUR CARDS, THE SAME GEOMETRY THE ROOM-SETUP TAB ALREADY DRAWS
   (`.om-fan`/`.om-fc`, menu-screen.js:226-234). Owner, 2026-08-23: the tile must
   show the plain, the steel, the gold and the copper card, because a card's tint
   is its LETTER'S VALUE and one card showed one tier. The four letters and the
   faces are `cardswatch.js`'s; this is only the splay, and it is copied rather
   than borrowed from menu-screen.js so the end screen does not inherit that
   file's lobby media queries (`.om-fan{--fcw:22px}` at :890 would have crushed
   the fan here the moment the stage went short).

   ⚠️ THE TILT IS PER CARD AND SYMMETRIC (-12°…+12°), so the OLD wrapper rotate
   is gone — two rotations would have compounded — and with it the `body.ar`
   mirror rule: a symmetric fan has nothing to mirror. `transform-origin:50% 100%`
   pivots each card on its bottom edge, which is what makes it read as a hand
   held rather than four cards leaning. The rotation is on the OUTER `.ogal-fc`
   box, which is the card face itself — the same box `.om-fc` rotates, and the
   skins that fit their letter from measured ink (`GRCARDS.fit`) are re-fitted
   by gallery.js after the markup lands. */
.ofin .ogal-prev .ogal-fan{
  display:flex;
  align-items:center;
  justify-content:center;
  max-width:100%;
  max-height:100%;
}
.ofin .ogal-fc{ transform-origin:50% 100%; box-shadow:0 6px 14px rgba(0,0,0,.5) }
/* the overlap gallery.js divides the tile's width by (`fitCard`, span 1+(n-1)*.58) */
.ofin .ogal-fc + .ogal-fc{ margin-inline-start:calc(var(--fcw) * -.42) }
/* ⚠️ THE FAN MUST SPLAY OUTWARD IN BOTH LANGUAGES, AND `nth-child` CANNOT DO
   THAT ALONE. `.ogal-fan` is a flex row, so under Arabic (`direction:rtl`, read
   off the swatch itself — the fan inherits it) child 1 is the RIGHTMOST card
   while its `rotate(-12deg)` still leans the top LEFT, i.e. INTO the fan: the
   hand closed inward instead of opening, photographed at 2x before this line.
   `--fanS` flips the sign with the reading direction, so the same four rules
   describe one symmetric fan in Arabic and in English.
   (`body.ar`, never `[dir=rtl]` — the pages ship `body dir="ltr"` and switch on
   that class alone; see section 2.) */
.ofin .ogal-prev .ogal-fan{ --fanS:1 }
body.ar .ofin .ogal-prev .ogal-fan{ --fanS:-1 }
.ofin .ogal-fc:nth-child(1){ transform:rotate(calc(-12deg * var(--fanS,1))) translateY(3px); z-index:1 }
.ofin .ogal-fc:nth-child(2){ transform:rotate(calc(-4deg * var(--fanS,1)));  z-index:2 }
.ofin .ogal-fc:nth-child(3){ transform:rotate(calc(4deg * var(--fanS,1)));   z-index:3 }
.ofin .ogal-fc:nth-child(4){ transform:rotate(calc(12deg * var(--fanS,1))) translateY(3px); z-index:4 }
/* «الغامض» draws its own silhouette and a drop shadow under it doubles the ink */
.ofin .ogal-fc.arcane{ box-shadow:none }

/* ---- table: `<span class="ottbl-sw" data-fit="plate|bleed">` ---------------
   🔴 THE ONE RULE THAT MUST NOT BE BROKEN HERE: the swatch paints itself with
   `background:var(--t-surface)` — the SHORTHAND, and only the shorthand
   (`tables/skin.css:333-334` and the warning at :361-370). `--t-surface` is
   whatever skin.js wrote: for a painted table a list of gradients ENDING IN A
   BARE COLOUR, for a photographed one a whole `url(...) center/100% 100%
   no-repeat`. Neither is a valid `<image>`, so `background-image:var(
   --t-surface)` is dropped by the parser — the whole declaration, silently —
   and the swatch renders as a black rectangle. Blue Steel is the table that
   proved it. NOTHING in this file writes a `background` or a `background-image`
   on `.ottbl-sw`, and nothing is layered over it either: the table's own
   surface already carries its weave.

   WHAT IS SAFE TO SET IS ITS BOX, and it needs one — `.ottbl-sw` ships with no
   width, no height and no radius of its own, because every door that mounts it
   (the picker at ~200px, the waiting-room band at 48px) sets its own. The
   swatch's whole recipe is percentage-based for exactly this reason
   (`skin.css:347`: "EVERY inset is a percentage so the same rule reads at 200px
   in the picker and at 48px in the waiting-room band"), so filling the preview
   box edge to edge is honest at any aspect the box happens to have — the room,
   the rail and the mat all re-derive from it. */
.ofin .ogal-prev .ottbl-sw{
  display:block;
  width:100%;
  height:100%;
  border-radius:calc(var(--ogalRadIn) * .62);
}

/* 🔴 NO ROOM AROUND THE TABLE IN THIS TILE — the black frame the owner named,
   2026-08-23: «تصميم الطاولة في حدود سودة وهذا يشيل الشكل الجمالي».
   It was never a colour anybody wrote here: skin.css's `plate` recipe draws the
   mat INSIDE ITS ROOM — the swatch itself is `--t-room` (the wall), `::before`
   is the rail, `::after` is the mat (skin.css:352-372). At 200px in the picker
   that reads as a table seen from across the room, which is exactly what he
   asked for there; in a tile ~300px wide and ~90px tall the wall and the rail
   are almost all of it, so the table arrives as a black border with a sliver of
   felt in the middle. THE PICKER IS UNTOUCHED — this is scoped to `.ogal-prev`.

   Here the MAT IS THE WHOLE SWATCH: the wall is dropped, the rail is dropped,
   and `::after` — which is still skin.css's own `background:var(--t-surface)`
   SHORTHAND, weave and all — is stretched to the box. Nothing in this file
   writes a `background` or a `background-image` on the swatch (section 4's rule,
   and the Blue Steel trap behind it): `background:none` only REMOVES the wall,
   it does not paint. The edge is the tile's own hairline, the one every other
   preview in the case already carries. */
.ofin .ogal-prev .ottbl-sw[data-fit="plate"]{
  background:none;
  box-shadow:none;
}
.ofin .ogal-prev .ottbl-sw[data-fit="plate"]::before{ display:none }
.ofin .ogal-prev .ottbl-sw[data-fit="plate"]::after{
  left:0; right:0; top:0; bottom:0;
  border-radius:inherit;
  /* the table's own key light, kept — only the room's shadow is gone.
     ⚠️ AND THE EDGE IS THE TABLE'S OWN `--t-edge`, NOT THE CASE'S GREY HAIRLINE.
     Dropping the wall also dropped the only contrast the mat had: «الفولاذ
     الأزرق» is #143440 and the tile it now fills is a dark teal panel, so the
     preview read as an EMPTY box — photographed at 2x. Every table declares
     `--t-edge` (skin.css:352) and it is what the game rims that same mat with,
     so the plate gets its own line back rather than one invented here. */
  box-shadow:inset 0 0 0 1px var(--t-edge,rgba(159,182,191,.16)),
             inset 0 0 26px rgba(var(--t-ac-rgb),.12),
             inset 0 -10px 22px -10px rgba(0,0,0,.4);
}
/* a PHOTOGRAPHED mat fills the tile too. `contain` on `--t-room` (skin.css:335)
   is what put a black band above and below it here — right for the picker, where
   he asked to see the whole mat, wrong for a wide short tile whose job is to say
   WHICH table. `cover` on the same photograph, no room colour behind it. */
.ofin .ogal-prev .ottbl-sw[data-fit="bleed"]{
  background-color:transparent;
  background-size:cover;
  box-shadow:inset 0 0 0 1px rgba(159,182,191,.16);
}

/* ---- avatar / crest: the page's own `savHtml(pi)` or felt's `avatarHtml(i)`
   The reference's third tile is "a circular emblem, dark disc … and a ring
   border". `savHtml` returns `<span class="sav">` wrapping an `<img>` or an
   avMedia node (`overtake.html:2666`), and bare `.sav` carries NO size on
   either page — it is only ever sized under `.srow` / `.rrow`
   (overtake.html:458, :492). So the box is set here, and the disc is drawn
   around it rather than on it. */
.ofin .ogal-tile[data-kind="avatar"] .ogal-prev{ border-radius:50% }
.ofin .ogal-tile[data-kind="avatar"] .ogal-prev > *{
  display:block;
  width:min(100%,calc(var(--ogalPrevH) * .92));
  height:min(100%,calc(var(--ogalPrevH) * .92));
  border-radius:50%;
  overflow:hidden;
  box-sizing:border-box;
  border:1px solid rgba(159,182,191,.3);
  background:radial-gradient(70% 70% at 50% 34%,rgba(24,58,71,.9),rgba(9,22,29,.95));
  box-shadow:inset 0 1px 0 rgba(234,252,255,.1),0 6px 16px rgba(0,0,0,.45);
}
/* whatever the renderer put inside, fitted to that disc. `object-position` is
   the same 50%/26% the podium portraits use (felt-final.css:90) — an
   illustrated avatar's subject sits high in its frame, and centring it crops
   the face. */
.ofin .ogal-prev img,
.ofin .ogal-prev svg,
.ofin .ogal-prev video{
  display:block;
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:50% 26%;
}
/* the last defence for a renderer this file has not met: nothing in a preview
   may be wider than the tile that holds it. */
.ofin .ogal-prev > *{ max-width:100% }


/* ==========================================================================
   5 · THE FRIENDS CASE — rows of avatar · name · status · Invite
   ==========================================================================
   Same shell, same header, same footer discipline as the kit case; only the
   body differs. The list is the one shrinkable child, for the reason given in
   section 3.

   ⚠️ A HOOK I DO NOT HAVE, STATED RATHER THAN PAPERED OVER: nothing in this
   file can cap HOW MANY rows arrive. The flex construction guarantees they can
   never overflow the case — `flex:0 1 auto` with `min-height:0` means N rows
   shrink proportionally instead of scrolling, which is what criterion 11's
   `scrollHeight == clientHeight` needs — but shrink is a squeeze, not a
   solution: twenty friends in a 700px case is twenty 30px rows with their
   status lines clipped. The reference shows FOUR rows and a footer that reads
   "4 of 11 friends online", i.e. the case is a sample and the count is the
   truth. That cap belongs to gallery.js, and it is the module builder's call,
   not mine to make in a stylesheet. */
.ofin .ogal-rows{
  flex:1 1 auto;
  min-height:0;
  display:flex;
  flex-direction:column;
  justify-content:flex-start;
}

/* THE ROW. Hairline separators between rows and nowhere else — `+` rather than
   a border on every row, so neither the first row's top nor the last row's
   bottom draws a line into the case's own padding. `border-block-start` rather
   than `border-top`: identical in both directions here, but it keeps every edge
   in this file in one vocabulary, which is how the physical one stops sneaking
   back in. */
.ofin .ogal-row{
  flex:0 1 auto;
  min-height:0;
  box-sizing:border-box;
  display:flex;
  align-items:center;
  gap:calc(var(--ogalPadIn) * .55);
  padding:calc(var(--ogalPadIn) * .55) calc(var(--ogalPadIn) * .2);
  overflow:hidden;
}
.ofin .ogal-row + .ogal-row{
  border-block-start:1px solid rgba(159,182,191,.14);
}
/* offline is not absence — the row still says who and still offers the invite,
   it simply stops claiming they are there. Criterion 8's principle one step
   down: never invent, never hide the truth to make the list look fuller. */
.ofin .ogal-row[data-online="0"]{ opacity:.62 }

/* THE PORTRAIT, AND THE PRESENCE DOT ON ITS RIM.
   ⚠️ `.ogal-av` DELIBERATELY DOES NOT CLIP, and that is the whole shape of this
   rule. The contract puts `.ogal-dot` INSIDE `.ogal-av`, and an
   `overflow:hidden` there would slice the dot off the rim — the identical trap
   finale.css:645-652 records for `.ofin-rank` inside `.ofin-av`, which it
   solved by moving the badge INSIDE the circle. That answer is right for a
   badge carrying a numeral and wrong for a presence dot, which reads as
   presence precisely because it sits ON the edge. So the clip moves one level
   in: the MEDIA is clipped to a circle, the container is not, and the dot
   overhangs freely.
   Criterion 7 measures the circle on `.ogal-av` ("computed radius ≥50% of
   width"), and `border-radius:50%` here answers it whether or not anything
   inside is round. */
.ofin .ogal-av{
  position:relative;
  flex:none;
  width:var(--ogalAv);
  height:var(--ogalAv);
  border-radius:50%;
  box-sizing:border-box;
}
/* everything the renderer put in, clipped to the disc — but never the dot. */
.ofin .ogal-av > :not(.ogal-dot){
  display:block;
  width:100%;
  height:100%;
  border-radius:50%;
  overflow:hidden;
  box-sizing:border-box;
  border:1px solid rgba(159,182,191,.26);
  background:linear-gradient(180deg,#16303a,#08181f);
}
.ofin .ogal-av img,
.ofin .ogal-av svg{
  display:block;
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:50% 26%;                 /* = felt-final.css:90's framing */
}
/* THE DOT. `inset-inline-end` so it crosses to the other rim in Arabic on its
   own — the portrait mirrors with the row, and a dot pinned physically would
   be the one thing left behind. 2% rather than 0: on a circle the box's corner
   is outside the ink, and 2% walks the dot back onto the 45° rim point where
   the reference draws it. The ring around it is the row's own ground, so the
   dot reads as a light on the portrait rather than as a hole punched in it. */
.ofin .ogal-dot{
  position:absolute;
  inset-inline-end:2%;
  bottom:2%;
  width:calc(var(--ogalAv) * .28);
  height:calc(var(--ogalAv) * .28);
  border-radius:50%;
  box-sizing:border-box;
  background:var(--ogalLive);
  box-shadow:0 0 0 2px rgba(13,37,48,.92),0 0 8px rgba(58,210,159,.55);
}
/* offline: the same lamp, unlit. Muted grey-teal and no glow — it is still a
   presence indicator, it is just telling the truth. */
.ofin .ogal-row[data-online="0"] .ogal-dot{
  background:rgba(159,182,191,.55);
  box-shadow:0 0 0 2px rgba(13,37,48,.92);
}

/* NAME OVER STATUS. `min-width:0` on the column is what lets both lines
   ellipsis instead of pushing the Invite pill off the row — a flex item's
   automatic minimum size is its content, and a long Arabic name is exactly the
   content that discovers that. */
.ofin .ogal-who{
  flex:1 1 auto;
  min-width:0;
  display:flex;
  flex-direction:column;
  gap:calc(var(--ogalStF) * .12);
  text-align:start;
}
.ofin .ogal-st{
  min-width:0;
  font:600 var(--ogalStF)/1.35 Cairo,sans-serif;
  color:var(--ogalMute);
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

/* THE INVITE PILL — an ACTION (criterion 9), so unlike `.ogal-pill` it is a
   real target and gets a real 32px box, for the reason given at `.ogal-more`.
   It only ever renders in the `full` fit step — the friends wing is the first
   thing the collapse takes (section 6) — so a 32px row height is never
   competing with a phone's budget. */
.ofin .ogal-inv{
  flex:none;
  box-sizing:border-box;
  -webkit-appearance:none;
  appearance:none;
  margin:0;
  padding:0 calc(var(--ogalPillF) * .78);
  min-height:var(--ogalTap);
  display:inline-flex;
  align-items:center;
  justify-content:center;
  border-radius:999px;
  font:700 var(--ogalPillF)/1.3 Cairo,sans-serif;
  white-space:nowrap;
  color:var(--ogalTeal);
  border:1px solid rgba(69,237,237,.45);
  background:rgba(69,237,237,.06);
  cursor:pointer;
}
.ofin .ogal-inv:hover{
  color:#9df7f7;
  border-color:rgba(69,237,237,.8);
  background:rgba(69,237,237,.13);
}
.ofin .ogal-inv:disabled{
  cursor:default;
  opacity:.5;
}

/* THE FOOT — `4 of 11 friends online`, muted, centred (the reference). Not a
   button: it states a count, it does not do anything. `tabular-nums` because
   both numbers change as friends come and go and a proportional digit set makes
   the line twitch. */
.ofin .ogal-foot{
  flex:none;
  padding-top:calc(var(--ogalPadIn) * .45);
  border-top:1px solid rgba(159,182,191,.14);
  font:600 var(--ogalFootF)/1.4 Cairo,sans-serif;
  font-variant-numeric:tabular-nums;
  color:var(--ogalMute);
  text-align:center;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

/* THE STATED EMPTY STATE — criterion 8: "where a friends graph or presence
   cannot answer, the case must state the truth and offer the one action that
   does work — never a fake friend, never an empty grey box".
   The CSS half of that is a SWITCH, not a decoration: exactly one body renders
   per `data-state`, and the rules below are written so that the fallback of an
   unknown or missing state is the EMPTY body, not the row list. A case that
   cannot say who is online must not be able to fall through to showing rows. */
.ofin .ogal-friends[data-state="ok"] .ogal-empty{ display:none }
.ofin .ogal-friends:not([data-state="ok"]) .ogal-rows,
.ofin .ogal-friends:not([data-state="ok"]) .ogal-foot{ display:none }
/* ⚠️ IT SITS WHERE THE FIRST ROW WOULD HAVE SAT — `flex:0 1 auto`, NOT `1 1 auto`.
   Round 2 gave this block `flex:1 1 auto` + `justify-content:center`, so it
   absorbed the whole case and hung its two children in the middle of it: at
   1920 the header, then ~340px of nothing, then a line and a button floating in
   a tall empty box (`rounds/r02/online-en-1920x1080.png`). The words and the
   door were right; only the place was wrong. A stated empty state is the wing's
   CONTENT, not a placeholder centred in a void — it belongs on the same rhythm
   the populated rows use, reading from the top down.

   THE RHYTHM IS THE ROW'S OWN, to the token. `.ogal-rows` carries no padding of
   its own, so a first `.ogal-row`'s content starts one `--ogalPadIn * .55` below
   the list's top edge; the padding below is that same pair, so the message's
   first line lands exactly where a first friend's name would have. Both bodies
   are direct children of `.ogal` and share its one `--ogalGap * .8` under the
   header, so the switch between them moves nothing.

   ⚠️ AND THE CASE'S HEIGHT IS NOT INVOLVED. `flex:0 1 auto` cannot grow, so the
   remaining height is simply left empty below the block — the case stays the
   `--ogalH` box section 1 derives, which is what keeps criterion 1's measured
   0.00px delta between the two wings true whatever a wing happens to contain.
   Shrinking the case to its content would fix this defect by breaking that one.
   `min-height:0` keeps the shrink half honest for criterion 11 (a long Arabic
   sentence in a short case gives, rather than overflowing) — and the button
   cannot be shrunk away with it, because its `min-height:var(--ogalTap)` below
   is a flex item's used minimum, not a suggestion. */
.ofin .ogal-empty{
  flex:0 1 auto;
  min-height:0;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:flex-start;
  gap:calc(var(--ogalPadIn) * .6);
  padding:calc(var(--ogalPadIn) * .55) calc(var(--ogalPadIn) * .2);
  text-align:center;
  font:600 var(--ogalStF)/1.5 Cairo,sans-serif;
  color:var(--ogalMute);
}
/* ⚠️ THE LINE IS A `<p>`, AND A `<p>` ARRIVES WITH `margin:1em 0` FROM THE UA.
   Left alone that is ~16px of phantom space above the first line and below it —
   the rhythm above measured out in tokens, then thrown off by a default nobody
   wrote. It is reset here rather than absorbed into the padding, so the two
   numbers stay the row's own. `min-width:0` lets a long unbroken name wrap
   instead of widening the case's content box; the module ships `dir="auto"` on
   it, which is what makes a mixed-script sentence read correctly, so this file
   sets no direction of its own. */
.ofin .ogal-msg{
  margin:0;
  min-width:0;
}
/* whatever the module offers as its one working action — `button.ogal-act`, or
   any button or link a future state uses — gets the same treatment the Invite
   pill has, so the empty state is a state of the same case and not a
   different-looking screen. The contract's own class is named FIRST rather than
   left to match the bare `button` selector by luck: a named hook that is a
   silent no-op is how a stylesheet and its module drift apart. */
.ofin .ogal-act,
.ofin .ogal-empty button,
.ofin .ogal-empty a{
  box-sizing:border-box;
  -webkit-appearance:none;
  appearance:none;
  margin:0;
  padding:0 calc(var(--ogalPillF) * .9);
  min-height:var(--ogalTap);
  display:inline-flex;
  align-items:center;
  justify-content:center;
  border-radius:999px;
  font:700 var(--ogalPillF)/1.3 Cairo,sans-serif;
  text-decoration:none;
  white-space:nowrap;
  color:var(--ogalTeal);
  border:1px solid rgba(69,237,237,.45);
  background:rgba(69,237,237,.06);
  cursor:pointer;
}
.ofin .ogal-act:hover,
.ofin .ogal-empty button:hover,
.ofin .ogal-empty a:hover{
  color:#9df7f7;
  border-color:rgba(69,237,237,.8);
  background:rgba(69,237,237,.13);
}
/* `loading` says so by breathing, not by inventing a skeleton of fake names. */
.ofin .ogal-friends[data-state="loading"] .ogal-empty{ animation:gr-ogalWait 1.6s ease-in-out infinite }
@keyframes gr-ogalWait{ 0%,100%{opacity:.5} 50%{opacity:.85} }


/* ==========================================================================
   6 · THE COLLAPSE — stated once in CONTRACT-DOM §5, implemented once here
   ==========================================================================
   ⚠️ `.ofin-opt` HAS NO CSS RULE ANYWHERE IN THIS REPO. It is a free marker
   class, not a behaviour, and a collapse that leaned on it would silently do
   nothing. The contract therefore states the steps itself and this is the
   implementation, keyed on the `data-ogal-fit` attribute gallery.js writes:

     full   gutter ≥ 196px and not ot-tight   both wings
     kit    gutter ≥ 196px and ot-tight       ⚠️ written by the JS, honoured by
                                              nobody — at ot-tight this file
                                              takes BOTH wings down; see below
     none   gutter < 196px, OR ot-tight       neither wing

   The order is the brief's: the friends case goes FIRST because it is the one
   panel on this screen that is about somebody else's session. The kit case
   would survive one step longer — on a phone it is the only thing that says
   what the champion was wearing — IF it could survive READABLY.

   ⚠️ IT CANNOT, AND ROUND 1 SHIPPED A STATED ORDER ITS OWN CSS CONTRADICTED.
   Measured at 844×390, the smallest rendered type inside the kit case is
   6.38px, and it is not a size this file sets: it is the card preview's own
   corner value chip, drawn by `cardswatch.js` and scaled with the card, so no
   floor written in section 1 can reach it. BAR criterion 12 requires 11px.
   CONTRACT-DOM §5 already says what to do about exactly that — "nothing shrinks
   below 11px and no tap target below 32px; below that the step above fires
   instead" — and the only step that holds the floor here is the one where
   nothing renders. So `ot-tight` collapses straight through `kit` to `none`,
   and the table above and the rules below now say the same thing.

   The `kit` rule stays written because gallery.js still writes the attribute
   and a step that is a silent no-op is how the two drifted apart in the first
   place. NOTHING SHRINKS TO STAY: the type floors and `--ogalTap` in section 1
   are hard. Not one of these rules touches the ceremony, the feats bar, the
   places grid or either exit — every selector below is rooted at `.ogal`. */
.ofin[data-ogal-fit="kit"] .ogal-friends{ display:none }
.ofin[data-ogal-fit="none"] .ogal{ display:none }
/* solo has no friends graph to draw on, so the wing is not built at all
   (CONTRACT-DOM §3). This is the belt to that braces: if the marker class is
   present the wing stays down even if a stale node survived a repaint. */
body.ogal-solo .ofin .ogal-friends{ display:none }

/* ---- WHAT A SHORT SCREEN GIVES UP: BOTH WINGS, WHOLE ----------------------
   `body.ot-tight` is felt.js's OWN signal (`felt.js:1931`, stage under 520px
   tall) — the same switch felt-final.css:252-255 and finale.css's short-screen
   block already ride. It is the only correct hook for this and it is used here
   for the reason the header of this file states: a viewport media query would
   be a breakpoint of my invention, and no size or step in this file is allowed
   to come from one.

   ⚠️ THIS REPLACES A WHOLE BLOCK OF SHORT-SCREEN TOKENS ROUND 1 SHIPPED —
   a smaller inset, `--ogalPrevK:.30`, a tighter gap, the tilt off, the blur
   off. Every one of them existed to keep the kit wing alive at 844×390, and
   every one of them is now unreachable, because the wing is not there. Dead
   rules that describe a state the file no longer has are exactly the drift the
   comment above is about, so they are gone rather than left to read as intent.
   What a short screen gives up is not ornament inside a case: it is the case.

   ⚠️ MORE SPECIFIC THAN THE GATE, DELIBERATELY. Section 2's
   `.ofin[data-ogal="on"] .ogal{display:flex}` turns the wings on at (0,3,0);
   this is (0,4,1) and later in the file, so a stale `data-ogal="on"` left by a
   pass that measured a taller stage still cannot bring a wing back on a phone.
   The ceremony, the feats bar and both exits are untouched — this rule reaches
   nothing but `.ogal`. */
body.ot-fin.ot-tight .ofin .ogal{ display:none }

/* ---- THE WORD BREAKDOWN TAKES THE SCREEN ---------------------------------
   `body.ot-fin.ot-det` opens `.ofin-det` (felt.js:2025 / :2247, painted by
   felt-final.css:215-222) — a focused overlay at `width:var(--finW)`, i.e. the
   centre column's width, which leaves both gutters uncovered. Two lit cases
   flanking a modal that is asking for the reader's whole attention is a
   competition the modal should win. They dim rather than disappear, so the
   frame does not jump when it closes, and they stop taking clicks so a stray
   tap in a gutter cannot fire an Invite behind the overlay. */
body.ot-fin.ot-det .ofin .ogal{
  opacity:.22;
  pointer-events:none;
}


/* ==========================================================================
   7 · MOTION
   ==========================================================================
   There is almost none, on purpose: the cases are secondary and a moving
   secondary object is a primary object. The one transition is on the tile's
   own hover, and the one animation is the `loading` breath in section 5.
   A reader who has asked for less motion gets both stilled — which is the
   whole of what this file animates, exactly as finale.css:1188-1194 states for
   its own two. */
.ofin .ogal-tile,
.ofin .ogal-more,
.ofin .ogal-inv{
  transition:background-color .18s ease,border-color .18s ease,color .18s ease;
}
@media(prefers-reduced-motion:reduce){
  .ofin .ogal-tile,
  .ofin .ogal-more,
  .ofin .ogal-inv{ transition:none }
  .ofin .ogal-friends[data-state="loading"] .ogal-empty{ animation:none }
}
