Overview

Column

Career scoreboard

Career final score
LM
MESSI
936
GOALS
VS
CR
RONALDO
981
GOALS
427 ASSISTS 261
1,363 G+A 1,242

Key performance indicators

Lionel Messi Cristiano Ronaldo
Total goals
936
vs
981
Total assists
427
vs
261
Appearances
1,182
vs
1,342
Goal contributions (G+A)
1,363
vs
1,242
Goals per match
0.79
vs
0.73
Assists per match
0.36
vs
0.19
Gold underline = higher value. Per-match figures are recalculated from the raw career totals.

Column

Goals comparison

Assists comparison

Messi

Column

Player profile

LM
PLAYER PROFILE
LIONEL MESSI
Argentina
Career totals from the dataset
936 GOALS
427 ASSISTS
1,182 APPEARANCES

Trophy cabinet

Ballon d'Or awards
awards through 2025
8
129
Champions League goals
21
World Cup goals
126
International goals
0.87
Goals per 90
0.40
Assists per 90
103.9
Minutes per goal

Column

Goals and assists: club vs international

How Messi compares with Ronaldo (leader = 100)

Ronaldo

Column

Player profile

CR
PLAYER PROFILE
CRISTIANO RONALDO
Portugal
Career totals from the dataset
981 GOALS
261 ASSISTS
1,342 APPEARANCES

Trophy cabinet

Ballon d'Or awards
awards through 2025
5
140
Champions League goals
11
World Cup goals
146
International goals
0.80
Goals per 90
0.21
Assists per 90
111.8
Minutes per goal

Column

Goals and assists: club vs international

How Ronaldo compares with Messi (leader = 100)

Match Centre

Column

Match stats (who wins each category?)

Messi Ronaldo
936 Goals 981
427 Assists 261
1,363 Goals + assists 1,242
1,182 Appearances 1,342
0.79 Goals per match 0.73
1.26 G+A per 90 minutes 1.02
103.9 Minutes per goal (lower is better) 111.8
126 International goals 146
129 Champions League goals 140
21 World Cup goals 11
8 Ballon d'Or awards 5

Column

Player profile radar (leader of each metric = 100)

Performance

Column

Overall performance (each metric indexed: the leader = 100)

Column

Big-stage numbers

Club vs International

Column

Goals: club vs international

Assists: club vs international

Column

Output per match: club vs international

Reading this page

  • No time series in the data. The CSV holds career totals only (no seasons or years), so a line chart of goals and assists over time would require data that does not exist. This page shows the club/international split instead.
  • Club = career total minus international. This assumes the career totals include national-team matches; the code checks that international figures never exceed the totals.
  • Per match = goals (or assists) divided by appearances in that setting.

Clubs

Column

Messi: club by club

LIONEL MESSI
Argentina | 8 clubs | 813 club goals | 996 club appearances
GRA
Grandoli 1992–1995
Argentina | Youth academy
n/a GOALS
n/a ASSISTS
n/a APPS
n/a GOALS / MATCH
n/a G+A / MATCH
NOB
Newell's Old Boys 1995–2000
Argentina | Youth academy
n/a GOALS
n/a ASSISTS
n/a APPS
n/a GOALS / MATCH
n/a G+A / MATCH
🏆 Youth league titles
BAY
FC Barcelona Youth 2000–2003
Spain | Youth academy
n/a GOALS
n/a ASSISTS
n/a APPS
n/a GOALS / MATCH
n/a G+A / MATCH
🏆 Youth league titles
BAC
FC Barcelona C 2003–2004
Spain | Tercera División
5 GOALS
n/a ASSISTS
10 APPS
0.50 GOALS / MATCH
n/a G+A / MATCH
BAB
FC Barcelona B 2004–2005
Spain | Segunda División
6 GOALS
n/a ASSISTS
22 APPS
0.27 GOALS / MATCH
n/a G+A / MATCH
FCB
FC Barcelona 2004–2021
Spain | La Liga
672 GOALS
305 ASSISTS
778 APPS
0.86 GOALS / MATCH
1.26 G+A / MATCH
🏆 10 La Liga 🏆 7 Copa del Rey 🏆 4 Champions League 🏆 3 Club World Cup 🏆 3 UEFA Super Cup 🏆 8 Supercopa de España
PSG
Paris Saint-Germain 2021–2023
France | Ligue 1
32 GOALS
35 ASSISTS
75 APPS
0.43 GOALS / MATCH
0.89 G+A / MATCH
🏆 2 Ligue 1 🏆 1 Trophée des Champions
MIA
Inter Miami 2023–present
USA | MLS
98 GOALS
66 ASSISTS
111 APPS
0.88 GOALS / MATCH
1.48 G+A / MATCH
🏆 Leagues Cup 2023 🏆 Supporters' Shield 2024 🏆 MLS Cup 2025

Column

Ronaldo: club by club

CRISTIANO RONALDO
Portugal | 9 clubs | 836 club goals | 1,109 club appearances
AND
Andorinha 1992–1995
Portugal | Youth academy
n/a GOALS
n/a ASSISTS
n/a APPS
n/a GOALS / MATCH
n/a G+A / MATCH
NAC
CD Nacional 1995–1997
Portugal | Youth academy
n/a GOALS
n/a ASSISTS
n/a APPS
n/a GOALS / MATCH
n/a G+A / MATCH
SPY
Sporting CP Youth 1997–2002
Portugal | Youth academy
n/a GOALS
n/a ASSISTS
n/a APPS
n/a GOALS / MATCH
n/a G+A / MATCH
SPB
Sporting CP B 2001–2002
Portugal | Reserve team
n/a GOALS
n/a ASSISTS
n/a APPS
n/a GOALS / MATCH
n/a G+A / MATCH
SCP
Sporting CP 2002–2003
Portugal | Primeira Liga
5 GOALS
6 ASSISTS
31 APPS
0.16 GOALS / MATCH
0.35 G+A / MATCH
🏆 Supertaça 2002
MUN
Manchester United (1st spell) 2003–2009
England | Premier League
118 GOALS
64 ASSISTS
292 APPS
0.40 GOALS / MATCH
0.62 G+A / MATCH
🏆 3 Premier League 🏆 1 Champions League 🏆 1 FA Cup 🏆 2 League Cup 🏆 1 Club World Cup
RMA
Real Madrid 2009–2018
Spain | La Liga
450 GOALS
131 ASSISTS
438 APPS
1.03 GOALS / MATCH
1.33 G+A / MATCH
🏆 2 La Liga 🏆 4 Champions League 🏆 2 Copa del Rey 🏆 3 Club World Cup 🏆 2 UEFA Super Cup 🏆 2 Supercopa de España
JUV
Juventus 2018–2021
Italy | Serie A
101 GOALS
22 ASSISTS
134 APPS
0.75 GOALS / MATCH
0.92 G+A / MATCH
🏆 2 Serie A 🏆 1 Coppa Italia 🏆 2 Supercoppa Italiana
MUN
Manchester United (2nd spell) 2021–2022
England | Premier League
27 GOALS
6 ASSISTS
54 APPS
0.50 GOALS / MATCH
0.61 G+A / MATCH
NAS
Al Nassr 2023–present
Saudi Arabia | Saudi Pro League
135 GOALS
28 ASSISTS
160 APPS
0.84 GOALS / MATCH
1.02 G+A / MATCH
🏆 Arab Club Champions Cup 2023 🏆 Saudi Pro League 2025/26

Club Stats

Column

Messi: goals and assists by club

Ronaldo: goals and assists by club

Column

Scoring rate by club (goals per match)

Club-by-club details

Pitch Heatmap

Column

Messi: where he operates on the pitch

LIONEL MESSI
ATTACKING DIRECTION →
Starts as a right winger cutting inside, later a roaming false nine between the lines.
Less time More time
Illustrative heatmap: the dataset has no tracking data, so the zones are drawn from the player's usual role at each club. Pick a club above.

Column

Ronaldo: where he operates on the pitch

CRISTIANO RONALDO
ATTACKING DIRECTION →
Wide attacker who beats defenders on the flank and cuts in to shoot.
Less time More time
Illustrative heatmap: the dataset has no tracking data, so the zones are drawn from the player's usual role at each club. Pick a club above.

Efficiency

Column

Scoring and creative efficiency (per match and per 90 minutes)

Column

Minutes needed per goal contribution (lower is better)

How to read these metrics

  • Per match divides output by appearances. It rewards consistency but treats a 10-minute cameo like a full game.
  • Per 90 divides by minutes played and multiplies by 90, so it adjusts for different amounts of playing time.
  • Minutes per goal / assist / G+A is the time needed for one contribution. Lower is better.
  • Data check: 10 of 10 pre-computed statistics in the CSV match values recalculated from the raw totals (within rounding).

Data

Column

Detailed statistics

Column

Validation of pre-computed statistics

About the data

  • File: messi_vs_ronaldo_comparison_dataset.csv
  • Format: wide - one row per statistic, one column per player (15 statistics).
  • Scope: career totals only; there are no seasons, years or competitions by season.
  • Clubs pages: club-by-club figures are typed into club_tbl in the setup chunk (not in the CSV); they are senior-team, all-competition numbers and approximate for current clubs (Inter Miami, Al Nassr). Edit that table to update them.
  • Recalculated: per-match, per-90 and minutes-per metrics are derived from the raw totals.
  • Validation: 10 of 10 pre-computed statistics in the CSV match values recalculated from the raw totals (within rounding).
  • Photos: Lionel Messi: no photo found (avatar shown); Cristiano Ronaldo: no photo found (avatar shown). To add photos, save images/messi.jpg and images/ronaldo.jpg next to this file and knit again.
---
title: "Messi vs Ronaldo | Career Analytics"
output:
  flexdashboard::flex_dashboard:
    orientation: columns
    vertical_layout: fill
    source_code: embed
---

