The advert above the lineups, as a stadium perimeter board: 130px tall, six LED rows, three scenarios auto-rotating. It replaces the 38px ticker on desktop only (≥1200px); on mobile nothing changes.
Two groups. The scenario-2 figures live in the
concept-assets/ folder and are
placeholder stock — they go in as a mask-image, so from the photo only the alpha (the shape) is used, never its colours.
The analyst portraits and the operator logo are the very files the analysis page already uses.
keeper-ready.png
keeper-dive.png
run-a.png
run-b.png
shooter-white.png
tipster-2.png
tipster-1.pngLicensing
The five figures are royalty-free stock with anonymous models, downloaded for the prototype. They must be replaced before production with licensed or in-house material. Because they go in as masks, replacing one is a single URL change — it touches no animation and no geometry, as long as the new PNG has a similar aspect ratio and a transparent background.
The same code that runs in the prototype — auto-rotation, identical timings. The board renders at its real width (874px): the ball and keeper positions were measured at that width.
The demo is not showing here. The component is desktop-only (≥1200px) and your window is narrower. Open it on a desktop.
Hovering the board pauses it. The buttons replay one scenario; the rotation carries on normally afterwards.
The board is one container with N scenarios, not N components. The container defines the surface, the pixel grid, the sponsor colour and the rotation clock; each scenario is an independent little film living inside it.
.adled 38px ticker, in exactly the same place: a sibling of
.xiwrap, inside .bo .adledwrap, flush above the lineup. Never a child of .xiwrap — that one is flex-direction:row on desktop, and the board would become a third column between the two teams..ledw-stage is display:none and the existing ticker shows instead.
This is not a responsive version of the same thing — they are two different creatives for two widths. At 390px the board has no room to tell any story.Five layers, in this order from the bottom up. The order is part of the spec, not a detail: the pixel grid must sit above the content.
| # | Layer | Class | Rule |
|---|---|---|---|
| 1 | Surface | .ledw |
130px tall, border-radius:16px 16px 0 0, filled with the sponsor's colour:
linear-gradient(180deg, mix(--bk 88% #000), mix(--bk 55% #000)). The
--bk comes from the Odds CTA's own rules — no new palette. |
| 2 | Scenarios | .ledw-msgs / .ledw-msg |
All in the DOM together, stacked; one visible at a time via clip-path. |
| 3 | Glow on the grass | .ledw-glow |
48px below the board, top:100%, radial from the --bk,
blur 7px, opacity .55. Needs z-index:2 on the stage — the
.xiwrap comes later in the DOM and would otherwise paint over it. |
| 4 | Pixel grid | .ledw-grid |
Two 4px repeating-gradients (the cells) plus one at 21.6px (the row seams = 130÷6). Above the content (z-index:6): the gaps cut into the letters and the figures, so they read as being made of the board, not printed on it. |
| 5 | Sweep | .ledw-sweep |
One diagonal gleam every ~8.5s. Pure atmosphere — it can go without breaking anything. |
Optional, via a class on the stage:
.tilt = rotateX(6deg) with perspective:900px on the parent;
.nodots, .noglow = those layers switch off.
.is-lt on .ledw and
every ink layer flips to dark text — the same rule the Odds CTA follows. The pixel grid does the same, turning from white to black.One JS clock, not one big CSS keyframe. Each scenario has its own internal timeline that restarts from frame 0 every time its turn comes round.
| Step | What happens | Why this way |
|---|---|---|
| Enter/exit | clip-path transition with steps(14), 0.42s. The outgoing scenario gets .is-out, the incoming one .is-on. |
The stepped timing makes it read as a board refreshing column by column, not like a web-page slide. The two directions alternate. |
| Restart | classList.remove('is-on') → void el.offsetWidth →
classList.add('is-on'). |
Without the forced reflow the browser coalesces the two changes into no change and the animations do not restart — you land in the middle of a loop. |
| Durations | {a:9000, f:9000, e:8000} ms, per scenario. |
Duration is a property of the scenario, not a constant of the container — which is why it is a map and not a single number. |
| Pause | .ledw-stage:hover .ledw * →
animation-play-state:paused. |
The only way anyone can study a single frame of a 26s loop. |
prefers-reduced-motion stops the clock.Approved by Vasilis (2026-08-02) out of six that were tried. The other three (photo-save, Golden Sub, Goal) exist in the local concept files and do not ship.
| # | Scenario | Duration | What plays |
|---|---|---|---|
| 1 | Sponsor .m-a | 9s (3×3s) | The operator logo «builds» itself out of flickering pixels → the odd slams in (Ολλανδία — Ιαπωνία · 1 · 1.85) → CTA ΠΑΙΞΕ ΝΟΜΙΜΑ* with the ΕΕΕΠ line. The three scenes cut, they do not fade — it is a stadium board. |
| 2 | The save .m-f | 9s | One continuous passage of play: the forward runs in pushing the ball, plants his foot and shoots; the keeper is on his line from frame 0, crouches and springs; the ball leaves only when the foot strikes it, and it dies in the gloves. Copy: «Θα κρατήσει το μηδέν;» + Goal 1.72 / No Goal 2.05. |
| 3 | The tip .m-e | 8s | On the left the two analysts as LED medallions (their own 3px dot-matrix, denser than the board's) flickering into life, one half a beat after the other; on the right the tip
1 & Over 2.5 @ 3.10; gold pixels fall over the top. |
Each player is the alpha of a photo as a mask-image over a flat colour. The anatomy stays real (these are not stick figures) while the rendering belongs to the board — and the kit is done in CSS.
/* το σχήμα από τη φωτογραφία, το χρώμα από την ομάδα */ .ledwsil{ background:currentColor; -webkit-mask-image:url(concept-assets/keeper-ready.png); mask-image:url(concept-assets/keeper-ready.png); mask-size:contain; mask-repeat:no-repeat; mask-position:center bottom; } /* ΕΜΦΑΝΙΣΙΜΟ = hard-stop gradient που τον κόβει η μάσκα: φανέλα / σορτσάκι / κάλτσες. Τα stops είναι ανά πόζα — κάθε μάσκα έχει δική της γεωμετρία· η βουτιά τρέχει 90deg γιατί το σώμα είναι οριζόντιο. */ .m-f .fr1{ color:#2650C8; /* Ιαπωνία */ background:linear-gradient(180deg,#2650C8 0 50%,#F3F3F3 50% 65%,#2650C8 65% 100%); } .m-f .lp2-dive{ color:#FF6A00; /* Ολλανδία, οριζόντιο σώμα */ background:linear-gradient(90deg,#FF6A00 0 36%,#F3F3F3 36% 54%,#FF6A00 54% 100%); }
The motion is flipbook + travel, which is how a real perimeter board works:
| Figure | Poses | Mechanism |
|---|---|---|
| Forward | run-a ⇄ run-b → shooter-white |
Two side-on strides alternate via steps(1) every 0.3s (~6.6fps) inside a travelling wrapper, with a bob per step; on the plant the shooting pose replaces the strides. |
| Goalkeeper | keeper-ready → keeper-dive |
A side-on ready stance (the same camera angle as the dive — that is what gives the continuity), a small sway, a knee bend, and the dive starts tilted and low and straightens into the stretch. |
The goal: to author scenarios without a deploy — a different creative per sponsor, per sport, per fixture, per campaign. The MVP stays at three hard-coded scenarios; this section is the spec for the next step, so that the MVP does not rule it out.
The model: one scenario = a duration + a list of layers. Each layer has a type, content, a position and a keyframe timeline. The renderer knows nothing about football — it knows layers.
{
"id": "save-clean-sheet",
"duration": 9000,
"sport": "football", // φίλτρο: πού επιτρέπεται
"weight": 3, // βάρος στη ρότα (0 = ανενεργό)
"layers": [
{ "type": "prop", "art": "goal-frame", "at": {"right":14,"top":6,"w":190,"h":118} },
{ "type": "figure", // φιγούρα με μάσκα
"poses": { "idle":"keeper-ready.png", "act":"keeper-dive.png" },
"kit": "$match.home.kit", // binding, όχι σταθερά
"at": {"right":52,"bottom":10,"h":80},
"track": [ {"t":0,"pose":"idle"}, {"t":.25,"crouch":true},
{"t":.30,"pose":"act","x":-38,"y":40,"rot":30},
{"t":.33,"x":0,"y":0,"rot":0} ] },
{ "type": "ball", "track": [ … ], "endsIn": "gloves" },
{ "type": "flash", "at_t": .33 },
{ "type": "copy", "in": .38, "align": "left",
"eyebrow": "Goal / No Goal",
"title": "Θα κρατήσει το μηδέν;",
"odds": [ {"label":"Goal", "value":"$odds.btts.yes"},
{"label":"No Goal","value":"$odds.btts.no"} ] }
]
}
Layer types — the MVP already implements all six, just hard-coded. Phase 2 turns them into data:
| type | What it is | Already exists as |
|---|---|---|
logo | Operator logo with the «switch-on» flicker | .ledw-logo |
copy | Eyebrow + title + subtitle, or odds chips | .lp-tx, .lp2-odds |
odds | A single odd/selection at large size | .ledw-odds |
figure | A figure with poses + kit + track | .ledwsil, .silf |
prop | Goal, ball, substitution board, confetti | .ledw-goal, .ledw-ball, .lp5-conf |
portrait | A face in an LED medallion (the analysts) | .lp5-av |
Bindings — because a scenario is not a picture but a template:
| Token | Supplies |
|---|---|
$match.home.kit / $match.away.kit |
The two kit colours — same scenario, correct teams on every fixture. |
$odds.<market>.<selection> |
The price, from the feed. If it is missing, the chip does not render — never an empty box or a «—». |
$tip.pick / $tip.odd / $tip.analysts[] |
The page's tip and analysts — whatever the verdict box already shows. |
$sponsor |
The operator: fill --bk, logo, is-lt. Already works in the MVP. |
sport / phase (pre-game vs live) / sponsor / campaign dates, ordering by weight. The «Golden Sub» scenario, for instance, only makes sense while that offer is running.DUR map.
background + currentColor on the layer, so that tomorrow the feed can supply them.
.ledw-msg does not know what a «shot» is — it knows «layers that move». Put match logic inside the container and phase 2 gets rewritten from scratch.What else phase 2 will need (out of scope for now): a scenario store plus a simple editor/preview, JSON validation, impression/click tracking per scenario, and a preview at the real 874px width before anything is published.
| Topic | What the component does |
|---|---|
| Motion | prefers-reduced-motion:reduce → all animations are off, the rota stops and the board parks on the CTA scene, fully legible. This is not «less motion», it is none. |
| Flash | The contact flash is 2 frames in ~9s — far below 3 flashes per second (WCAG 2.3.1). Do not repeat it in a new scenario without re-measuring. |
| Text | All copy is real DOM, not images — it translates and it reads. The figures, the goal and the ball are aria-hidden: they are decoration. |
| The link | One <a> wraps the whole board, with an aria-label that states
operator + fixture + odd + «Παίξε νόμιμα» — because the scenes rotate and a screen-reader user never catches one in time. :focus-visible = a gold outline. |
| Contrast | White text only on dark fills; on light ones the .is-ltrule applies. The pixel grid takes ~30% of the luminance off — measure the contrast with the grid on top, not without it. |
transform, opacity and clip-path — composited by the compositor, never touching the main thread.setTimeout per scenario. Nothing runs per frame.loading="lazy" does not help (they are CSS masks) — size them correctly from the start instead.<div class="bo adledwrap"> <a class="adled bo-stoiximan" data-adbrand …>…</a> <!-- mobile ticker, μένει --> <div class="ledw-stage tilt" data-ledw> <!-- desktop board --> <a class="ledw bo-stoiximan" data-ledwbrand href="…" aria-label="…"> <span class="ledw-msgs"> <span class="ledw-msg m-a" data-m="a">…</span> <span class="ledw-msg m-f" data-m="f">…</span> <span class="ledw-msg m-e" data-m="e">…</span> </span> <span class="ledw-grid" aria-hidden="true"></span> <span class="ledw-sweep" aria-hidden="true"></span> </a> <span class="ledw-glow" aria-hidden="true"></span> </div> </div>
bo-* class off the ticker via a MutationObserver, rather than reading the remote itself. That way the board, the pinned banner and the centre-circle mark
cannot disagree. In production, whoever provides the sponsor,
one source — do not add a second..xiwrap loses its top corners.
The board keeps border-radius on top and the pitch becomes
0 0 16px 16px — the two read as one object. This already holds for the ticker; just do not break it.| Item | MVP | Note |
|---|---|---|
| The surface (grid, glow, sweep, tilt, brand fill) | YES | — |
| The 3 scenarios + auto-rotation | YES | Hard-coded, each with its own timeline. |
Sponsor from a single source, is-lt |
YES | Already works. |
| Odds/tip from the feed | PARTLY | In the MVP the prices sit in the markup. Feed them from the same model that feeds the verdict box — we do not want the board quoting a different price from the card. |
| Configurable scenarios (JSON, editor, targeting) | NO | §05 — phase 2. |
| A mobile version of the board | NO | The ticker stays as it is below 1200px. |
| Licensed photographic material | NO | Placeholder stock — a URL change, see Assets. |
Foxbet · Analysis page · LED wall. Concept — pending sign-off. The scenarios are hard-coded in the MVP; configurability (§05) is phase 2 and the MVP must not rule it out.