ΕΛEN
Foxbet · Component handover

LED wall — pitch-side advertising board

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.

Analysis page · Ενδεκάδες Concept · pending sign-off Desktop only ≥1200px Configurable scenarios = NOT MVP

Assets used

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.

Licensing

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.

Live demo

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.

00Read this first

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.

ℹ️Where it goes. It replaces the .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.
Desktop only. Below 1200px the .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.
The scenarios are hard-coded in the MVP. Making them configurable — authoring our own scenarios without a deploy — is phase 2 and is specified in §05. Do not build it in the MVP, but do not close the door on it: §05 names the three decisions that would cost us.

01The surface — anatomy & tokens

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.

#LayerClassRule
1Surface.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.
2Scenarios.ledw-msgs / .ledw-msg All in the DOM together, stacked; one visible at a time via clip-path.
3Glow 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.
4Pixel 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.
5Sweep.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.

The «view from the stands» is, in order of importance: the glow falling on the grass, the sweep passing over, and only then the tilt. The tilt on its own reads as a bug; it is the glow that puts you in the spectator's seat.
Light operators. On light fills (interwetten, pamestoixima, winmasters) the .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.

02The rotation — model & timing

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.

StepWhat happensWhy 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.offsetWidthclassList.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.
ℹ️The rotation never stops on its own. A manual replay (dev panel) restarts the cycle from that scenario — it does not park. Only prefers-reduced-motion stops the clock.

03The three MVP scenarios

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.

#ScenarioDurationWhat plays
1Sponsor .m-a9s (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.
2The save .m-f9s 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.
3The tip .m-e8s 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.
Kit rule: red = our team, white = the opponent, blue = the keeper. In scenario 2 of the Ολλανδία–Ιαπωνία page: Ιαπωνία (blue) shoots, Ολλανδία (orange) keeps the clean sheet, because the page's tip is «1».

04The figures — masks, not photos

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:

FigurePosesMechanism
Forwardrun-arun-bshooter-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.
Goalkeeperkeeper-readykeeper-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 ball's positions are measured, not «a nice arc». The keyframe at 33% is the gloves at the 874px width. If the goal, the keeper or the board's width changes, measure again — otherwise the flash goes off in mid-air and the passage reads as a goal.

05Configurability — scenarios of our own NOT MVP · phase 2

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:

typeWhat it isAlready exists as
logoOperator logo with the «switch-on» flicker.ledw-logo
copyEyebrow + title + subtitle, or odds chips.lp-tx, .lp2-odds
oddsA single odd/selection at large size.ledw-odds
figureA figure with poses + kit + track.ledwsil, .silf
propGoal, ball, substitution board, confetti.ledw-goal, .ledw-ball, .lp5-conf
portraitA face in an LED medallion (the analysts).lp5-av

Bindings — because a scenario is not a picture but a template:

TokenSupplies
$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.
ℹ️Rota & targeting (phase 2). The page asks «give me scenarios for this context» and gets a list back: filter on 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.
The three decisions that would close the door — these are the only ones we ask the MVP to honour:
1. Do not put the timings inside one monolithic keyframe. Each scenario keeps its own timeline and its own duration in DUR map.
2. Do not hard-code the kit colours inside the masks. They are background + currentColor on the layer, so that tomorrow the feed can supply them.
3. Keep the renderer indifferent to the content. The .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.

06Accessibility

TopicWhat the component does
Motion prefers-reduced-motion:reduceall 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.

07Performance

08Markup & where it hooks in

<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>
ℹ️The sponsor is not resolved a second time. In the prototype the board copies the 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.
The .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.

09What is MVP and what is not

ItemMVPNote
The surface (grid, glow, sweep, tilt, brand fill) YES
The 3 scenarios + auto-rotation YESHard-coded, each with its own timeline.
Sponsor from a single source, is-lt YESAlready 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 NOThe ticker stays as it is below 1200px.
Licensed photographic material NOPlaceholder stock — a URL change, see Assets.
Live prototype: foxbet-analysis.pages.dev/ledwall (and the analysis pages: pre-game · live).

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.