```{r setup, include=FALSE}
# ============================================================================
# SETUP, DATA LOADING, CLEANING AND DERIVED METRICS
# ============================================================================
library(flexdashboard)
library(tidyverse)   # dplyr, readr, ggplot2, tidyr, purrr, stringr
library(plotly)
library(DT)
library(scales)
library(htmltools)

knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE)

# ---- Football theme palette (dark pitch green, chalk white, trophy gold) ----
col_bg     <- "#0B1A16"
col_panel  <- "#10261F"
col_gold   <- "#F2C14E"
col_text   <- "#F3F7F4"
col_muted  <- "#9DB8AC"
col_club   <- "#3F8F6B"                       # grass green for "club" splits
player_cols <- c(Messi = "#6CC3F5", Ronaldo = "#FF4D5A")
players     <- c("Messi", "Ronaldo")
font_stack  <- "Barlow Condensed, Inter, Segoe UI, Helvetica, Arial, sans-serif"

# ---- 1. Locate and load the CSV --------------------------------------------
# Looks in the working directory for e.g. messi_vs_ronaldo_comparison_dataset.csv
# or messi_vs_ronaldo_comparison_dataset(1).csv
candidate_files <- list.files(pattern = "messi.*ronaldo.*\\.csv$", ignore.case = TRUE)
if (length(candidate_files) == 0) {
  stop("Dataset not found. Save the CSV in the same folder as this .Rmd file ",
       "(current working directory: ", getwd(), ").")
}
data_file <- candidate_files[1]
raw <- readr::read_csv(data_file, show_col_types = FALSE, trim_ws = TRUE)

# ---- 2. Detect the structure (wide format: one row per statistic) ----------
nm <- tolower(names(raw))
messi_col   <- names(raw)[str_detect(nm, "messi")][1]
ronaldo_col <- names(raw)[str_detect(nm, "ronaldo")][1]
metric_col  <- names(raw)[str_detect(nm, "categor|metric|measure")][1]
unit_col    <- names(raw)[str_detect(nm, "^unit")][1]
if (is.na(metric_col)) metric_col <- names(raw)[1]
if (is.na(messi_col) || is.na(ronaldo_col)) {
  stop("Could not find one column for Messi and one for Ronaldo. Columns found: ",
       paste(names(raw), collapse = ", "))
}

# ---- 3. Clean: types, blanks, duplicates -----------------------------------
dat <- tibble(
  metric  = str_squish(as.character(raw[[metric_col]])),
  unit    = if (is.na(unit_col)) "" else as.character(raw[[unit_col]]),
  Messi   = parse_number(as.character(raw[[messi_col]])),
  Ronaldo = parse_number(as.character(raw[[ronaldo_col]]))
) %>%
  mutate(unit = replace_na(unit, "")) %>%
  filter(!is.na(metric), metric != "") %>%
  distinct(metric, .keep_all = TRUE)          # one row per statistic: no double counting

# ---- 4. Helpers --------------------------------------------------------------
# Number formatting (scalar)
fmt <- function(x, digits = 0) {
  if (length(x) == 0 || is.na(x)) "n/a" else scales::comma(x, accuracy = 10^(-digits))
}

# Decimals to show for a statistic: whole numbers -> 0, "minutes per" -> 1, else 2
row_digits <- function(metric, a, b) {
  vals <- c(a, b)
  is_int <- all(abs(vals - round(vals)) < 1e-9, na.rm = TRUE)
  if (is_int) 0 else if (str_detect(tolower(metric), "minutes per")) 1 else 2
}

# For these statistics a LOWER value is better
lower_better <- function(metric) str_detect(tolower(metric), "minutes per")

dat <- dat %>% mutate(digits = pmap_dbl(list(metric, Messi, Ronaldo), row_digits))

# Look up one raw value from the CSV (NA if that statistic is not in the file)
val <- function(metric, player) {
  x <- dat[[player]][tolower(dat$metric) == tolower(metric)]
  if (length(x) == 0) NA_real_ else x[1]
}

# ---- 5. Derived metrics, recalculated from the raw totals ------------------
build_stats <- function(p) {
  g     <- val("Goals", p)
  a     <- val("Assists", p)
  apps  <- val("Appearances", p)
  mins  <- val("Minutes played", p)
  ig    <- val("International goals", p)
  ia    <- val("International assists", p)
  iapps <- val("International appearances", p)
  tibble(
    player = p,
    goals = g, assists = a, apps = apps, minutes = mins, ga = g + a,
    g_per_app = g / apps, a_per_app = a / apps, ga_per_app = (g + a) / apps,
    g_per90 = g / mins * 90, a_per90 = a / mins * 90, ga_per90 = (g + a) / mins * 90,
    min_per_goal = mins / g, min_per_assist = mins / a, min_per_ga = mins / (g + a),
    intl_goals = ig, intl_assists = ia, intl_apps = iapps,
    club_goals = g - ig, club_assists = a - ia, club_apps = apps - iapps,
    club_g_per_app = (g - ig) / (apps - iapps), intl_g_per_app = ig / iapps,
    club_a_per_app = (a - ia) / (apps - iapps), intl_a_per_app = ia / iapps,
    cl_goals = val("Champions League goals", p),
    wc_goals = val("World Cup goals", p),
    ballon   = val("Ballon d'Or awards", p)
  )
}
S <- bind_rows(build_stats("Messi"), build_stats("Ronaldo"))

sv  <- function(col, p) S[[col]][S$player == p]                  # one derived value
has <- function(...) all(!is.na(unlist(S[, c(...)])))            # are all inputs available?
note <- function(txt) tags$div(style = paste0("color:", col_muted, ";padding:20px;"), txt)

# The club/international split assumes career totals INCLUDE international games.
# Only use it if that is plausible (international <= total for every measure).
split_ok <- has("club_goals", "club_assists", "club_apps") &&
  all(unlist(S[, c("club_goals", "club_assists", "club_apps")]) >= 0)

# ---- 6. Index table: every metric scaled so the leader = 100 ---------------
# Higher-is-better metrics: value / best * 100.
# Lower-is-better metrics (minutes per goal): best / value * 100 (inverted).
index_tbl <- dat %>%
  filter(!is.na(Messi), !is.na(Ronaldo), Messi > 0, Ronaldo > 0) %>%
  mutate(
    lower = lower_better(metric),
    best  = if_else(lower, pmin(Messi, Ronaldo), pmax(Messi, Ronaldo)),
    Messi_idx   = if_else(lower, best / Messi,   Messi / best) * 100,
    Ronaldo_idx = if_else(lower, best / Ronaldo, Ronaldo / best) * 100
  )

# ---- 7. Validate the pre-computed statistics in the CSV --------------------
chk_def <- tribble(
  ~metric,                          ~col,           ~tol,   ~d,
  "Goals + assists",                "ga",           0.5,    0,
  "Goals per appearance",           "g_per_app",    0.0051, 3,
  "Goals per 90 minutes",           "g_per90",      0.0051, 3,
  "Goals + assists per 90 minutes", "ga_per90",     0.0051, 3,
  "Minutes per goal",               "min_per_goal", 0.051,  2
)
validation <- chk_def %>%
  crossing(player = players) %>%
  mutate(
    reported   = map2_dbl(metric, player, val),
    recomputed = map2_dbl(col, player, sv),
    status = case_when(
      is.na(reported) | is.na(recomputed)   ~ "Not checked",
      abs(reported - recomputed) <= tol     ~ "Match",
      TRUE                                  ~ "Mismatch"
    ),
    Reported   = map2_chr(reported, d, fmt),
    Recomputed = map2_chr(recomputed, d, fmt)
  ) %>%
  arrange(player, match(metric, chk_def$metric))

n_checked <- sum(validation$status != "Not checked")
n_ok      <- sum(validation$status == "Match")
validation_msg <- if (n_checked == 0) {
  "None of the pre-computed statistics in the CSV could be validated."
} else {
  paste0(n_ok, " of ", n_checked, " pre-computed statistics in the CSV match values ",
         "recalculated from the raw totals (within rounding).")
}

# ---- 8. Plot styling helpers -------------------------------------------------
# Common dark, minimal plotly layout
style_plot <- function(p, ytitle = NULL, xtitle = NULL, legend = TRUE,
                       xaxis_extra = list(), yaxis_extra = list()) {
  p %>%
    layout(
      paper_bgcolor = "rgba(0,0,0,0)",
      plot_bgcolor  = "rgba(0,0,0,0)",
      font   = list(color = col_text, family = font_stack, size = 14),
      xaxis  = modifyList(list(title = xtitle, showgrid = FALSE, zeroline = FALSE,
                               color = col_muted, automargin = TRUE), xaxis_extra),
      yaxis  = modifyList(list(title = ytitle, showgrid = FALSE, zeroline = FALSE,
                               color = col_muted, automargin = TRUE), yaxis_extra),
      showlegend = legend,
      legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.12),
      hoverlabel = list(bgcolor = col_panel, bordercolor = col_gold,
                        font = list(color = col_text, family = font_stack, size = 14)),
      margin = list(l = 10, r = 10, t = 25, b = 10)
    ) %>%
    config(displaylogo = FALSE,
           modeBarButtonsToRemove = c("lasso2d", "select2d", "autoScale2d"))
}

# ggplot2 theme (used where ggplot2 + ggplotly is the better fit)
theme_dash <- function() {
  theme_minimal(base_size = 13) +
    theme(
      plot.background  = element_rect(fill = "transparent", color = NA),
      panel.background = element_rect(fill = "transparent", color = NA),
      text         = element_text(color = col_text),
      axis.text    = element_text(color = col_muted),
      axis.title   = element_blank(),
      panel.grid   = element_blank(),
      strip.text   = element_text(color = col_gold, face = "bold"),
      legend.position = "none"
    )
}

# Two-bar chart (Messi vs Ronaldo) with data labels and custom tooltips
bar_two <- function(values, tooltips, ytitle) {
  d <- tibble(player = factor(players, levels = players),
              value = values, label = map_chr(values, fmt), tip = tooltips)
  plot_ly(d, x = ~player, y = ~value, color = ~player, colors = player_cols,
          type = "bar", text = ~label, textposition = "outside", cliponaxis = FALSE,
          textfont = list(color = col_text, size = 18),
          hovertext = ~tip, hoverinfo = "text") %>%
    style_plot(ytitle = ytitle, legend = FALSE,
               yaxis_extra = list(range = c(0, max(values, na.rm = TRUE) * 1.18)))
}

# ---- 9. HTML building blocks (scoreboard, KPI cards, match-stat bars) ------
# A number that animates from 0 up to its value (see count_js below).
# The final value is already in the text, so it still shows if JavaScript is off.
cnt <- function(x, digits = 0, class = "") {
  if (is.na(x)) {
    tags$span(class = class, "n/a")
  } else {
    tags$span(class = paste(class, "countup"),
              `data-target` = x, `data-digits` = digits, fmt(x, digits))
  }
}

count_js <- "(function(){ function run(){ var els=document.querySelectorAll('.countup'); for(var i=0;i<els.length;i++){ (function(el){ var target=parseFloat(el.getAttribute('data-target')); var d=parseInt(el.getAttribute('data-digits')||'0',10); if(isNaN(target)){return;} var start=null; var dur=1500; function step(ts){ if(start===null){start=ts;} var p=Math.min((ts-start)/dur,1); var e=1-Math.pow(1-p,3); el.textContent=(target*e).toLocaleString('en-GB',{minimumFractionDigits:d,maximumFractionDigits:d}); if(p<1){requestAnimationFrame(step);} } requestAnimationFrame(step); })(els[i]); } } document.addEventListener('DOMContentLoaded',run); if(window.jQuery){ window.jQuery(document).on('shown.bs.tab',run); } })();"

# KPI card: both players side by side, the higher value is underlined in gold
kpi_card <- function(title, m, r, digits = 0) {
  lead_m <- !is.na(m) && !is.na(r) && m > r
  lead_r <- !is.na(m) && !is.na(r) && r > m
  tags$div(
    class = "kpi-card",
    tags$div(class = "kpi-title", title),
    tags$div(
      class = "kpi-row",
      cnt(m, digits, paste(c("kpi-val", "messi", if (lead_m) "lead"), collapse = " ")),
      tags$div(class = "kpi-vs", "vs"),
      cnt(r, digits, paste(c("kpi-val", "ronaldo", if (lead_r) "lead"), collapse = " "))
    )
  )
}

# One "match stats" row: values on each side and a split bar showing each player's share.
# invert = TRUE for statistics where lower is better (share is based on 1 / value).
stat_row <- function(label, m, r, digits = 0, invert = FALSE) {
  if (is.na(m) || is.na(r) || m <= 0 || r <= 0) return(NULL)
  pm <- if (invert) (1 / m) / (1 / m + 1 / r) * 100 else m / (m + r) * 100
  tags$div(
    class = "ms-row",
    tags$div(class = "ms-head",
             tags$span(class = "ms-val messi", fmt(m, digits)),
             tags$span(class = "ms-label", label),
             tags$span(class = "ms-val ronaldo", fmt(r, digits))),
    tags$div(class = "ms-bar",
             tags$div(class = "ms-fill messi",   style = sprintf("flex:%.2f 1 0;", pm)),
             tags$div(class = "ms-fill ronaldo", style = sprintf("flex:%.2f 1 0;", 100 - pm)))
  )
}

# ---- 10. Player photos, avatars, icons and profile blocks -------------------
# Full display names come from the CSV column headers (e.g. "Lionel Messi")
full_name <- c(Messi = messi_col, Ronaldo = ronaldo_col)

# Optional player photos: save them as images/messi.jpg and images/ronaldo.jpg
# (jpg, jpeg, png or webp) in a folder called "images" next to this .Rmd.
# They are embedded into the HTML file. Keep each photo under about 500 KB.
# If no photo is found, a styled silhouette avatar with the player's initials is shown.
photo_uri <- function(p) {
  if (!dir.exists("images")) return(NA_character_)
  f <- list.files("images", pattern = paste0("^", tolower(p), ".*\\.(jpg|jpeg|png|webp)$"),
                  ignore.case = TRUE, full.names = TRUE)
  if (length(f) == 0) NA_character_ else knitr::image_uri(f[1])
}
photo_src <- c(Messi = photo_uri("Messi"), Ronaldo = photo_uri("Ronaldo"))

initials <- function(x) toupper(paste(substr(strsplit(x, " ")[[1]], 1, 1), collapse = ""))
rgba <- function(col, a) { v <- col2rgb(col); sprintf("rgba(%d,%d,%d,%.2f)", v[1], v[2], v[3], a) }

# Fallback avatar: original silhouette in the player's colour, with initials
avatar_svg <- function(p) {
  col <- player_cols[[p]]
  HTML(paste0(
    "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120' width='100%' height='100%'>",
    "<defs><clipPath id='clip-", p, "'><circle cx='60' cy='60' r='58'/></clipPath>",
    "<linearGradient id='bg-", p, "' x1='0' y1='0' x2='0' y2='1'>",
    "<stop offset='0' stop-color='", col, "' stop-opacity='.45'/>",
    "<stop offset='1' stop-color='#0B1A16' stop-opacity='.95'/></linearGradient></defs>",
    "<circle cx='60' cy='60' r='58' fill='url(#bg-", p, ")' stroke='", col, "' stroke-width='3'/>",
    "<g clip-path='url(#clip-", p, ")'>",
    "<circle cx='60' cy='45' r='19' fill='", col, "' fill-opacity='.9'/>",
    "<path d='M18 120C22 84 40 72 60 72s38 12 42 48Z' fill='", col, "' fill-opacity='.9'/>",
    "<text x='60' y='104' text-anchor='middle' font-family='Oswald, Arial, sans-serif' ",
    "font-size='22' font-weight='700' fill='#0B1A16'>", initials(full_name[[p]]), "</text></g></svg>"))
}

# Player picture: the photo if one was supplied, otherwise the avatar
player_avatar <- function(p, size = 70) {
  src <- photo_src[[p]]
  px <- as.integer(size)
  if (!is.na(src)) {
    tags$img(class = "avatar-img", src = src, alt = full_name[[p]],
             style = sprintf("width:%dpx;height:%dpx;border-color:%s;", px, px, player_cols[[p]]))
  } else {
    tags$div(class = "avatar-svg", style = sprintf("width:%dpx;height:%dpx;", px, px), avatar_svg(p))
  }
}

# ---- Original, generic SVG icons (cup, golden ball, assist, globe, clock) ----
.icon_counter <- 0
next_id <- function() { .icon_counter <<- .icon_counter + 1; paste0("ic", .icon_counter) }
svg_open <- function(size) {
  paste0("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' width='", size, "' height='", size, "'>")
}
gold_grad <- function(id) {
  paste0("<defs><linearGradient id='", id, "' x1='0' y1='0' x2='1' y2='1'>",
         "<stop offset='0' stop-color='#FFE08A'/><stop offset='.55' stop-color='#F2C14E'/>",
         "<stop offset='1' stop-color='#B8860B'/></linearGradient></defs>")
}

icon_cup <- function(size = 40) {
  id <- next_id()
  HTML(paste0(svg_open(size), gold_grad(id),
    "<path d='M16 6h32v16c0 11-7 19-16 19S16 33 16 22Z' fill='url(#", id, ")'/>",
    "<path d='M16 11H7c0 11 5 17 12 19M48 11h9c0 11-5 17-12 19' fill='none' stroke='#F2C14E' stroke-width='4' stroke-linecap='round'/>",
    "<rect x='29' y='41' width='6' height='9' fill='#D9A93A'/>",
    "<path d='M19 58h26l-3-8H22Z' fill='url(#", id, ")'/>",
    "<path d='M23 11v11c0 6 3 11 7 13' fill='none' stroke='#FFF6D6' stroke-opacity='.6' stroke-width='2.5' stroke-linecap='round'/></svg>"))
}

icon_ball <- function(size = 40) {
  id <- next_id()
  HTML(paste0(svg_open(size), gold_grad(id),
    "<circle cx='32' cy='32' r='27' fill='url(#", id, ")' stroke='#8A6A0A' stroke-width='1.5'/>",
    "<polygon points='32,23 40.6,29.2 37.3,39.3 26.7,39.3 23.4,29.2' fill='#8A6A0A' fill-opacity='.85'/>",
    "<path d='M32 23V6M40.6 29.2L56.7 24M37.3 39.3L47.3 53M26.7 39.3L16.7 53M23.4 29.2L7.3 24' stroke='#8A6A0A' stroke-width='1.6' fill='none' stroke-opacity='.8'/>",
    "<path d='M15 22C19 14 27 9 36 9' fill='none' stroke='#FFF6D6' stroke-opacity='.6' stroke-width='3' stroke-linecap='round'/></svg>"))
}

icon_assist <- function(size = 40) {
  HTML(paste0(svg_open(size),
    "<circle cx='12' cy='50' r='5' fill='#F2C14E'/>",
    "<path d='M12 50C16 26 36 16 52 24' fill='none' stroke='#F2C14E' stroke-width='4' stroke-linecap='round' stroke-dasharray='1 8'/>",
    "<polygon points='58,27 46,21 47,34' fill='#F2C14E'/></svg>"))
}

icon_globe <- function(size = 40) {
  HTML(paste0(svg_open(size),
    "<g fill='none' stroke='#F2C14E' stroke-width='3' stroke-linecap='round'>",
    "<circle cx='32' cy='32' r='26'/><ellipse cx='32' cy='32' rx='11' ry='26' stroke-width='2.5'/>",
    "<path d='M6 32h52M11 19h42M11 45h42' stroke-width='2.5'/></g></svg>"))
}

icon_clock <- function(size = 40) {
  HTML(paste0(svg_open(size),
    "<g fill='none' stroke='#F2C14E' stroke-width='3.5' stroke-linecap='round'>",
    "<circle cx='32' cy='35' r='23'/><path d='M32 35V21M32 35l10 6M26 6h12'/></g></svg>"))
}

# Small stat tile: icon, animated number, label
stat_tile <- function(icon, value, label, digits = 0) {
  tags$div(class = "tile", icon, cnt(value, digits, "tile-val"), tags$div(class = "tile-label", label))
}

# Profile hero card: photo/avatar, name and three headline numbers
profile_hero <- function(p) {
  tags$div(
    class = paste("profile-hero", tolower(p)),
    tags$div(class = "ph-bigflag", flag_tag(p, 170)),
    tags$div(class = "ph-photo", player_avatar(p, 150)),
    tags$div(
      class = "ph-info",
      tags$div(class = "ph-tag", "PLAYER PROFILE"),
      tags$div(class = "ph-name", toupper(full_name[[p]])),
      tags$div(class = "ph-country", flag_tag(p, 40), tags$span(country[[p]])),
      tags$div(class = "ph-sub", "Career totals from the dataset"),
      tags$div(
        class = "ph-chips",
        tags$div(class = "ph-chip", cnt(sv("goals", p), 0, "ph-num"), tags$span("GOALS")),
        tags$div(class = "ph-chip", cnt(sv("assists", p), 0, "ph-num"), tags$span("ASSISTS")),
        tags$div(class = "ph-chip", cnt(sv("apps", p), 0, "ph-num"), tags$span("APPEARANCES"))
      )
    )
  )
}

# Trophy cabinet: one golden ball per Ballon d'Or award in the data, plus stat tiles
profile_cabinet <- function(p) {
  n <- sv("ballon", p)
  unit_txt <- dat$unit[tolower(dat$metric) == "ballon d'or awards"][1]
  if (length(unit_txt) == 0 || is.na(unit_txt)) unit_txt <- ""
  balls <- if (!is.na(n) && n > 0) {
    lapply(seq_len(min(n, 20)), function(i)
      tags$span(class = "cab-ball", style = sprintf("animation-delay:%.2fs;", i * 0.12), icon_ball(34)))
  } else NULL
  tagList(
    if (!is.na(n)) tags$div(
      class = "cab-row",
      tags$div(class = "cab-text",
               tags$div(class = "cab-label", "Ballon d'Or awards"),
               tags$div(class = "cab-unit", unit_txt)),
      tags$div(class = "cab-icons", balls),
      cnt(n, 0, "cab-count")),
    tags$div(
      class = "tile-grid",
      stat_tile(icon_cup(38),    sv("cl_goals", p),     "Champions League goals"),
      stat_tile(icon_cup(38),    sv("wc_goals", p),     "World Cup goals"),
      stat_tile(icon_globe(38),  sv("intl_goals", p),   "International goals"),
      stat_tile(icon_ball(38),   sv("g_per90", p),      "Goals per 90", 2),
      stat_tile(icon_assist(38), sv("a_per90", p),      "Assists per 90", 2),
      stat_tile(icon_clock(38),  sv("min_per_goal", p), "Minutes per goal", 1)
    )
  )
}

# Two donuts: where the player's goals and assists came from (club vs international)
profile_donuts <- function(p) {
  if (!split_ok) return(note("Club/international split is not available (missing or inconsistent data)."))
  pie_cols <- c(col_club, col_gold)
  plot_ly() %>%
    add_pie(labels = c("Club", "International"),
            values = c(sv("club_goals", p), sv("intl_goals", p)),
            hole = 0.62, sort = FALSE, name = "Goals",
            domain = list(x = c(0, 0.47), y = c(0.05, 0.95)),
            marker = list(colors = pie_cols, line = list(color = col_bg, width = 3)),
            textinfo = "value+percent", textfont = list(color = col_text, size = 13),
            hoverinfo = "label+value+percent") %>%
    add_pie(labels = c("Club", "International"),
            values = c(sv("club_assists", p), sv("intl_assists", p)),
            hole = 0.62, sort = FALSE, name = "Assists", showlegend = FALSE,
            domain = list(x = c(0.53, 1), y = c(0.05, 0.95)),
            marker = list(colors = pie_cols, line = list(color = col_bg, width = 3)),
            textinfo = "value+percent", textfont = list(color = col_text, size = 13),
            hoverinfo = "label+value+percent") %>%
    layout(
      paper_bgcolor = "rgba(0,0,0,0)",
      font = list(color = col_text, family = font_stack, size = 14),
      showlegend = TRUE,
      legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.02),
      hoverlabel = list(bgcolor = col_panel, bordercolor = col_gold, font = list(color = col_text)),
      annotations = list(
        list(text = "<b>GOALS</b>", x = 0.235, y = 0.5, xref = "paper", yref = "paper",
             showarrow = FALSE, font = list(size = 16, color = col_gold)),
        list(text = "<b>ASSISTS</b>", x = 0.765, y = 0.5, xref = "paper", yref = "paper",
             showarrow = FALSE, font = list(size = 16, color = col_gold))),
      margin = list(l = 10, r = 10, t = 10, b = 30)
    ) %>%
    config(displaylogo = FALSE)
}

# One player's metrics against the rival (leader of each metric = 100)
profile_index <- function(p) {
  if (nrow(index_tbl) == 0) return(note("No comparable metrics in the dataset."))
  rival <- setdiff(players, p)
  pc <- player_cols[[p]]
  d <- index_tbl %>%
    mutate(
      idx = .data[[paste0(p, "_idx")]],
      own_txt = map2_chr(.data[[p]], digits, fmt),
      rival_txt = map2_chr(.data[[rival]], digits, fmt),
      bar_col = if_else(idx >= 99.999, pc, rgba(pc, 0.45)),
      metric = factor(metric, levels = rev(unique(dat$metric))),
      tip = paste0("<b>", full_name[[p]], "</b><br>", metric, ": ", own_txt, " ", unit,
                   "<br>", rival, ": ", rival_txt, " ", unit,
                   "<br>Index: ", round(idx, 1),
                   if_else(idx >= 99.999, "<br><i>Leads this metric</i>", ""))
    )
  plot_ly(d, y = ~metric, x = ~idx, type = "bar", orientation = "h",
          marker = list(color = ~bar_col),
          text = ~round(idx), textposition = "outside", cliponaxis = FALSE,
          textfont = list(color = col_text, size = 12),
          hovertext = ~tip, hoverinfo = "text") %>%
    style_plot(xtitle = "Index (leader of each metric = 100)", legend = FALSE,
               xaxis_extra = list(range = c(0, 122))) %>%
    layout(shapes = list(list(type = "line", x0 = 100, x1 = 100, y0 = 0, y1 = 1, yref = "paper",
                              line = list(color = col_gold, dash = "dot", width = 1))))
}

# ---- 11. National flags (original SVG drawings, waved with CSS) --------------
country <- c(Messi = "Argentina", Ronaldo = "Portugal")

flag_svg <- function(ctry) {
  if (ctry == "Argentina") {
    ang  <- seq(0, 330, by = 30) * pi / 180
    rays <- paste0(sprintf("<line x1='%.2f' y1='%.2f' x2='%.2f' y2='%.2f'/>",
                           30 + 5.2 * cos(ang), 20 + 5.2 * sin(ang),
                           30 + 7.4 * cos(ang), 20 + 7.4 * sin(ang)), collapse = "")
    paste0("<rect width='60' height='40' fill='#74ACDF'/>",
           "<rect y='13.33' width='60' height='13.34' fill='#FFFFFF'/>",
           "<g stroke='#F6B40E' stroke-width='.9' stroke-linecap='round'>", rays, "</g>",
           "<circle cx='30' cy='20' r='4.3' fill='#F6B40E' stroke='#C98A00' stroke-width='.5'/>")
  } else {
    paste0("<rect width='60' height='40' fill='#FF0000'/>",
           "<rect width='24' height='40' fill='#046A38'/>",
           "<circle cx='24' cy='20' r='7.2' fill='none' stroke='#FFE000' stroke-width='1.6'/>",
           "<circle cx='24' cy='20' r='4.2' fill='none' stroke='#FFE000' stroke-width='.8'/>",
           "<path d='M20.4 15.6h7.2v6c0 2.6-1.7 4.2-3.6 5-1.9-.8-3.6-2.4-3.6-5z' fill='#FFFFFF' stroke='#FFE000' stroke-width='.6'/>",
           "<path d='M21.8 17h4.4v4.2c0 1.6-1 2.6-2.2 3.2-1.2-.6-2.2-1.6-2.2-3.2z' fill='#FF0000'/>")
  }
}

# A waving flag (width in px, 3:2 ratio). Animated by the .flag CSS rules.
flag_tag <- function(p, w = 44) {
  h <- round(w * 2 / 3)
  tags$span(
    class = paste("flag", tolower(p)),
    style = sprintf("width:%dpx;height:%dpx;", as.integer(w), as.integer(h)),
    title = country[[p]],
    HTML(paste0("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40' width='100%' height='100%' ",
                "preserveAspectRatio='none'>", flag_svg(country[[p]]), "</svg>")),
    tags$span(class = "flag-shine"))
}

# ---- 12. Club-by-club career data (typed in; NOT part of the CSV) -----------
# Senior-team figures, all competitions. Past clubs are final; the current clubs
# (Inter Miami, Al Nassr) change every match - figures are approximate, taken from
# public reports in Sep-Oct 2026. Assists for Sporting CP, Manchester United,
# Inter Miami and Al Nassr are estimates. Edit this table to update anything.
club_tbl <- tribble(
  ~player,   ~club,              ~abbr, ~nation,   ~league,           ~years,                  ~apps, ~goals, ~assists, ~color,    ~ink,      ~trophies,
  "Messi",   "Grandoli",         "GRA", "Argentina","Youth academy",  "1992\u20131995",        NA,    NA,     NA,       "#7A8F86", "#FFFFFF", "",
  "Messi",   "Newell's Old Boys","NOB", "Argentina","Youth academy",  "1995\u20132000",        NA,    NA,     NA,       "#B3121B", "#FFFFFF", "Youth league titles",
  "Messi",   "FC Barcelona Youth","BAY","Spain",  "Youth academy",  "2000\u20132003",        NA,    NA,     NA,       "#5B1A33", "#FFFFFF", "Youth league titles",
  "Messi",   "FC Barcelona C",   "BAC", "Spain",   "Tercera Divisi\u00f3n", "2003\u20132004",  10,    5,      NA,       "#7A1F3D", "#FFFFFF", "",
  "Messi",   "FC Barcelona B",   "BAB", "Spain",   "Segunda Divisi\u00f3n", "2004\u20132005",  22,    6,      NA,       "#8C2A4D", "#FFFFFF", "",
  "Messi",   "FC Barcelona",     "FCB", "Spain",   "La Liga",         "2004\u20132021",        778,   672,    305,      "#A50044", "#FFFFFF", "10 La Liga;7 Copa del Rey;4 Champions League;3 Club World Cup;3 UEFA Super Cup;8 Supercopa de Espa\u00f1a",
  "Messi",   "Paris Saint-Germain", "PSG", "France", "Ligue 1",       "2021\u20132023",        75,    32,     35,       "#1F5FB4", "#FFFFFF", "2 Ligue 1;1 Troph\u00e9e des Champions",
  "Messi",   "Inter Miami",      "MIA", "USA",     "MLS",             "2023\u2013present",     111,   98,     66,       "#F7B5CD", "#0B1A16", "Leagues Cup 2023;Supporters' Shield 2024;MLS Cup 2025",
  "Ronaldo", "Andorinha",        "AND", "Portugal","Youth academy",  "1992\u20131995",        NA,    NA,     NA,       "#7A8F86", "#FFFFFF", "",
  "Ronaldo", "CD Nacional",      "NAC", "Portugal","Youth academy",  "1995\u20131997",        NA,    NA,     NA,       "#1B3A8C", "#FFFFFF", "",
  "Ronaldo", "Sporting CP Youth","SPY", "Portugal","Youth academy",  "1997\u20132002",        NA,    NA,     NA,       "#4FA06E", "#FFFFFF", "",
  "Ronaldo", "Sporting CP B",    "SPB", "Portugal","Reserve team",   "2001\u20132002",        NA,    NA,     NA,       "#3E8E5E", "#FFFFFF", "",
  "Ronaldo", "Sporting CP",      "SCP", "Portugal","Primeira Liga",   "2002\u20132003",        31,    5,      6,        "#00843D", "#FFFFFF", "Superta\u00e7a 2002",
  "Ronaldo", "Manchester United (1st spell)", "MUN", "England", "Premier League", "2003\u20132009", 292, 118, 64, "#DA291C", "#FFFFFF", "3 Premier League;1 Champions League;1 FA Cup;2 League Cup;1 Club World Cup",
  "Ronaldo", "Real Madrid",      "RMA", "Spain",   "La Liga",         "2009\u20132018",        438,   450,    131,      "#EDEDED", "#0B1A16", "2 La Liga;4 Champions League;2 Copa del Rey;3 Club World Cup;2 UEFA Super Cup;2 Supercopa de Espa\u00f1a",
  "Ronaldo", "Juventus",         "JUV", "Italy",   "Serie A",         "2018\u20132021",        134,   101,    22,       "#C9C9C9", "#0B1A16", "2 Serie A;1 Coppa Italia;2 Supercoppa Italiana",
  "Ronaldo", "Manchester United (2nd spell)", "MUN", "England", "Premier League", "2021\u20132022", 54, 27, 6, "#B71C1C", "#FFFFFF", "",
  "Ronaldo", "Al Nassr",         "NAS", "Saudi Arabia", "Saudi Pro League", "2023\u2013present", 160,  135,    28,       "#F9D71C", "#0B1A16", "Arab Club Champions Cup 2023;Saudi Pro League 2025/26"
) %>%
  group_by(player) %>%
  mutate(ga = goals + assists, g_app = goals / apps, a_app = assists / apps,
         ga_app = (goals + assists) / apps, share = goals / sum(goals, na.rm = TRUE)) %>%
  ungroup() %>%
  mutate(stage_senior = !is.na(goals),
         tip = paste0("<b>", club, "</b> (", years, ")<br>", league, ", ", nation,
                      "<br>Appearances: ", comma(apps), "<br>Goals: ", comma(goals),
                      "<br>Assists: ", comma(assists), "<br>Goals per match: ", sprintf("%.2f", g_app),
                      "<br>G+A per match: ", sprintf("%.2f", ga_app),
                      "<br>Share of club goals: ", sprintf("%.1f%%", share * 100)))

# One club card: badge (initials, no logos), animated numbers, goal-share bar, honours
club_card <- function(r, i, max_goals) {
  pc <- player_cols[[r$player]]
  f2 <- function(x) if (is.na(x)) "n/a" else sprintf("%.2f", x)
  bar_w <- if (is.na(r$goals)) 0 else r$goals / max_goals * 100
  share_txt <- if (is.na(r$goals)) "Youth / reserve spell - stats not included in this table" else
    if (is.na(r$assists)) sprintf("%.1f%% of the player's club goals (assists not recorded)", r$share * 100) else
    sprintf("%.1f%% of the player's club goals", r$share * 100)
  trophies <- trimws(strsplit(r$trophies, ";", fixed = TRUE)[[1]])
  tags$div(
    class = "club-card",
    style = sprintf("--cc:%s;--ink:%s;--pc:%s;animation-delay:%.2fs;", r$color, r$ink, pc, i * 0.14),
    tags$div(class = "cc-badge", r$abbr),
    tags$div(
      class = "cc-main",
      tags$div(class = "cc-top",
               tags$span(class = "cc-name", r$club),
               tags$span(class = "cc-years", r$years)),
      tags$div(class = "cc-sub", paste0(r$nation, "  |  ", r$league)),
      tags$div(
        class = "cc-stats",
        tags$div(class = "cc-stat", cnt(r$goals, 0, "cc-num"), tags$span("GOALS")),
        tags$div(class = "cc-stat", cnt(r$assists, 0, "cc-num"), tags$span("ASSISTS")),
        tags$div(class = "cc-stat", cnt(r$apps, 0, "cc-num"), tags$span("APPS")),
        tags$div(class = "cc-stat", tags$span(class = "cc-num", f2(r$g_app)), tags$span("GOALS / MATCH")),
        tags$div(class = "cc-stat", tags$span(class = "cc-num", f2(r$ga_app)), tags$span("G+A / MATCH"))
      ),
      tags$div(class = "cc-bar", tags$div(class = "cc-fill", style = sprintf("width:%.1f%%;", bar_w))),
      tags$div(class = "cc-share", share_txt),
      tags$div(class = "cc-trophies",
               lapply(trophies, function(t) tags$span(class = "cc-trophy", paste0("\U0001F3C6 ", t))))
    )
  )
}

# A player's whole club career: flag header + one card per club
club_column <- function(p) {
  d <- club_tbl %>% filter(player == p)
  tagList(
    tags$div(class = "club-head",
             flag_tag(p, 52),
             tags$div(tags$div(class = "ch-name", toupper(full_name[[p]])),
                      tags$div(class = "ch-sub",
                               paste0(country[[p]], "  |  ", n_distinct(sub(" \\(.*$", "", d$club)), " clubs  |  ",
                                      comma(sum(d$goals, na.rm = TRUE)), " club goals  |  ",
                                      comma(sum(d$apps, na.rm = TRUE)), " club appearances")))),
    tags$div(class = "club-list club-scroll",
             lapply(seq_len(nrow(d)), function(i) club_card(d[i, ], i, max(d$goals, na.rm = TRUE)))),
    tags$script(HTML(count_js))
  )
}

# Goals and assists by club for one player
club_chart <- function(p) {
  d <- club_tbl %>% filter(player == p, !is.na(goals)) %>% mutate(club = factor(club, levels = club))
  plot_ly(d, x = ~club) %>%
    add_bars(y = ~goals, name = "Goals", marker = list(color = player_cols[[p]]),
             text = ~goals, textposition = "outside", cliponaxis = FALSE,
             textfont = list(color = col_text, size = 14), hovertext = ~tip, hoverinfo = "text") %>%
    add_bars(y = ~assists, name = "Assists", marker = list(color = col_gold),
             text = ~assists, textposition = "outside", cliponaxis = FALSE,
             textfont = list(color = col_text, size = 14), hovertext = ~tip, hoverinfo = "text") %>%
    style_plot(ytitle = "Count", yaxis_extra = list(range = c(0, max(d$goals) * 1.2))) %>%
    layout(barmode = "group", bargap = 0.25)
}

# ---- 13. Pitch heatmaps with a running player (ILLUSTRATIVE) -----------------
# The dataset has no positional / tracking data, so these heat zones are hand-drawn
# from each player's commonly described role at each club. They show the idea of a
# heatmap, not measured data. Pitch is 105 x 68; the team attacks left to right.
# zone = c(x, y, intensity 0-1, radius). Edit the numbers to change the picture.
pitch_zones <- list(
  Messi = list(
    "Barcelona" = list(
      role = "Starts as a right winger cutting inside, later a roaming false nine between the lines.",
      zones = list(c(74,46,1,13), c(62,36,.9,14), c(86,36,.9,10), c(50,40,.55,13), c(92,30,.5,8), c(38,36,.3,12)),
      path = "M38 46 C52 52 62 44 70 41 S84 36 92 34 S80 22 64 28 S44 40 38 46"),
    "Paris Saint-Germain" = list(
      role = "Right-sided forward who drops deep to link play and create for team-mates.",
      zones = list(c(70,44,.9,13), c(58,38,.8,14), c(80,34,.6,11), c(46,42,.5,13), c(88,40,.4,9)),
      path = "M44 44 C54 50 64 46 72 42 S86 40 90 36 S76 28 62 32 S46 38 44 44"),
    "Inter Miami" = list(
      role = "Free-roaming playmaker in the centre and right channel, finishing late in attacks.",
      zones = list(c(62,36,1,15), c(72,42,.8,13), c(52,38,.7,14), c(82,36,.6,11), c(40,40,.4,12), c(90,32,.35,8)),
      path = "M40 40 C50 34 60 40 70 38 S84 34 90 32 S74 46 60 42 S44 44 40 40")
  ),
  Ronaldo = list(
    "Manchester United" = list(
      role = "Wide attacker who beats defenders on the flank and cuts in to shoot.",
      zones = list(c(80,22,.9,13), c(88,34,.8,10), c(70,46,.6,12), c(62,30,.5,13), c(94,34,.7,8), c(78,48,.45,10)),
      path = "M60 20 C72 16 82 20 88 28 S96 36 90 40 S72 48 66 38 S56 24 60 20"),
    "Real Madrid" = list(
      role = "Left winger who became the main central goalscorer, hunting around the penalty area.",
      zones = list(c(82,24,1,13), c(92,34,.9,10), c(72,30,.7,13), c(62,22,.45,12), c(86,46,.35,9)),
      path = "M62 20 C72 18 84 20 90 28 S96 34 92 38 S78 34 70 30 S58 22 62 20"),
    "Juventus" = list(
      role = "Left-sided striker who attacks the box and arrives late for headers and finishes.",
      zones = list(c(88,28,.8,11), c(94,36,.9,9), c(78,24,.6,12), c(68,34,.45,13), c(84,44,.4,10)),
      path = "M66 30 C76 24 86 24 92 30 S96 38 88 42 S72 40 66 30"),
    "Al Nassr" = list(
      role = "Central striker and penalty-area poacher; most of his time is spent inside the box.",
      zones = list(c(94,34,1,9), c(86,34,.8,11), c(90,24,.5,9), c(90,44,.5,9), c(74,34,.35,12)),
      path = "M72 34 C80 28 90 26 95 32 S92 42 86 42 S76 38 72 34")
  )
)

pitch_svg <- function(uid, zones, path, pc) {
  hid <- paste0("heat", uid)
  stripes <- paste(vapply(0:9, function(i) sprintf(
    "<rect x='%.1f' y='0' width='10.5' height='68' fill='%s'/>", i * 10.5, if (i %% 2 == 0) "#1D7A4F" else "#1A6E48"),
    character(1)), collapse = "")
  blobs <- paste(vapply(zones, function(z) sprintf(
    "<circle cx='%.1f' cy='%.1f' r='%.1f' fill='url(#%s)' opacity='%.2f'><animate attributeName='opacity' values='%.2f;%.2f;%.2f' dur='3.5s' repeatCount='indefinite'/></circle>",
    z[1], z[2], z[4], hid, z[3], z[3] * .75, z[3], z[3] * .75), character(1)), collapse = "")
  lines <- paste0(
    "<g fill='none' stroke='#FFFFFF' stroke-width='.45' stroke-opacity='.85'>",
    "<rect x='0' y='0' width='105' height='68'/><line x1='52.5' y1='0' x2='52.5' y2='68'/>",
    "<circle cx='52.5' cy='34' r='9.15'/>",
    "<rect x='0' y='13.85' width='16.5' height='40.3'/><rect x='88.5' y='13.85' width='16.5' height='40.3'/>",
    "<rect x='0' y='24.85' width='5.5' height='18.3'/><rect x='99.5' y='24.85' width='5.5' height='18.3'/>",
    "<path d='M16.5 26.69 A9.15 9.15 0 0 1 16.5 41.31'/><path d='M88.5 26.69 A9.15 9.15 0 0 0 88.5 41.31'/>",
    "<rect x='-1.6' y='30.3' width='1.6' height='7.4'/><rect x='105' y='30.3' width='1.6' height='7.4'/></g>",
    "<circle cx='11' cy='34' r='.5' fill='#fff'/><circle cx='94' cy='34' r='.5' fill='#fff'/><circle cx='52.5' cy='34' r='.5' fill='#fff'/>")
  paste0(
    "<svg class='pitch-svg' xmlns='http://www.w3.org/2000/svg' viewBox='-2.5 -2 110 72' width='100%'>",
    "<defs><radialGradient id='", hid, "'>",
    "<stop offset='0%' stop-color='#FF2A2A' stop-opacity='.95'/><stop offset='35%' stop-color='#FF9800' stop-opacity='.7'/>",
    "<stop offset='68%' stop-color='#FFEB3B' stop-opacity='.35'/><stop offset='100%' stop-color='#FFEB3B' stop-opacity='0'/>",
    "</radialGradient></defs>",
    stripes, blobs, lines,
    "<path d='", path, "' fill='none' stroke='", pc, "' stroke-width='.7' stroke-dasharray='2 2' opacity='.9'>",
    "<animate attributeName='stroke-dashoffset' from='0' to='-8' dur='1s' repeatCount='indefinite'/></path>",
    "<g><animateMotion dur='11s' repeatCount='indefinite' begin='-0.7s' path='", path, "'/>",
    "<circle r='.9' fill='#FFFFFF' stroke='#222' stroke-width='.25'/></g>",
    "<g><animateMotion dur='11s' repeatCount='indefinite' path='", path, "'/>",
    "<circle r='2' fill='", pc, "' opacity='.3'><animate attributeName='r' values='2;5;2' dur='1.2s' repeatCount='indefinite'/></circle>",
    "<circle r='1.9' fill='", pc, "' stroke='#FFFFFF' stroke-width='.5'/></g>",
    "<text x='52.5' y='67' text-anchor='middle' font-size='2.6' fill='#FFFFFF' fill-opacity='.55' ",
    "font-family='Oswald, Arial, sans-serif' letter-spacing='.4'>ATTACKING DIRECTION &#8594;</text>",
    "</svg>")
}

pitch_js <- "(function(){ if(window.__pitchBound){return;} window.__pitchBound=true; document.addEventListener('click',function(e){ var b=e.target.closest ? e.target.closest('.pitch-btn') : null; if(!b){return;} var w=b.closest('.pitch-wrap'); var bs=w.querySelectorAll('.pitch-btn'); for(var i=0;i<bs.length;i++){bs[i].classList.remove('active');} b.classList.add('active'); var vs=w.querySelectorAll('.pitch-view'); for(var j=0;j<vs.length;j++){ vs[j].style.display = (vs[j].getAttribute('data-id')===b.getAttribute('data-target')) ? 'block' : 'none'; } }); })();"

pitch_panel <- function(p) {
  zs <- pitch_zones[[p]]
  nm <- names(zs)
  btns <- lapply(seq_along(nm), function(i)
    tags$button(type = "button", class = paste("pitch-btn", if (i == 1) "active" else ""),
                `data-target` = paste0(p, "-", i), nm[i]))
  views <- lapply(seq_along(nm), function(i)
    tags$div(class = "pitch-view", `data-id` = paste0(p, "-", i),
             style = if (i == 1) "display:block;" else "display:none;",
             HTML(pitch_svg(paste0(p, i), zs[[i]]$zones, zs[[i]]$path, player_cols[[p]])),
             tags$div(class = "pitch-role", zs[[i]]$role)))
  tagList(
    tags$div(
      class = "pitch-wrap",
      tags$div(class = "pitch-head", flag_tag(p, 34), tags$span(class = "pitch-title", toupper(full_name[[p]]))),
      tags$div(class = "pitch-btns", btns),
      views,
      tags$div(class = "pitch-legend", tags$span("Less time"), tags$span(class = "pitch-scale"), tags$span("More time")),
      tags$div(class = "pitch-note",
               "Illustrative heatmap: the dataset has no tracking data, so the zones are drawn from the player's usual role at each club. Pick a club above.")),
    tags$script(HTML(pitch_js)))
}
```

