The 11 players of each team on a pitch, with the bench underneath. One markup for both breakpoints: on desktop two half-pitches side by side, on mobile a single vertical pitch where the two halves join up. Every player opens a card with stats and one suggestion.
The pitch is almost entirely CSS — the grass, the lines, the box, the arc and the corners are borders and gradients, not images. Images appear in only two places: each team's flag/crest in the header, and the sponsor mark at the centre.
flagcdn · nl.png
flagcdn · jp.pngNote
The flags are placeholders from flagcdn — in production the team crest comes from the feed, in the same 20×14 box. The sponsor mark is always the
circle-largecut: it is a round mark at the centre of the pitch, not a rectangular logo. With no sponsor nothing is placed — not even a mark of our own.
The real code, in two iframes at the widths that were measured. Try hover on a player on desktop and tap on mobile — these are two different behaviours, not the same card at two sizes.
Two halves side by side, each attacking towards the centre. Shown at 72% scale so it fits the page — the dimensions in the §01 table are the real ones.
A single vertical pitch; the two benches go underneath.
Two decisions that define the component, and neither is stylistic:
--win (#027A48), not #039855. White names on the old green measure 3.73:1 and fail AA; on
--win they measure 5.41. The circles: ink on white 17.40, ink on gold 9.63.GK→DEF→MID→FWD and the away team
FWD→MID→DEF→GK. That way both halves attack inwards with no reversal in the markup and no flex-direction:row-reverse anywhere.| Part | Class | Desktop (1440) | Mobile (390) | Rule |
|---|---|---|---|---|
| Card | .xiwrap | 874 × 428 | 390 × 827 | overflow:hidden + border-radius. This is why the player card is position:fixed on <body> — inside here it would be clipped. |
| Team | .xiteam[data-side] | flex column | display:contents |
On mobile the team disappears as a box and its children join the same grid as order, so the two halves merge into one pitch. |
| Header | .xihd | 437 × 32 | 390 × 32 | flag 20×14 · name 13/700 · status · formation on the right, in
--num. |
| Pitch | .xipitch | 437 × 258 | 390 × 243 | Background --win,
padding 4px 10px (desktop) / 10px 4px (mobile). |
| Rows | .lineup / .pline |
row / column | column / row |
The same DOM: on desktop the row becomes a column. min-height 250 /
210. |
| Player | .pl | circle 32 · box 48 tall | same | Circle + name, gap 3px. The name truncates with an ellipsis and never wraps. |
| Number | .pl-no | 32 × 32, --num 13/700 |
home white ·
away --gold | |
| Bench | .xisubs | 437 × 138 | full width | Two columns, zebra striping per pair of rows (4n+1, 4n+2). |
.pline needs
align-items:center and flex:0 0 auto on the player. Without them the column inherits flex-start and the boxes, whose width comes from the length of the name (measured 50/53/45/32px), no longer share an axis — the column reads crooked.There are no x/y anywhere. A formation is a list of rows and a player's position follows from the row they belong to.
// 4-3-3 γηπεδούχος: GK → DEF → MID → FWD <div class="lineup"> <div class="pline">…1 GK…</div> <div class="pline">…4 DEF…</div> <div class="pline">…3 MID…</div> <div class="pline">…3 FWD…</div> </div> // φιλοξενούμενη 4-3-3: η ΙΔΙΑ λίστα ανάποδα — FWD → MID → DEF → GK
The spacing inside a row is justify-content:space-around, so every formation works with no new CSS: 4-4-2, 3-5-2, 5-3-2, 4-2-3-1 — the feed only has to supply the rows. The position (GK/DEF/MID/FWD) the card needs comes from
the row index, not from a data field.
.xihd-f) is a label; what actually draws is the rows. If the two disagree, the feed is wrong.| Desktop (≥1200 & hover) | Mobile / touch | |
|---|---|---|
| Trigger | hover = preview · click = pin | tap = sheet from the bottom, with a scrim |
| Position | position:fixed, above the player; if it does not fit it
flips below | pinned to the bottom; CSS sets the position — the JS does not compute left/top |
| Interaction | pointer-events:none while it is a preview; it becomes auto only once pinned | always interactive, with a ✕ and a scrim |
Content per position — 2–4 stats and one suggestion:
| Position | Stats | Suggestion |
|---|---|---|
| Goalkeeper | Appearances · Saves/match · Clean sheets · Save % | Clean sheet |
| Defender | Appearances · Tackles/match · Clearances · Duels | 2+ tackles |
| Midfielder | Appearances · Passes · Key passes · Shots on target | 1+ assist or 1+ shot on target |
| Forward | Appearances · Goals · Goals/90′ · Shots · xG | To score |
γκολ ÷ συμμετοχές from the two rows above it. Feed it from the same random generator and the card contradicts itself (10 goals in 24 appearances next to «0.72 / 90′») — and dividing two numbers on screen is the first thing a reader does.seeded(no,i) = frac(sin(no·37.7 + i·11.3)·10⁴) — the same player always shows the same numbers, while no two players look alike. In production all of this comes from the feed; the formula goes away.There is no instruction strip above the pitch any more. One player beats like a heart and that is the whole invitation.
| Element | Behaviour |
|---|---|
| Which player | The forward with the shortest «to score» price — computed with the same formula that prices his own card, so the two can never disagree. In the demo data: Γκάκπο, №10, @2.32. |
| When it starts | On IntersectionObserver (threshold .25) — when the pitch becomes visible, not on load: the lineups sit deep in the page and a timer started at load would have expired before the user got there. |
| How long it lasts | 20 seconds, or until the first tap on any player — at that point the hint has done its job. |
| What you see | A double beat on the circle (scale 1→1.24→1→1.15) and two rings
travelling outwards, one white and one gold half a beat later. |
prefers-reduced-motion the pulse becomes
a static ring for the same 20s: the «tap here» signal stays, the motion goes.Everything the component draws, with field names. Anything missing must not render, rather than become an empty box.
{
"status": "probable" | "confirmed", // → το pill «Πιθανές/Επιβεβαιωμένες»
"home": {
"name": "Ολλανδία",
"crest": "…/nl.png", // 20×14, cover
"formation": "4-3-3", // ΕΤΙΚΕΤΑ — δεν ζωγραφίζει
"lines": [ // ΑΥΤΟ ζωγραφίζει· GK πρώτα
[ {"no":1, "name":"Φερμπρούχεν"} ],
[ {"no":22, "name":"Ντάμφρις"}, … ],
…
],
"bench": [ {"no":13, "name":"Φλέκεν"}, … ],
"stats": { "10": { "apps":24, "goals":9, "xg":0.41, … } },
"props": { "10": { "market":"Να σκοράρει", "odd":2.32, "url":"/go/…" } }
},
"away": { … },
"sponsor": "stoiximan" | null // null ⇒ ΚΑΝΕΝΑ σήμα στο κέντρο
}
.xiwrap. It is
position:fixed on <body>, one node for all players. The .xiwrap is overflow:hidden — a child of it would be clipped. (The same trap hit the form-bubbles popovers.)z-index on .xipitch. It would create a stacking context and trap anything that has to escape above it. What is raised is the row
(.pline{z-index:2}), not the pitch.overflow:hidden; together they read as a single mark on the halfway line.
With no sponsor it hides completely and gives its space back..xiteam is display:contents. Give it a box again and the home bench lands between the two halves and cuts the pitch in half — which is exactly what happened before the order.| Topic | What holds |
|---|---|
| Contrast | Names on --win 5.41:1 · ink on a white circle
17.40 · ink on gold 9.63. All ≥ AA. |
| Team ≠ colour alone | White vs gold separates the teams, but it is not the only carrier: a header with flag and name sits above each half, and the halves are spatially separate. |
| Touch target | The circle is 32px but the target is the whole
.pl (48px tall) — and on mobile the player takes the width of its row. |
| Motion | The pulse is disabled under prefers-reduced-motion and a static ring remains. |
| Keyboard | Outstanding The players are
<div> with hover/click. Production needs
tabindex="0", role="button", activation on Enter/Space and Esc to close. It is the component's only known gap. |
| Page | State |
|---|---|
foxbet-responsive.html — pre-game |
pill = «Πιθανές» (data-xi="prob") |
foxbet-responsive-live.html — live |
pill = «Επιβεβαιωμένες» (data-xi="conf"). Nothing else changes. |
Above it sits the ad slot — today the 38px ticker, with the LED wall as the desktop concept. The two are joined: the board keeps the top corners and the
.xiwrap becomes 0 0 16px 16px, so they read as one object. Do not round both.
Foxbet · Analysis page · Ενδεκάδες. Implemented on both analysis pages; measured in the browser (1440px / 390px) on 2026-08-02. Player stats and prices are demo data — in production they come from the feed.