ΕΛEN
Foxbet · Component handover

Ενδεκάδες — the pitch

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.

Implemented · pre-game + live Measured 2026-08-02 · 1440px / 390px Data: demo, seeded from the shirt number

Assets used

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.

Note

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.

Live demo — both breakpoints

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.

Desktop · real 1440px viewport (874px card)

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.

Mobile · 390px

A single vertical pitch; the two benches go underneath.

00Read this first

Two decisions that define the component, and neither is stylistic:

1Numbers, not photos. The old lineup put the same stock photo on all 22 players. Nobody licenses 22 portraits per match, so the shirt number in a circle is the only honest way to do it — and it is two elements per player instead of three, which is where most of the height saving comes from.
2The green is --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.
ℹ️DOM order does the work of orientation. The home team is written 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.

01Anatomy measured 2026-08-02

PartClassDesktop (1440)Mobile (390)Rule
Card.xiwrap874 × 428390 × 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.xihd437 × 32390 × 32 flag 20×14 · name 13/700 · status · formation on the right, in --num.
Pitch.xipitch437 × 258390 × 243 Background --win, padding 4px 10px (desktop) / 10px 4px (mobile).
Rows.lineup / .pline row / columncolumn / row The same DOM: on desktop the row becomes a column. min-height 250 / 210.
Player.plcircle 32 · box 48 tall sameCircle + name, gap 3px. The name truncates with an ellipsis and never wraps.
Number.pl-no32 × 32, --num 13/700 home white · away --gold
Bench.xisubs437 × 138full width Two columns, zebra striping per pair of rows (4n+1, 4n+2).
On desktop the .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.

02Formation — from order, not from coordinates

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.

ℹ️Which is why there is no «4-2-3-1 layout» setting. The formation in the header (.xihd-f) is a label; what actually draws is the rows. If the two disagree, the feed is wrong.

03The player card — two behaviours

Desktop (≥1200 & hover)Mobile / touch
Triggerhover = preview · click = pin tap = sheet from the bottom, with a scrim
Positionposition:fixed, above the player; if it does not fit it flips belowpinned to the bottom; CSS sets the position — the JS does not compute left/top
Interactionpointer-events:none while it is a preview; it becomes auto only once pinnedalways interactive, with a ✕ and a scrim

Content per position — 2–4 stats and one suggestion:

PositionStatsSuggestion
GoalkeeperAppearances · Saves/match · Clean sheets · Save %Clean sheet
DefenderAppearances · Tackles/match · Clearances · Duels2+ tackles
MidfielderAppearances · Passes · Key passes · Shots on target1+ assist or 1+ shot on target
ForwardAppearances · Goals · Goals/90′ · Shots · xGTo score
«Goals / 90′» is DERIVED, not random. It is computed γκολ ÷ συμμετοχές 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.
ℹ️The demo numbers are deterministic. 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.
The suggestion is deliberately not dressed as an Odds CTA. On purpose: in production this slot hosts the real component, and a look-alike here would be a second source of truth.

04The pulse — how the user learns they are tappable

There is no instruction strip above the pitch any more. One player beats like a heart and that is the whole invitation.

ElementBehaviour
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 lasts20 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.
On prefers-reduced-motion the pulse becomes a static ring for the same 20s: the «tap here» signal stays, the motion goes.

05The data model it needs

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 ⇒ ΚΑΝΕΝΑ σήμα στο κέντρο
}
The bench is not capped at 8. The demo has 8+8 because that is what the UEFA team sheet carries; the grid is two columns with zebra striping per pair, so it takes any number. Do not truncate the list in the UI — if the feed gives 12, show 12.

06Easy mistakes

07Accessibility

TopicWhat holds
ContrastNames on --win 5.41:1 · ink on a white circle 17.40 · ink on gold 9.63. All ≥ AA.
Team ≠ colour aloneWhite 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 targetThe circle is 32px but the target is the whole .pl (48px tall) — and on mobile the player takes the width of its row.
MotionThe 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.

08Where it runs & what surrounds it

PageState
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.

Live: pre-game · live. Open the lineups and hover/tap a player.

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.