```{css, echo=FALSE}
/* =====================================================================
   FOOTBALL THEME - dark pitch green, chalk lines, trophy gold
   Fonts load from Google Fonts (needs internet); fallbacks are provided.
   ===================================================================== */
@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@500;600;700&family=Barlow+Condensed:wght@400;500;600&family=Inter:wght@400;500;600&display=swap');

:root {
  --bg: #0B1A16; --panel: rgba(16,38,31,.80); --line: rgba(255,255,255,.14);
  --gold: #F2C14E; --text: #F3F7F4; --muted: #9DB8AC;
  --messi: #6CC3F5; --ronaldo: #FF4D5A; --grass: #3F8F6B;
}

/* ---------- Keyframes ---------- */
@keyframes slideUp { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes fadeIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes wipe    { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes fillbar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes spin    { to { transform: rotate(360deg); } }
@keyframes pulse   { 0%, 100% { box-shadow: 0 0 0 0 rgba(242,193,78,.55); } 50% { box-shadow: 0 0 0 14px rgba(242,193,78,0); } }
@keyframes blink   { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@keyframes shine   { 0%, 60% { left: -60%; } 100% { left: 130%; } }

/* ---------- Page: mown-grass stripes + soft vignette ---------- */
body {
  background: repeating-linear-gradient(90deg, #0B1A16 0, #0B1A16 90px, #0E2019 90px, #0E2019 180px);
  background-attachment: fixed;
  color: var(--text);
  font-family: 'Inter', 'Segoe UI', Helvetica, Arial, sans-serif;
}
body::before {
  content: ''; position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse at center, rgba(0,0,0,0) 45%, rgba(0,0,0,.5) 100%);
}
.tab-content, .section, .dashboard-column { background: transparent !important; color: var(--text); }
.tab-pane.active { animation: fadeIn .5s ease; }

/* ---------- Navbar ---------- */
.navbar-inverse {
  background: linear-gradient(90deg, #06110D, #0F2B21 55%, #06110D);
  border: 0; border-bottom: 3px solid var(--gold);
  box-shadow: 0 4px 18px rgba(0,0,0,.55);
}
.navbar-inverse .navbar-brand {
  font-family: 'Oswald', 'Segoe UI', sans-serif; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: #fff;
}
.navbar-inverse .navbar-brand::before { content: '\26BD'; display: inline-block; margin-right: 10px; animation: spin 6s linear infinite; }
.navbar-inverse .navbar-nav > li > a {
  font-family: 'Oswald', 'Segoe UI', sans-serif; text-transform: uppercase;
  letter-spacing: .1em; font-size: 13px; color: #CFE3D9; transition: color .2s, background .2s;
}
.navbar-inverse .navbar-nav > li > a:hover { color: var(--gold); background: rgba(242,193,78,.08); }
.navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus {
  background: rgba(242,193,78,.12); color: var(--gold); box-shadow: inset 0 -3px 0 var(--gold);
}

/* ---------- Glass panels with chalk-line borders ---------- */
.chart-wrapper {
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  box-shadow: 0 8px 24px rgba(0,0,0,.35);
  transition: box-shadow .3s ease, border-color .3s ease;
  animation: slideUp .7s ease backwards;
}
.chart-wrapper:hover { box-shadow: 0 12px 30px rgba(0,0,0,.5), 0 0 0 1px rgba(242,193,78,.4); border-color: rgba(242,193,78,.5); }
.dashboard-column > .chart-wrapper:nth-child(2) { animation-delay: .12s; }
.dashboard-column > .chart-wrapper:nth-child(3) { animation-delay: .24s; }
.chart-stage { background: transparent; color: var(--text); }
.chart-stage a { color: var(--gold); }
.chart-title {
  position: relative; padding-left: 34px;
  font-family: 'Oswald', 'Segoe UI', sans-serif; text-transform: uppercase; letter-spacing: .14em; font-size: 13px; font-weight: 600;
  color: var(--gold); border-bottom: 1px solid var(--line);
  background: linear-gradient(90deg, rgba(242,193,78,.2), rgba(242,193,78,0) 70%);
}
.chart-title::before { content: '\26BD'; position: absolute; left: 12px; font-size: 13px; }

/* ---------- Plotly bars: wipe-in animation, gold glow on hover ---------- */
.js-plotly-plot .barlayer .point path { animation: wipe 1.1s cubic-bezier(.2,.8,.2,1) both; transition: filter .2s; }
.js-plotly-plot .barlayer .point text { animation: fadeIn .6s ease .9s both; }
.js-plotly-plot .barlayer .point path:hover { filter: brightness(1.25) drop-shadow(0 0 8px rgba(242,193,78,.65)); }
.js-plotly-plot .scatterlayer .trace, .js-plotly-plot .pielayer .slice { animation: fadeIn 1s ease both; }

/* ---------- Scoreboard ---------- */
.scoreboard {
  position: relative; margin: 12px 14px; padding: 14px 10px 12px; border-radius: 12px; overflow: hidden;
  border: 1px solid var(--line); text-align: center;
  background:
    url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 220' preserveAspectRatio='none'%3E%3Cg fill='none' stroke='white' stroke-opacity='.16' stroke-width='2'%3E%3Crect x='4' y='4' width='592' height='212'/%3E%3Cline x1='300' y1='4' x2='300' y2='216'/%3E%3Ccircle cx='300' cy='110' r='42'/%3E%3Crect x='4' y='62' width='66' height='96'/%3E%3Crect x='530' y='62' width='66' height='96'/%3E%3Crect x='4' y='88' width='24' height='44'/%3E%3Crect x='572' y='88' width='24' height='44'/%3E%3C/g%3E%3C/svg%3E") center / 100% 100% no-repeat,
    linear-gradient(180deg, rgba(17,52,40,.95), rgba(8,26,19,.95));
}
.sb-top { font-family: 'Oswald', sans-serif; letter-spacing: .22em; font-size: 11px; color: var(--muted); text-transform: uppercase; }
.live-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #FF4D5A; margin-right: 8px; animation: blink 1.4s ease-in-out infinite; }
.sb-main { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
.sb-side { flex: 1; }
.sb-name { font-family: 'Oswald', sans-serif; letter-spacing: .18em; font-size: 15px; font-weight: 600; }
.sb-side.messi .sb-name, .sb-side.messi .sb-score { color: var(--messi); }
.sb-side.ronaldo .sb-name, .sb-side.ronaldo .sb-score { color: var(--ronaldo); }
.sb-score { display: block; font-family: 'Oswald', sans-serif; font-size: 58px; font-weight: 700; line-height: 1.05; text-shadow: 0 0 22px currentColor; }
.sb-sub { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .2em; color: var(--muted); font-size: 12px; }
.sb-vs {
  width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  font-family: 'Oswald', sans-serif; font-weight: 700; color: #0B1A16; background: var(--gold); animation: pulse 2.2s infinite;
}
.sb-foot { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.sb-chip {
  display: flex; align-items: center; gap: 10px; padding: 4px 12px; border-radius: 20px;
  background: rgba(0,0,0,.35); border: 1px solid var(--line); font-family: 'Oswald', sans-serif; font-size: 15px;
}
.sb-chip .messi { color: var(--messi); } .sb-chip .ronaldo { color: var(--ronaldo); }
.sb-chip-label { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .14em; font-size: 12px; color: var(--muted); }

/* ---------- KPI cards ---------- */
.kpi-legend { display: flex; gap: 18px; align-items: center; padding: 10px 14px 0 14px; font-family: 'Barlow Condensed', sans-serif; letter-spacing: .08em; font-size: 14px; color: var(--muted); }
.kpi-legend .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; }
.dot.messi { background: var(--messi); } .dot.ronaldo { background: var(--ronaldo); }
.kpi-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; padding: 12px 14px; }
.kpi-card {
  position: relative; overflow: hidden; padding: 12px 14px; border-radius: 10px;
  background: linear-gradient(160deg, rgba(23,58,45,.9), rgba(12,32,25,.9));
  border: 1px solid var(--line); border-top: 3px solid var(--gold);
  transition: transform .25s ease, box-shadow .25s ease; animation: slideUp .6s ease backwards;
}
.kpi-card:nth-child(1) { animation-delay: .05s; } .kpi-card:nth-child(2) { animation-delay: .12s; }
.kpi-card:nth-child(3) { animation-delay: .19s; } .kpi-card:nth-child(4) { animation-delay: .26s; }
.kpi-card:nth-child(5) { animation-delay: .33s; } .kpi-card:nth-child(6) { animation-delay: .40s; }
.kpi-card:hover { transform: translateY(-4px); box-shadow: 0 10px 24px rgba(0,0,0,.5), 0 0 0 1px var(--gold); }
.kpi-card::after {
  content: ''; position: absolute; top: 0; left: -60%; width: 40%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.09), transparent);
  transform: skewX(-20deg); animation: shine 5s ease-in-out infinite;
}
.kpi-title { font-family: 'Barlow Condensed', sans-serif; color: var(--muted); font-size: 13px; text-transform: uppercase; letter-spacing: .14em; }
.kpi-row { display: flex; align-items: baseline; justify-content: space-between; margin-top: 6px; }
.kpi-val { font-family: 'Oswald', sans-serif; font-size: 30px; font-weight: 600; line-height: 1.1; }
.kpi-val.messi { color: var(--messi); } .kpi-val.ronaldo { color: var(--ronaldo); }
.kpi-val.lead { text-decoration: underline; text-decoration-color: var(--gold); text-decoration-thickness: 3px; text-underline-offset: 6px; }
.kpi-vs { font-family: 'Barlow Condensed', sans-serif; color: var(--muted); font-size: 13px; letter-spacing: .1em; }
.kpi-note { color: var(--muted); font-size: 12px; padding: 0 14px 12px 14px; }

/* ---------- Match stats (split bars, like a TV match-centre) ---------- */
.ms-wrap { padding: 8px 18px 14px 18px; }
.ms-legend { display: flex; justify-content: space-between; font-family: 'Oswald', sans-serif; letter-spacing: .16em; font-size: 13px; text-transform: uppercase; margin-bottom: 4px; }
.ms-legend .messi { color: var(--messi); } .ms-legend .ronaldo { color: var(--ronaldo); }
.ms-row { margin: 9px 0 13px 0; }
.ms-head { display: flex; justify-content: space-between; align-items: baseline; }
.ms-val { font-family: 'Oswald', sans-serif; font-size: 19px; font-weight: 600; min-width: 70px; }
.ms-val.messi { color: var(--messi); text-align: left; } .ms-val.ronaldo { color: var(--ronaldo); text-align: right; }
.ms-label { font-family: 'Barlow Condensed', sans-serif; text-transform: uppercase; letter-spacing: .14em; color: var(--muted); font-size: 13px; text-align: center; }
.ms-bar { display: flex; gap: 3px; height: 10px; margin-top: 4px; border-radius: 6px; overflow: hidden; background: rgba(255,255,255,.06); }
.ms-fill { height: 100%; }
.ms-fill.messi   { background: linear-gradient(90deg, #3C9BD6, var(--messi)); transform-origin: left;  animation: fillbar 1.3s cubic-bezier(.2,.8,.2,1) both; }
.ms-fill.ronaldo { background: linear-gradient(90deg, #C62E3A, var(--ronaldo)); transform-origin: right; animation: fillbar 1.3s cubic-bezier(.2,.8,.2,1) both; }

/* ---------- Avatars and player photos ---------- */
.avatar-img { display: block; margin: 0 auto 4px; border-radius: 50%; object-fit: cover; object-position: center top; border: 3px solid var(--gold); box-shadow: 0 0 18px rgba(0,0,0,.5); }
.avatar-svg { display: block; margin: 0 auto 4px; }

/* ---------- Profile hero ---------- */
@keyframes popIn { from { opacity: 0; transform: scale(.2) rotate(-90deg); } to { opacity: 1; transform: none; } }
@keyframes floaty { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
.profile-hero { display: flex; align-items: center; gap: 22px; margin: 12px 14px; padding: 18px 20px; border-radius: 14px; border: 1px solid var(--line); overflow: hidden; position: relative; }
.profile-hero.messi   { --pc: #6CC3F5; background: linear-gradient(135deg, rgba(108,195,245,.28), rgba(8,24,18,.96) 62%); }
.profile-hero.ronaldo { --pc: #FF4D5A; background: linear-gradient(135deg, rgba(255,77,90,.26), rgba(8,24,18,.96) 62%); }
.ph-photo { position: relative; width: 150px; height: 150px; flex: none; animation: floaty 5s ease-in-out infinite; }
.ph-photo::before { content: ''; position: absolute; top: -6px; left: -6px; right: -6px; bottom: -6px; border-radius: 50%; background: conic-gradient(var(--gold), var(--pc), var(--gold)); animation: spin 8s linear infinite; }
.ph-photo .avatar-img, .ph-photo .avatar-svg { position: relative; margin: 0; border-radius: 50%; border: 4px solid #0B1A16; box-sizing: border-box; background: #0B1A16; }
.ph-info { flex: 1; min-width: 0; }
.ph-tag { font-family: 'Oswald', sans-serif; letter-spacing: .24em; font-size: 11px; color: var(--gold); }
.ph-name { font-family: 'Oswald', sans-serif; font-size: 30px; font-weight: 700; line-height: 1.1; color: var(--pc); text-shadow: 0 0 20px rgba(0,0,0,.6); }
.ph-sub { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .1em; color: var(--muted); font-size: 14px; margin-top: 2px; }
.ph-chips { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.ph-chip { padding: 6px 12px; border-radius: 10px; background: rgba(0,0,0,.35); border: 1px solid var(--line); border-top: 2px solid var(--pc); text-align: center; }
.ph-num { display: block; font-family: 'Oswald', sans-serif; font-size: 24px; font-weight: 600; line-height: 1.1; }
.ph-chip span:last-child { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .14em; font-size: 11px; color: var(--muted); }

/* ---------- Trophy cabinet and stat tiles ---------- */
.cab-row { display: flex; align-items: center; gap: 14px; margin: 12px 14px 4px; padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(242,193,78,.35); background: linear-gradient(135deg, rgba(242,193,78,.14), rgba(8,24,18,.6)); }
.cab-label { font-family: 'Oswald', sans-serif; letter-spacing: .12em; text-transform: uppercase; font-size: 13px; color: var(--gold); }
.cab-unit { font-family: 'Barlow Condensed', sans-serif; color: var(--muted); font-size: 12px; letter-spacing: .08em; }
.cab-icons { flex: 1; display: flex; flex-wrap: wrap; gap: 4px; }
.cab-ball { display: inline-block; line-height: 0; transition: transform .4s ease; animation: popIn .55s cubic-bezier(.2,1.4,.4,1) backwards; filter: drop-shadow(0 2px 4px rgba(0,0,0,.5)); }
.cab-ball:hover { transform: rotate(360deg) scale(1.25); }
.cab-count { font-family: 'Oswald', sans-serif; font-size: 40px; font-weight: 700; color: var(--gold); text-shadow: 0 0 16px rgba(242,193,78,.5); }
.tile-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 10px 14px 14px; }
.tile { text-align: center; padding: 10px 6px; border-radius: 10px; border: 1px solid var(--line); border-top: 3px solid var(--gold); background: linear-gradient(160deg, rgba(23,58,45,.9), rgba(12,32,25,.9)); transition: transform .25s ease, box-shadow .25s ease; animation: slideUp .6s ease backwards; }
.tile:hover { transform: translateY(-4px); box-shadow: 0 10px 22px rgba(0,0,0,.5), 0 0 0 1px var(--gold); }
.tile svg { display: block; margin: 0 auto 4px; }
.tile-val { display: block; font-family: 'Oswald', sans-serif; font-size: 25px; font-weight: 600; color: var(--text); line-height: 1.1; }
.tile-label { font-family: 'Barlow Condensed', sans-serif; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); font-size: 12px; margin-top: 2px; }

/* ---------- DT tables ---------- */
.dataTables_wrapper, table.dataTable, table.dataTable td, .dataTables_info, .dataTables_length label, .dataTables_filter label { color: var(--text) !important; font-family: 'Barlow Condensed', 'Inter', sans-serif; font-size: 15px; }
table.dataTable thead th { background: rgba(242,193,78,.12); color: var(--gold) !important; font-family: 'Oswald', sans-serif; letter-spacing: .08em; text-transform: uppercase; border-bottom: 1px solid var(--gold) !important; }
table.dataTable tbody tr { background-color: rgba(10,26,20,.55) !important; }
table.dataTable.stripe tbody tr.odd { background-color: rgba(18,45,35,.65) !important; }
table.dataTable.hover tbody tr:hover { background-color: rgba(242,193,78,.14) !important; }
.dataTables_filter input, .dataTables_length select { background: #10261F; color: var(--text); border: 1px solid var(--gold); border-radius: 4px; }
.dataTables_wrapper .dataTables_paginate .paginate_button { color: var(--text) !important; }
.dataTables_wrapper .dataTables_paginate .paginate_button.current { background: var(--gold) !important; color: #0B1A16 !important; border: 1px solid var(--gold); }

/* ---------- Scrollbars ---------- */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #2C5A48; border-radius: 4px; }
::-webkit-scrollbar-track { background: transparent; }

/* =====================================================================
   NATIONAL FLAGS, CLUB CARDS AND EXTRA ANIMATION
   ===================================================================== */
@keyframes wave {
  0%, 100% { transform: perspective(260px) rotateY(0deg) skewY(0deg); }
  25%      { transform: perspective(260px) rotateY(-16deg) skewY(2.5deg); }
  75%      { transform: perspective(260px) rotateY(14deg) skewY(-2.5deg); }
}
@keyframes flagshine { 0% { left: -70%; } 55%, 100% { left: 130%; } }
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-40px); } to { opacity: 1; transform: none; } }
@keyframes growbar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes pulseBadge { 0% { box-shadow: 0 0 0 0 var(--cc); } 70%, 100% { box-shadow: 0 0 0 12px rgba(0,0,0,0); } }
@keyframes roll { from { left: -40px; transform: rotate(0deg); } to { left: 100%; transform: rotate(1800deg); } }
@keyframes shimmer { 0% { background-position: 0% 50%; } 100% { background-position: 200% 50%; } }
@keyframes bounceIn { 0% { transform: scale(.3); opacity: 0; } 60% { transform: scale(1.12); opacity: 1; } 100% { transform: scale(1); } }
@keyframes wiggle { 0%, 100% { transform: rotate(0); } 25% { transform: rotate(-12deg); } 75% { transform: rotate(12deg); } }

/* ---------- Waving flags ---------- */
.flag { display: inline-block; position: relative; overflow: hidden; vertical-align: middle; border-radius: 3px;
        box-shadow: 0 3px 10px rgba(0,0,0,.55); transform-origin: left center; animation: wave 3.4s ease-in-out infinite; }
.flag.ronaldo { animation-delay: -1.1s; }
.flag svg { display: block; }
.flag-shine { position: absolute; top: 0; bottom: 0; left: -70%; width: 45%;
              background: linear-gradient(100deg, transparent, rgba(255,255,255,.5), transparent);
              transform: skewX(-20deg); animation: flagshine 3.4s ease-in-out infinite; }
.sb-side .flag { display: block; margin: 2px auto 4px; }
.kpi-legend .flag, .ms-legend .flag { margin: 0 4px 0 2px; }
.ms-legend span { display: inline-flex; align-items: center; }
.ph-country { display: flex; align-items: center; gap: 10px; margin-top: 6px; font-family: 'Oswald', sans-serif;
              letter-spacing: .2em; text-transform: uppercase; font-size: 13px; color: var(--text); }
.ph-bigflag { position: absolute; right: 18px; top: 50%; margin-top: -57px; opacity: .2; pointer-events: none; }
.ph-bigflag .flag { box-shadow: none; }
.ph-name { background: linear-gradient(90deg, var(--pc), #FFFFFF, var(--pc)); background-size: 200% 100%;
           -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; text-shadow: none; animation: shimmer 5s linear infinite; }

/* ---------- Rolling ball along the navbar, bouncing score and icons ---------- */
.navbar-inverse { position: relative; }
.navbar-inverse::after { content: '\26BD'; position: absolute; bottom: -22px; left: -40px; font-size: 17px; z-index: 5;
                         pointer-events: none; animation: roll 16s linear infinite; }
.sb-score { animation: bounceIn 1s cubic-bezier(.2,1.4,.4,1) both; }
.sb-side.ronaldo .sb-score { animation-delay: .15s; }
.tile:hover svg { animation: wiggle .5s ease; }
.kpi-card:hover .kpi-val { transform: scale(1.08); }
.kpi-val { display: inline-block; transition: transform .25s ease; }

/* ---------- Club-by-club cards ---------- */
.chart-stage { overflow-y: auto; }
.club-head { display: flex; align-items: center; gap: 16px; margin: 12px 14px 4px; padding: 12px 16px; border-radius: 12px;
             border: 1px solid rgba(242,193,78,.35); background: linear-gradient(135deg, rgba(242,193,78,.16), rgba(8,24,18,.7));
             animation: slideUp .6s ease backwards; }
.ch-name { font-family: 'Oswald', sans-serif; font-size: 22px; font-weight: 700; letter-spacing: .08em; color: var(--gold); }
.ch-sub { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .1em; color: var(--muted); font-size: 14px; }
.club-list { padding: 4px 14px 14px; }
.club-card { position: relative; display: flex; gap: 14px; margin: 10px 0; padding: 12px 14px; overflow: hidden; border-radius: 12px;
             border: 1px solid var(--line); border-left: 5px solid var(--cc);
             background: linear-gradient(160deg, rgba(23,58,45,.92), rgba(12,32,25,.92));
             animation: slideInLeft .75s cubic-bezier(.2,.8,.2,1) backwards; transition: transform .25s ease, box-shadow .25s ease; }
.club-card:hover { transform: translateX(6px) scale(1.01); box-shadow: 0 10px 24px rgba(0,0,0,.5), 0 0 0 1px var(--cc); }
.club-card::after { content: ''; position: absolute; top: 0; left: -60%; width: 35%; height: 100%;
                    background: linear-gradient(100deg, transparent, rgba(255,255,255,.08), transparent);
                    transform: skewX(-20deg); animation: shine 6s ease-in-out infinite; }
.cc-badge { flex: none; width: 54px; height: 54px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
            font-family: 'Oswald', sans-serif; font-weight: 700; font-size: 15px; letter-spacing: .04em;
            background: var(--cc); color: var(--ink); border: 3px solid rgba(255,255,255,.85); animation: pulseBadge 2.6s infinite; }
.cc-main { flex: 1; min-width: 0; }
.cc-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.cc-name { font-family: 'Oswald', sans-serif; font-size: 19px; font-weight: 600; color: var(--text); letter-spacing: .04em; }
.cc-years { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .1em; color: var(--gold); font-size: 14px; }
.cc-sub { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .1em; color: var(--muted); font-size: 13px; text-transform: uppercase; }
.cc-stats { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.cc-stat { padding: 4px 10px; border-radius: 8px; background: rgba(0,0,0,.32); border: 1px solid var(--line); border-top: 2px solid var(--pc); text-align: center; min-width: 62px; }
.cc-num { display: block; font-family: 'Oswald', sans-serif; font-size: 20px; font-weight: 600; line-height: 1.15; color: var(--text); }
.cc-stat span:last-child { font-family: 'Barlow Condensed', sans-serif; letter-spacing: .1em; font-size: 10.5px; color: var(--muted); }
.cc-bar { height: 8px; margin-top: 10px; border-radius: 5px; overflow: hidden; background: rgba(255,255,255,.08); }
.cc-fill { height: 100%; transform-origin: left; border-radius: 5px; background: linear-gradient(90deg, var(--cc), var(--pc));
           animation: growbar 1.4s cubic-bezier(.2,.8,.2,1) .35s backwards; }
.cc-share { font-family: 'Barlow Condensed', sans-serif; font-size: 12px; letter-spacing: .08em; color: var(--muted); margin-top: 3px; }
.cc-trophies { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.cc-trophy { padding: 2px 9px; border-radius: 12px; font-family: 'Barlow Condensed', sans-serif; font-size: 12.5px; letter-spacing: .04em;
             color: var(--gold); border: 1px solid rgba(242,193,78,.4); background: rgba(242,193,78,.08); animation: bounceIn .6s ease .6s backwards; }

/* ---------- Pitch heatmap + scrolling club lists ---------- */
.club-scroll { max-height: calc(100vh - 235px); overflow-y: auto; overflow-x: hidden; padding-right: 6px; }
.pitch-wrap { padding: 10px 16px 14px; }
.pitch-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.pitch-title { font-family: 'Oswald', sans-serif; font-size: 20px; font-weight: 700; letter-spacing: .1em; color: var(--gold); }
.pitch-btns { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.pitch-btn { cursor: pointer; padding: 5px 14px; border-radius: 18px; border: 1px solid var(--line); background: rgba(0,0,0,.35); color: var(--text);
             font-family: 'Oswald', sans-serif; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; transition: all .2s ease; }
.pitch-btn:hover { border-color: var(--gold); color: var(--gold); transform: translateY(-2px); }
.pitch-btn.active { background: var(--gold); color: #0B1A16; border-color: var(--gold); }
.pitch-svg { display: block; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.5); animation: fadeIn .8s ease; }
.pitch-role { margin-top: 8px; font-family: 'Barlow Condensed', sans-serif; font-size: 15px; letter-spacing: .05em; color: var(--text); }
.pitch-legend { display: flex; align-items: center; gap: 10px; margin-top: 8px; font-family: 'Barlow Condensed', sans-serif; font-size: 12px; letter-spacing: .1em; color: var(--muted); }
.pitch-scale { flex: 0 0 160px; height: 10px; border-radius: 5px; background: linear-gradient(90deg, rgba(255,235,59,.1), #FFEB3B, #FF9800, #FF2A2A); }
.pitch-note { margin-top: 6px; font-size: 12px; color: var(--muted); }
```

Overview
=====================================

Column {data-width=420}
-------------------------------------

### Career scoreboard {data-height=360}

```{r scoreboard}
# Animated "final score" board: career goals, with assists and G+A underneath
tagList(
  tags$div(
    class = "scoreboard",
    tags$div(class = "sb-top", tags$span(class = "live-dot"), "Career final score"),
    tags$div(
      class = "sb-main",
      tags$div(class = "sb-side messi",
               player_avatar("Messi", 64),
               flag_tag("Messi", 36),
               tags$div(class = "sb-name", "MESSI"),
               cnt(sv("goals", "Messi"), 0, "sb-score"),
               tags$div(class = "sb-sub", "GOALS")),
      tags$div(class = "sb-vs", "VS"),
      tags$div(class = "sb-side ronaldo",
               player_avatar("Ronaldo", 64),
               flag_tag("Ronaldo", 36),
               tags$div(class = "sb-name", "RONALDO"),
               cnt(sv("goals", "Ronaldo"), 0, "sb-score"),
               tags$div(class = "sb-sub", "GOALS"))
    ),
    tags$div(
      class = "sb-foot",
      tags$div(class = "sb-chip", cnt(sv("assists", "Messi"), 0, "messi"),
               tags$span(class = "sb-chip-label", "ASSISTS"),
               cnt(sv("assists", "Ronaldo"), 0, "ronaldo")),
      tags$div(class = "sb-chip", cnt(sv("ga", "Messi"), 0, "messi"),
               tags$span(class = "sb-chip-label", "G+A"),
               cnt(sv("ga", "Ronaldo"), 0, "ronaldo"))
    )
  ),
  tags$script(HTML(count_js))
)
```

### Key performance indicators

```{r kpis}
# Section A: KPI cards (all values come from, or are recalculated from, the CSV)
tagList(
  tags$div(class = "kpi-legend",
           tags$span(class = "dot messi"), flag_tag("Messi", 24), "Lionel Messi",
           tags$span(class = "dot ronaldo"), flag_tag("Ronaldo", 24), "Cristiano Ronaldo"),
  tags$div(
    class = "kpi-grid",
    kpi_card("Total goals",                sv("goals", "Messi"),      sv("goals", "Ronaldo"),      0),
    kpi_card("Total assists",              sv("assists", "Messi"),    sv("assists", "Ronaldo"),    0),
    kpi_card("Appearances",                sv("apps", "Messi"),       sv("apps", "Ronaldo"),       0),
    kpi_card("Goal contributions (G+A)",   sv("ga", "Messi"),         sv("ga", "Ronaldo"),         0),
    kpi_card("Goals per match",            sv("g_per_app", "Messi"),  sv("g_per_app", "Ronaldo"),  2),
    kpi_card("Assists per match",          sv("a_per_app", "Messi"),  sv("a_per_app", "Ronaldo"),  2)
  ),
  tags$div(class = "kpi-note",
           "Gold underline = higher value. Per-match figures are recalculated from the raw career totals.")
)
```

Column {data-width=580}
-------------------------------------

### Goals comparison

```{r goals}
# Section B: total goals
if (has("goals")) {
  tip_goals <- function(p) paste0(
    "<b>", p, "</b><br>Total goals: ", fmt(sv("goals", p)),
    "<br>Appearances: ", fmt(sv("apps", p)),
    "<br>Goals per match: ", fmt(sv("g_per_app", p), 2),
    "<br>Goals per 90 min: ", fmt(sv("g_per90", p), 2))
  bar_two(S$goals, unname(vapply(players, tip_goals, character(1))), "Career goals")
} else note("Goals are not available in the dataset.")
```

### Assists comparison

```{r assists}
# Section C: total assists
if (has("assists")) {
  tip_assists <- function(p) paste0(
    "<b>", p, "</b><br>Total assists: ", fmt(sv("assists", p)),
    "<br>Assists per match: ", fmt(sv("a_per_app", p), 2),
    "<br>Assists per 90 min: ", fmt(sv("a_per90", p), 2),
    "<br>International assists: ", fmt(sv("intl_assists", p)))
  bar_two(S$assists, unname(vapply(players, tip_assists, character(1))), "Career assists")
} else note("Assists are not available in the dataset.")
```

Messi
=====================================

Column {data-width=400}
-------------------------------------

### Player profile {data-height=270}

```{r messi_hero}
profile_hero("Messi")
```

### Trophy cabinet

```{r messi_cabinet}
profile_cabinet("Messi")
```

Column {data-width=600}
-------------------------------------

### Goals and assists: club vs international

```{r messi_donuts}
profile_donuts("Messi")
```

### How Messi compares with Ronaldo (leader = 100)

```{r messi_index}
profile_index("Messi")
```

Ronaldo
=====================================

Column {data-width=400}
-------------------------------------

### Player profile {data-height=270}

```{r ronaldo_hero}
profile_hero("Ronaldo")
```

### Trophy cabinet

```{r ronaldo_cabinet}
profile_cabinet("Ronaldo")
```

Column {data-width=600}
-------------------------------------

### Goals and assists: club vs international

```{r ronaldo_donuts}
profile_donuts("Ronaldo")
```

### How Ronaldo compares with Messi (leader = 100)

```{r ronaldo_index}
profile_index("Ronaldo")
```

Match Centre
=====================================

Column {data-width=450}
-------------------------------------

### Match stats (who wins each category?)

```{r match_stats}
# TV-style split bars. Each bar shows the share of the two players' combined total.
# For "minutes per goal" lower is better, so the share is based on 1 / value.
tags$div(
  class = "ms-wrap",
  tags$div(class = "ms-legend", tags$span(class = "messi", flag_tag("Messi", 26), "Messi"), tags$span(class = "ronaldo", "Ronaldo", flag_tag("Ronaldo", 26))),
  stat_row("Goals",                  sv("goals", "Messi"),     sv("goals", "Ronaldo")),
  stat_row("Assists",                sv("assists", "Messi"),   sv("assists", "Ronaldo")),
  stat_row("Goals + assists",        sv("ga", "Messi"),        sv("ga", "Ronaldo")),
  stat_row("Appearances",            sv("apps", "Messi"),      sv("apps", "Ronaldo")),
  stat_row("Goals per match",        sv("g_per_app", "Messi"), sv("g_per_app", "Ronaldo"), 2),
  stat_row("G+A per 90 minutes",     sv("ga_per90", "Messi"),  sv("ga_per90", "Ronaldo"),  2),
  stat_row("Minutes per goal (lower is better)", sv("min_per_goal", "Messi"), sv("min_per_goal", "Ronaldo"), 1, invert = TRUE),
  stat_row("International goals",    sv("intl_goals", "Messi"), sv("intl_goals", "Ronaldo")),
  stat_row("Champions League goals", sv("cl_goals", "Messi"),  sv("cl_goals", "Ronaldo")),
  stat_row("World Cup goals",        sv("wc_goals", "Messi"),  sv("wc_goals", "Ronaldo")),
  stat_row("Ballon d'Or awards",     sv("ballon", "Messi"),    sv("ballon", "Ronaldo"))
)
```

Column {data-width=550}
-------------------------------------

### Player profile radar (leader of each metric = 100)

```{r radar}
# Scouting-style radar built from the indexed metrics (only metrics present in the CSV)
radar_pick  <- c("Goals", "Assists", "Goals per appearance", "Goals + assists per 90 minutes",
                 "Champions League goals", "World Cup goals", "Ballon d'Or awards", "International goals")
radar_short <- c("Goals" = "Goals", "Assists" = "Assists", "Goals per appearance" = "Goals / match",
                 "Goals + assists per 90 minutes" = "G+A / 90", "Champions League goals" = "UCL goals",
                 "World Cup goals" = "WC goals", "Ballon d'Or awards" = "Ballon d'Or",
                 "International goals" = "Intl goals")

radar_tbl <- index_tbl %>%
  filter(metric %in% radar_pick) %>%
  arrange(match(metric, radar_pick)) %>%
  mutate(label = coalesce(unname(radar_short[metric]), metric),
         Messi_txt = map2_chr(Messi, digits, fmt),
         Ronaldo_txt = map2_chr(Ronaldo, digits, fmt))

if (nrow(radar_tbl) >= 3) {
  close_poly <- function(x) c(x, x[1])          # repeat the first point to close the shape
  theta <- close_poly(radar_tbl$label)

  plot_ly() %>%
    add_trace(type = "scatterpolar", mode = "lines+markers", fill = "toself", name = "Messi",
              r = close_poly(radar_tbl$Messi_idx), theta = theta,
              line = list(color = player_cols[["Messi"]], width = 3),
              marker = list(color = player_cols[["Messi"]], size = 8),
              fillcolor = "rgba(108,195,245,0.25)", hoverinfo = "text",
              text = paste0("<b>Messi</b><br>", theta, ": ", close_poly(radar_tbl$Messi_txt),
                            "<br>Index: ", round(close_poly(radar_tbl$Messi_idx), 1))) %>%
    add_trace(type = "scatterpolar", mode = "lines+markers", fill = "toself", name = "Ronaldo",
              r = close_poly(radar_tbl$Ronaldo_idx), theta = theta,
              line = list(color = player_cols[["Ronaldo"]], width = 3),
              marker = list(color = player_cols[["Ronaldo"]], size = 8),
              fillcolor = "rgba(255,77,90,0.22)", hoverinfo = "text",
              text = paste0("<b>Ronaldo</b><br>", theta, ": ", close_poly(radar_tbl$Ronaldo_txt),
                            "<br>Index: ", round(close_poly(radar_tbl$Ronaldo_idx), 1))) %>%
    layout(
      paper_bgcolor = "rgba(0,0,0,0)",
      font = list(color = col_text, family = font_stack, size = 14),
      polar = list(
        bgcolor = "rgba(255,255,255,0.03)",
        radialaxis = list(range = c(0, 100), tickvals = c(25, 50, 75, 100),
                          gridcolor = "rgba(255,255,255,0.15)", linecolor = "rgba(255,255,255,0.2)",
                          tickfont = list(color = col_muted, size = 10)),
        angularaxis = list(gridcolor = "rgba(255,255,255,0.15)", linecolor = "rgba(255,255,255,0.2)",
                           tickfont = list(color = col_text, size = 14))
      ),
      legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.05),
      hoverlabel = list(bgcolor = col_panel, bordercolor = col_gold, font = list(color = col_text)),
      margin = list(l = 50, r = 50, t = 30, b = 30)
    ) %>%
    config(displaylogo = FALSE, modeBarButtonsToRemove = c("lasso2d", "select2d"))
} else note("Not enough comparable metrics in the dataset for a radar chart.")
```

Performance
=====================================

Column {data-width=600}
-------------------------------------

### Overall performance (each metric indexed: the leader = 100)

```{r index_chart}
# Section D: every metric in the CSV, normalised so scale differences do not distort
# (index_tbl is built in the setup chunk)
if (nrow(index_tbl) > 0) {
  idx_long <- index_tbl %>%
    pivot_longer(c(Messi_idx, Ronaldo_idx), names_to = "player", values_to = "index") %>%
    mutate(
      player = factor(str_remove(player, "_idx"), levels = players),
      raw    = if_else(player == "Messi", Messi, Ronaldo),
      raw_txt = map2_chr(raw, digits, fmt),
      metric = factor(metric, levels = rev(unique(dat$metric))),
      tip = paste0("<b>", player, "</b><br>", metric, ": ", raw_txt, " ", unit,
                   "<br>Index: ", round(index, 1),
                   if_else(lower, "<br><i>Lower is better - index inverted</i>", ""))
    )

  plot_ly(idx_long, y = ~metric, x = ~index, color = ~player, colors = player_cols,
          type = "bar", orientation = "h",
          text = ~round(index), textposition = "outside", cliponaxis = FALSE,
          textfont = list(color = col_text, size = 12),
          hovertext = ~tip, hoverinfo = "text") %>%
    style_plot(xtitle = "Index (leader of each metric = 100)",
               xaxis_extra = list(range = c(0, 125)),
               yaxis_extra = list(automargin = TRUE)) %>%
    layout(barmode = "group",
           shapes = list(list(type = "line", x0 = 100, x1 = 100, y0 = 0, y1 = 1, yref = "paper",
                              line = list(color = col_gold, dash = "dot", width = 1))))
} else note("No metrics with values for both players.")
```

Column {data-width=400}
-------------------------------------

### Big-stage numbers

```{r big_stage}
# International and trophy statistics (different scales, so each has its own axis)
big_defs <- tribble(
  ~label,                    ~col,
  "Champions League goals",  "cl_goals",
  "World Cup goals",         "wc_goals",
  "International goals",     "intl_goals",
  "Ballon d'Or awards",      "ballon"
)
big_long <- big_defs %>%
  crossing(player = players) %>%
  mutate(value = map2_dbl(col, player, sv)) %>%
  filter(!is.na(value)) %>%
  mutate(
    label  = factor(label, levels = big_defs$label),
    player = factor(player, levels = players),
    tip    = paste0("<b>", player, "</b><br>", label, ": ", map_chr(value, fmt))
  )

if (nrow(big_long) > 0) {
  g <- ggplot(big_long, aes(x = player, y = value, fill = player)) +
    geom_col(aes(text = tip), width = 0.65) +
    geom_text(aes(label = comma(value)), vjust = -0.5, color = col_text, size = 4) +
    facet_wrap(~label, scales = "free_y", ncol = 2) +
    scale_fill_manual(values = player_cols) +
    scale_y_continuous(expand = expansion(mult = c(0, 0.22))) +
    theme_dash()
  ggplotly(g, tooltip = "text") %>%
    layout(paper_bgcolor = "rgba(0,0,0,0)", plot_bgcolor = "rgba(0,0,0,0)") %>%
    config(displaylogo = FALSE, modeBarButtonsToRemove = c("lasso2d", "select2d", "autoScale2d"))
} else note("No international or award statistics in the dataset.")
```

Club vs International
=====================================

Column {data-width=500}
-------------------------------------

### Goals: club vs international

```{r split_goals}
# The dataset has career totals only (no seasons or years), so a time trend is not
# possible. This page uses the club/international split instead.
# Club = career total - international (assumes career totals include national-team games).
split_chart <- function(club, intl, ytitle) {
  total <- club + intl
  plot_ly(x = players) %>%
    add_bars(y = club, name = "Club", marker = list(color = col_club),
             text = map_chr(club, fmt), textposition = "inside", insidetextanchor = "middle",
             textfont = list(color = "#FFFFFF", size = 16), hoverinfo = "text",
             hovertext = paste0("<b>", players, "</b><br>Club: ", map_chr(club, fmt),
                                " (", percent(club / total, accuracy = 0.1), " of total)")) %>%
    add_bars(y = intl, name = "International", marker = list(color = col_gold),
             text = map_chr(intl, fmt), textposition = "inside", insidetextanchor = "middle",
             textfont = list(color = col_bg, size = 16), hoverinfo = "text",
             hovertext = paste0("<b>", players, "</b><br>International: ", map_chr(intl, fmt),
                                " (", percent(intl / total, accuracy = 0.1), " of total)")) %>%
    style_plot(ytitle = ytitle) %>%
    layout(barmode = "stack")
}

if (split_ok) {
  split_chart(S$club_goals, S$intl_goals, "Career goals")
} else note("Club/international split is not available (missing or inconsistent data).")
```

### Assists: club vs international

```{r split_assists}
if (split_ok) {
  split_chart(S$club_assists, S$intl_assists, "Career assists")
} else note("Club/international split is not available (missing or inconsistent data).")
```

Column {data-width=500}
-------------------------------------

### Output per match: club vs international

```{r split_rates}
if (split_ok && has("club_g_per_app", "intl_g_per_app", "club_a_per_app", "intl_a_per_app")) {
  rate_labels <- c("Goals per match (club)", "Goals per match (international)",
                   "Assists per match (club)", "Assists per match (international)")
  rate_cols   <- c("club_g_per_app", "intl_g_per_app", "club_a_per_app", "intl_a_per_app")
  rate_split <- tibble(label = rep(rate_labels, each = 2),
                       player = rep(players, times = 4),
                       value = unlist(lapply(rate_cols, function(cn) c(sv(cn, "Messi"), sv(cn, "Ronaldo"))))) %>%
    mutate(label = factor(label, levels = rate_labels),
           player = factor(player, levels = players),
           tip = paste0("<b>", player, "</b><br>", label, ": ", map_chr(value, fmt, digits = 2)))

  plot_ly(rate_split, x = ~label, y = ~value, color = ~player, colors = player_cols,
          type = "bar", text = ~map_chr(value, fmt, digits = 2), textposition = "outside",
          cliponaxis = FALSE, textfont = list(color = col_text, size = 14),
          hovertext = ~tip, hoverinfo = "text") %>%
    style_plot(ytitle = "Per match",
               yaxis_extra = list(range = c(0, max(rate_split$value, na.rm = TRUE) * 1.2))) %>%
    layout(barmode = "group")
} else note("Per-match club/international rates are not available.")
```

### Reading this page

* **No time series in the data.** The CSV holds career totals only (no seasons or years), so a line chart of goals and assists over time would require data that does not exist. This page shows the club/international split instead.
* **Club = career total minus international.** This assumes the career totals include national-team matches; the code checks that international figures never exceed the totals.
* **Per match** = goals (or assists) divided by appearances in that setting.

Clubs
=====================================

Column {data-width=500}
-------------------------------------

### Messi: club by club

```{r messi_clubs}
club_column("Messi")
```

Column {data-width=500}
-------------------------------------

### Ronaldo: club by club

```{r ronaldo_clubs}
club_column("Ronaldo")
```

Club Stats
=====================================

Column {data-width=500}
-------------------------------------

### Messi: goals and assists by club

```{r messi_club_chart}
club_chart("Messi")
```

### Ronaldo: goals and assists by club

```{r ronaldo_club_chart}
club_chart("Ronaldo")
```

Column {data-width=500}
-------------------------------------

### Scoring rate by club (goals per match)

```{r club_rate}
rate_d <- club_tbl %>%
  filter(!is.na(goals)) %>%
  mutate(club_f = factor(club, levels = unique(club)),
         player = factor(player, levels = players))
plot_ly(rate_d, x = ~club_f, y = ~g_app, color = ~player, colors = player_cols,
        type = "bar", text = ~sprintf("%.2f", g_app), textposition = "outside",
        cliponaxis = FALSE, textfont = list(color = col_text, size = 14),
        hovertext = ~tip, hoverinfo = "text") %>%
  style_plot(ytitle = "Goals per match",
             yaxis_extra = list(range = c(0, max(rate_d$g_app) * 1.2))) %>%
  layout(bargap = 0.25)
```

### Club-by-club details

```{r club_table}
club_dt <- club_tbl %>%
  transmute(Player = player, Club = club, Years = years, Apps = apps, Goals = goals,
            Assists = assists, `G+A` = ga, `Goals/match` = round(g_app, 2),
            `Assists/match` = round(a_app, 2),
            `Share of club goals` = ifelse(is.na(share), "n/a", paste0(round(share * 100, 1), "%")),
            Honours = gsub(";", " | ", trophies, fixed = TRUE))

datatable(club_dt, rownames = FALSE, class = "compact stripe hover",
          options = list(pageLength = 5, scrollX = TRUE, dom = "tp")) %>%
  formatStyle("Player", color = styleEqual(c("Messi", "Ronaldo"), unname(player_cols)),
              fontWeight = "bold")
```

Pitch Heatmap
=====================================

Column {data-width=500}
-------------------------------------

### Messi: where he operates on the pitch

```{r messi_pitch}
pitch_panel("Messi")
```

Column {data-width=500}
-------------------------------------

### Ronaldo: where he operates on the pitch

```{r ronaldo_pitch}
pitch_panel("Ronaldo")
```

Efficiency
=====================================

Column {data-width=600}
-------------------------------------

### Scoring and creative efficiency (per match and per 90 minutes)

```{r efficiency_rates}
# Section F: rate statistics, recalculated from goals, assists, appearances and minutes
rate_defs <- tribble(
  ~label,                ~col,          ~how,
  "Goals / match",       "g_per_app",   "goals / appearances",
  "Goals / 90",          "g_per90",     "goals / minutes x 90",
  "Assists / match",     "a_per_app",   "assists / appearances",
  "Assists / 90",        "a_per90",     "assists / minutes x 90",
  "G+A / match",         "ga_per_app",  "(goals + assists) / appearances",
  "G+A / 90",            "ga_per90",    "(goals + assists) / minutes x 90"
)
rate_long <- rate_defs %>%
  crossing(player = players) %>%
  mutate(value = map2_dbl(col, player, sv)) %>%
  filter(!is.na(value)) %>%
  mutate(label = factor(label, levels = rate_defs$label),
         player = factor(player, levels = players),
         tip = paste0("<b>", player, "</b><br>", label, ": ", map_chr(value, fmt, digits = 2),
                      "<br><i>", how, "</i>"))

if (nrow(rate_long) > 0) {
  plot_ly(rate_long, x = ~label, y = ~value, color = ~player, colors = player_cols,
          type = "bar", text = ~map_chr(value, fmt, digits = 2), textposition = "outside",
          cliponaxis = FALSE, textfont = list(color = col_text, size = 15),
          hovertext = ~tip, hoverinfo = "text") %>%
    style_plot(ytitle = "Rate",
               yaxis_extra = list(range = c(0, max(rate_long$value) * 1.18))) %>%
    layout(barmode = "group")
} else note("Not enough data (goals, assists, appearances, minutes) to calculate efficiency.")
```

Column {data-width=400}
-------------------------------------

### Minutes needed per goal contribution (lower is better)

```{r efficiency_minutes}
min_defs <- tribble(
  ~label,                ~col,              ~how,
  "Minutes per goal",    "min_per_goal",    "minutes played / goals",
  "Minutes per assist",  "min_per_assist",  "minutes played / assists",
  "Minutes per G+A",     "min_per_ga",      "minutes played / (goals + assists)"
)
min_long <- min_defs %>%
  crossing(player = players) %>%
  mutate(value = map2_dbl(col, player, sv)) %>%
  filter(!is.na(value)) %>%
  mutate(label = factor(label, levels = min_defs$label),
         player = factor(player, levels = players),
         tip = paste0("<b>", player, "</b><br>", label, ": ", map_chr(value, fmt, digits = 1),
                      "<br><i>", how, "</i>"))

if (nrow(min_long) > 0) {
  plot_ly(min_long, x = ~label, y = ~value, color = ~player, colors = player_cols,
          type = "bar", text = ~map_chr(value, fmt, digits = 1), textposition = "outside",
          cliponaxis = FALSE, textfont = list(color = col_text, size = 14),
          hovertext = ~tip, hoverinfo = "text") %>%
    style_plot(ytitle = "Minutes",
               yaxis_extra = list(range = c(0, max(min_long$value) * 1.18))) %>%
    layout(barmode = "group")
} else note("Minutes played are not available.")
```

### How to read these metrics

* **Per match** divides output by appearances. It rewards consistency but treats a 10-minute cameo like a full game.
* **Per 90** divides by minutes played and multiplies by 90, so it adjusts for different amounts of playing time.
* **Minutes per goal / assist / G+A** is the time needed for one contribution. **Lower is better.**
* **Data check:** `r validation_msg`

Data
=====================================

Column {data-width=620}
-------------------------------------

### Detailed statistics

```{r detail_table}
# Section G: interactive table. Display columns are formatted text; hidden numeric
# columns keep sorting numeric (via orderData).
stats_table <- dat %>%
  mutate(
    diff = Messi - Ronaldo,
    leader = case_when(
      is.na(Messi) | is.na(Ronaldo) ~ "n/a",
      Messi == Ronaldo              ~ "Tie",
      lower_better(metric)          ~ if_else(Messi < Ronaldo, "Messi", "Ronaldo"),
      TRUE                          ~ if_else(Messi > Ronaldo, "Messi", "Ronaldo")
    ),
    Messi_txt   = map2_chr(Messi, digits, fmt),
    Ronaldo_txt = map2_chr(Ronaldo, digits, fmt),
    diff_txt    = map2_chr(diff, digits, function(x, d) {
      if (is.na(x)) "n/a" else paste0(if (x > 0) "+" else "", fmt(x, d))
    })
  )

dt_display <- stats_table %>%
  transmute(
    Metric = metric, Unit = unit,
    Messi = Messi_txt, Ronaldo = Ronaldo_txt,
    `Difference (Messi - Ronaldo)` = diff_txt,
    Leader = leader,
    Messi_num = Messi, Ronaldo_num = Ronaldo, Diff_num = diff
  )

datatable(
  dt_display,
  rownames = FALSE,
  class = "compact stripe hover",
  options = list(
    pageLength = 10,
    lengthMenu = c(5, 10, 15),
    order = list(),
    columnDefs = list(
      list(orderData = 6, targets = 2),
      list(orderData = 7, targets = 3),
      list(orderData = 8, targets = 4),
      list(visible = FALSE, targets = 6:8),
      list(className = "dt-right", targets = 2:4),
      list(className = "dt-center", targets = 5)
    )
  )
) %>%
  formatStyle("Messi", color = player_cols[["Messi"]], fontWeight = "bold") %>%
  formatStyle("Ronaldo", color = player_cols[["Ronaldo"]], fontWeight = "bold") %>%
  formatStyle("Leader",
              color = styleEqual(c("Messi", "Ronaldo", "Tie", "n/a"),
                                 c(player_cols[["Messi"]], player_cols[["Ronaldo"]], col_gold, col_muted)),
              fontWeight = "bold")
```

Column {data-width=380}
-------------------------------------

### Validation of pre-computed statistics

```{r validation_table}
# Reported value in the CSV vs value recalculated from raw totals
val_display <- validation %>%
  transmute(Player = player, Metric = metric, Reported, Recomputed, Status = status)

datatable(
  val_display,
  rownames = FALSE,
  class = "compact stripe",
  options = list(dom = "t", paging = FALSE, ordering = FALSE)
) %>%
  formatStyle("Status",
              color = styleEqual(c("Match", "Mismatch", "Not checked"),
                                 c("#4CAF50", "#FF4D5A", col_muted)),
              fontWeight = "bold")
```

### About the data

```{r about_data, results='asis'}
photo_msg <- paste(vapply(players, function(p) paste0(
  full_name[[p]], ": ", if (is.na(photo_src[[p]])) "no photo found (avatar shown)" else "photo found"),
  character(1)), collapse = "; ")
cat(
  "* **File:** `", data_file, "`\n",
  "* **Format:** wide - one row per statistic, one column per player (", nrow(dat), " statistics).\n",
  "* **Scope:** career totals only; there are no seasons, years or competitions by season.\n",
  "* **Clubs pages:** club-by-club figures are typed into `club_tbl` in the setup chunk (not in the CSV); they are senior-team, all-competition numbers and approximate for current clubs (Inter Miami, Al Nassr). Edit that table to update them.\n",
  "* **Recalculated:** per-match, per-90 and minutes-per metrics are derived from the raw totals.\n",
  "* **Validation:** ", validation_msg, "\n",
  "* **Photos:** ", photo_msg, ". To add photos, save `images/messi.jpg` and `images/ronaldo.jpg` next to this file and knit again.\n",
  sep = "")
```