type: custom:button-card
entity: sensor.espn_mlb_scoreboard_raw
show_name: false
show_icon: false
show_state: false
triggers_update:
- sensor.espn_mlb_scoreboard_raw
variables:
src: sensor.espn_mlb_scoreboard_raw
fav: ATL
mlb_logo: https://a.espncdn.com/i/teamlogos/leagues/500/mlb.png
styles:
card:
- padding: 0
- border-radius: 34px
- overflow: hidden
- background: "#efe1bd"
- box-shadow: 0 18px 40px rgba(0,0,0,.48)
grid:
- grid-template-areas: "\"main\""
- grid-template-columns: 1fr
custom_fields:
main:
- width: 100%
- padding: 0
custom_fields:
main: |
[[[
const st = states[variables.src];
const events = st?.attributes?.events || [];
const fav = String(variables.fav || 'ATL').toUpperCase();
const getComp = e => e?.competitions?.[0] || {};
const ev = events.find(e =>
(getComp(e).competitors || []).some(t => t.team?.abbreviation === fav)
) || events[0];
if (!ev) return `<div class="retro-shell"><div class="retro-card"><div class="empty">No MLB games found</div></div></div>`;
const comp = getComp(ev);
const teams = comp.competitors || [];
const away = teams.find(t => t.homeAway === 'away');
const home = teams.find(t => t.homeAway === 'home');
if (!away || !home) return `<div class="retro-shell"><div class="retro-card"><div class="empty">Game data unavailable</div></div></div>`;
const type = comp.status?.type || ev.status?.type || {};
const state = type.state || 'pre';
const badge =
state === 'in' ? 'LIVE' :
state === 'post' ? 'FINAL' :
'UP NEXT';
const logo = t =>
t.team?.logo ||
t.team?.logos?.[0]?.href ||
t.team?.logos?.[0]?.url ||
'';
const record = t => t.records?.[0]?.summary || '';
const getStat = (t, keys) => {
const stats = t.statistics || [];
for (const k of keys) {
const found = stats.find(s =>
String(s.name || '').toLowerCase() === k ||
String(s.abbreviation || '').toLowerCase() === k
);
if (found?.displayValue !== undefined) return found.displayValue;
}
return '0';
};
const hits = t => getStat(t, ['hits', 'h']);
const errors = t => getStat(t, ['errors', 'e']);
const line = t => {
const arr = Array.from({ length: 9 }, () => '0');
(t.linescores || []).forEach(ls => {
const i = Number(ls.period) - 1;
if (i >= 0 && i < 9) arr[i] = ls.displayValue ?? ls.value ?? '0';
});
return arr;
};
const teamName = t => {
const name = t.team?.displayName || t.team?.shortDisplayName || '';
return name.toUpperCase();
};
const collectStrings = (obj, out = []) => {
if (!obj) return out;
if (typeof obj === 'string') {
out.push(obj);
return out;
}
if (Array.isArray(obj)) {
obj.forEach(x => collectStrings(x, out));
return out;
}
if (typeof obj === 'object') {
Object.entries(obj).forEach(([key, value]) => {
const k = String(key).toLowerCase();
if (
k.includes('winning') ||
k.includes('win') ||
k.includes('losing') ||
k.includes('loss') ||
k.includes('wp') ||
k.includes('lp') ||
k.includes('pitcher')
) {
if (typeof value === 'string') out.push(`${key}: ${value}`);
else collectStrings(value, out);
} else if (
k === 'description' ||
k === 'displayvalue' ||
k === 'displayname' ||
k === 'headline' ||
k === 'text' ||
k === 'summary' ||
k === 'name' ||
k === 'shortname'
) {
if (typeof value === 'string') out.push(value);
} else if (typeof value === 'object') {
collectStrings(value, out);
}
});
}
return out;
};
const cleanPitcherName = txt => {
if (!txt) return '';
return String(txt)
.replace(/^winning pitcher\s*[:\-]\s*/i, '')
.replace(/^losing pitcher\s*[:\-]\s*/i, '')
.replace(/^winner\s*[:\-]\s*/i, '')
.replace(/^loser\s*[:\-]\s*/i, '')
.replace(/^wp\s*[:\-]\s*/i, '')
.replace(/^lp\s*[:\-]\s*/i, '')
.replace(/\s*\([^)]*\)\s*$/g, '')
.trim();
};
const findPitcherDecision = (kind) => {
const target = kind === 'win' ? 'winning' : 'losing';
const short = kind === 'win' ? 'wp' : 'lp';
const directCandidates = [
comp?.situation?.[`${target}Pitcher`],
comp?.situation?.[`${kind}Pitcher`],
ev?.situation?.[`${target}Pitcher`],
ev?.situation?.[`${kind}Pitcher`],
comp?.[`${target}Pitcher`],
comp?.[`${kind}Pitcher`],
ev?.[`${target}Pitcher`],
ev?.[`${kind}Pitcher`],
st?.attributes?.[`${target}_pitcher`],
st?.attributes?.[`${kind}_pitcher`],
st?.attributes?.[`${short}`]
];
for (const item of directCandidates) {
const name =
item?.athlete?.shortName ||
item?.athlete?.displayName ||
item?.shortName ||
item?.displayName ||
item?.name ||
item;
if (typeof name === 'string' && name.trim()) return cleanPitcherName(name);
}
const searchArea = [
comp?.details,
comp?.notes,
comp?.headlines,
comp?.leaders,
comp?.situation,
ev?.details,
ev?.notes,
ev?.headlines,
ev?.leaders,
ev?.competitions,
st?.attributes
];
const strings = collectStrings(searchArea, []);
const patterns = kind === 'win'
? [
/(?:winning pitcher|winner|wp)\s*[:\-]\s*([^,;|]+)/i,
/\bW\s*[:\-]\s*([^,;|]+)/i
]
: [
/(?:losing pitcher|loser|lp)\s*[:\-]\s*([^,;|]+)/i,
/\bL\s*[:\-]\s*([^,;|]+)/i
];
for (const txt of strings) {
for (const pattern of patterns) {
const match = String(txt).match(pattern);
if (match?.[1]) return cleanPitcherName(match[1]);
}
}
const allObjects = [
...(Array.isArray(comp?.details) ? comp.details : []),
...(Array.isArray(comp?.notes) ? comp.notes : []),
...(Array.isArray(ev?.details) ? ev.details : []),
...(Array.isArray(ev?.notes) ? ev.notes : [])
];
for (const obj of allObjects) {
const typeText = String(obj?.type || obj?.name || obj?.label || '').toLowerCase();
if (
typeText.includes(target) ||
typeText.includes(`${target}pitcher`) ||
typeText === short
) {
const val =
obj?.athlete?.shortName ||
obj?.athlete?.displayName ||
obj?.player?.shortName ||
obj?.player?.displayName ||
obj?.displayValue ||
obj?.displayName ||
obj?.description ||
obj?.headline ||
obj?.text ||
obj?.name;
if (val) return cleanPitcherName(val);
}
}
return '';
};
const fallbackWinner =
away.winner ? away.team?.shortDisplayName :
home.winner ? home.team?.shortDisplayName :
'β';
const fallbackLoser =
away.winner ? home.team?.shortDisplayName :
home.winner ? away.team?.shortDisplayName :
'β';
const winningPitcher = findPitcherDecision('win');
const losingPitcher = findPitcherDecision('loss');
const winDisplay = winningPitcher || fallbackWinner;
const lossDisplay = losingPitcher || fallbackLoser;
const decisionReady = state === 'post' || away.winner || home.winner || winningPitcher || losingPitcher;
return `
<div class="retro-shell">
<div class="retro-card">
<div class="header-section">
<div class="top-title">
<span>β
</span>
<b>MAJOR LEAGUE BASEBALL</b>
<span>β
</span>
</div>
<div class="stripe-row">
<i></i>
<div class="mlb-mark">
<img src="${variables.mlb_logo}">
</div>
<i></i>
</div>
</div>
<div class="status-row">
<span>β
</span>
<div class="status-pill">${badge}</div>
<span>β
</span>
</div>
<div class="matchup">
<div class="team-card away">
<div class="logo-circle navy">
${logo(away) ? `<img src="${logo(away)}">` : ''}
</div>
<div class="abbr">${away.team?.abbreviation || 'AWY'}</div>
<div class="team-name">${teamName(away)}</div>
<div class="record">${record(away) || ''}</div>
</div>
<div class="score-area">
<div class="score-grid">
<div class="score navy-text">${away.score ?? '0'}</div>
<div class="score-divider"></div>
<div class="score red-text">${home.score ?? '0'}</div>
</div>
<div class="vs-box"><span>VS</span></div>
</div>
<div class="team-card home">
<div class="logo-circle red-bg">
${logo(home) ? `<img src="${logo(home)}">` : ''}
</div>
<div class="abbr red-text">${home.team?.abbreviation || 'HME'}</div>
<div class="team-name red-text">${teamName(home)}</div>
<div class="record">${record(home) || ''}</div>
</div>
</div>
<div class="linescore">
<div class="ls-row head">
<b></b>
${[1,2,3,4,5,6,7,8,9].map(i => `<span>${i}</span>`).join('')}
<strong>R</strong>
<strong>H</strong>
<strong>E</strong>
</div>
<div class="ls-row">
<b>${away.team?.abbreviation || 'AWY'}</b>
${line(away).map(x => `<span>${x}</span>`).join('')}
<strong>${away.score ?? '0'}</strong>
<strong>${hits(away)}</strong>
<strong>${errors(away)}</strong>
</div>
<div class="ls-row home-row">
<b>${home.team?.abbreviation || 'HME'}</b>
${line(home).map(x => `<span>${x}</span>`).join('')}
<strong>${home.score ?? '0'}</strong>
<strong>${hits(home)}</strong>
<strong>${errors(home)}</strong>
</div>
</div>
<div class="award-row">
<div class="award">
<small>β
WINNING PITCHER β
</small>
<b>${winDisplay}</b>
<span>${decisionReady ? (winningPitcher ? 'Pitching Decision' : 'Winning Team') : 'Pending'}</span>
</div>
<div class="ball-wrap">
<div class="burst"></div>
<div class="baseball">βΎ</div>
</div>
<div class="award">
<small>β
LOSING PITCHER β
</small>
<b>${lossDisplay}</b>
<span>${decisionReady ? (losingPitcher ? 'Pitching Decision' : 'Losing Team') : 'Pending'}</span>
</div>
</div>
<div class="bottom-band">
<span>β
β
β
GREAT MOMENTS.</span>
<b>EST.<br>1869</b>
<span>TIMELESS TRADITION. β
β
β
</span>
</div>
</div>
</div>
`;
]]]
card_mod:
style: |
* {
box-sizing: border-box;
}
.retro-shell {
padding: 14px;
background:
radial-gradient(circle at 20% 10%, rgba(255,255,255,.38), transparent 18%),
radial-gradient(circle at 80% 82%, rgba(255,255,255,.22), transparent 22%),
linear-gradient(180deg, #f9edcd 0%, #ead19c 100%);
color: #0e2a44;
font-family: Georgia, "Times New Roman", serif;
overflow: hidden;
}
.retro-card {
position: relative;
overflow: hidden;
padding: 30px 18px 20px;
border-radius: 30px;
border: 7px solid #0e2a44;
background:
radial-gradient(rgba(14,42,68,.11) 1px, transparent 1px),
radial-gradient(rgba(194,56,51,.10) 1px, transparent 1px),
linear-gradient(180deg, #faedcc, #efd8a6);
background-size: 10px 10px, 14px 14px, auto;
box-shadow:
inset 0 0 0 2px #c23833,
inset 0 0 0 12px rgba(255,255,255,.22),
0 14px 26px rgba(0,0,0,.20);
}
.retro-card::before {
content: "";
position: absolute;
inset: 14px;
border: 1px solid rgba(194,56,51,.55);
border-radius: 20px;
pointer-events: none;
z-index: 1;
}
.header-section,
.status-row,
.matchup,
.linescore,
.award-row,
.bottom-band {
position: relative;
z-index: 2;
}
.header-section {
display: grid;
grid-template-rows: auto auto;
gap: 8px;
margin-bottom: 10px;
}
.top-title {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 0 40px 4px 40px;
color: #0e2a44;
font-size: 10px;
font-weight: 900;
letter-spacing: .12em;
white-space: nowrap;
overflow: visible;
text-align: center;
}
.top-title b {
min-width: 0;
overflow: visible;
text-overflow: clip;
white-space: nowrap;
}
.top-title span {
color: #c23833;
flex: 0 0 auto;
font-size: 14px;
}
.stripe-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 8px;
margin: 0 32px 4px;
}
.stripe-row i {
height: 20px;
background:
linear-gradient(
#c23833 0 5px,
transparent 5px 8px,
#0e2a44 8px 13px,
transparent 13px 16px,
#c23833 16px 20px
);
}
.mlb-mark {
width: 72px;
height: 46px;
border-radius: 7px;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
flex: 0 0 auto;
}
.mlb-mark img {
width: 72px;
height: 46px;
object-fit: contain;
display: block;
filter: drop-shadow(0 1px 1px rgba(0,0,0,.2));
}
.status-row {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
color: #c23833;
font-size: 22px;
font-weight: 900;
margin-top: 2px;
margin-bottom: 12px;
}
.status-pill {
min-width: 176px;
padding: 8px 30px;
border-radius: 999px;
background: #0e2a44;
color: #f8ebc9;
text-align: center;
font-size: 18px;
font-weight: 900;
letter-spacing: .22em;
white-space: nowrap;
}
.matchup {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(120px, 160px) minmax(0, 1fr);
gap: 4px;
align-items: center;
text-align: center;
margin-bottom: 18px;
}
.team-card {
min-width: 0;
overflow: visible;
display: grid;
justify-items: center;
}
.logo-circle {
width: 86px;
height: 86px;
margin: 0 auto 12px;
border-radius: 50%;
border: 2px solid #b8322e;
outline: 2px solid #0e2a44;
outline-offset: 4px;
background: #0e2a44;
padding: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.logo-circle.navy {
background: #0e2a44;
color: #0e2a44;
}
.logo-circle.red-bg {
background: #c23833;
color: #c23833;
}
.logo-circle img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 1px 1px rgba(0,0,0,.18));
}
.abbr {
font-size: 40px;
line-height: .9;
font-weight: 900;
letter-spacing: .03em;
white-space: nowrap;
font-family: Georgia, "Times New Roman", serif;
}
.red-text {
color: #c23833;
}
.team-name {
margin-top: 6px;
color: #c23833;
max-width: 100%;
font-size: 11px;
font-weight: 900;
letter-spacing: .05em;
text-transform: uppercase;
white-space: normal;
overflow: hidden;
text-align: center;
font-family: Georgia, "Times New Roman", serif;
}
.record {
margin-top: 9px;
color: #0e2a44;
font-family: Georgia, "Times New Roman", serif;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
position: relative;
display: inline-flex;
align-items: center;
gap: 7px;
}
.record::before,
.record::after {
content: "";
width: 28px;
height: 1px;
background: rgba(14,42,68,.75);
display: block;
}
.score-area {
position: relative;
min-width: 0;
display: grid;
place-items: center;
}
.score-grid {
width: 100%;
display: grid;
grid-template-columns: 1fr 1px 1fr;
align-items: center;
gap: 10px;
}
.score {
font-family: Impact, "Arial Narrow", "Roboto Condensed", sans-serif;
font-size: 50px;
line-height: .78;
font-weight: 10;
letter-spacing: .01em;
transform: scaleY(2.2);
transform-origin: center;
}
.navy-text {
color: #0e2a44;
}
.score-divider {
width: 1px;
height: 120px;
background: rgba(14,42,68,.56);
}
.vs-box {
position: absolute;
width: 38px;
height: 38px;
transform: rotate(45deg);
border: 2px solid rgba(14,42,68,.72);
background: #f8ebc9;
color: #0e2a44;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-style: italic;
font-weight: 900;
}
.vs-box span {
display: block;
transform: rotate(-45deg);
}
.linescore {
width: 100%;
border: 3px solid #0e2a44;
border-radius: 14px;
overflow: hidden;
background: rgba(255,255,255,.14);
margin-bottom: 22px;
}
.ls-row {
display: grid;
grid-template-columns: 70px repeat(9, minmax(14px, 1fr)) 30px 30px 30px;
border-top: 1px solid rgba(14,42,68,.42);
}
.ls-row.head {
border-top: 0;
}
.ls-row > * {
min-width: 0;
padding: 8px 2px;
text-align: center;
border-right: 1px solid rgba(14,42,68,.34);
font-size: 14px;
line-height: 1;
font-weight: 900;
white-space: nowrap;
overflow: hidden;
}
.ls-row b {
background: #0e2a44;
color: #f8ebc9;
font-size: 17px;
}
.ls-row.home-row b {
background: #c23833;
}
.award-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 72px minmax(0, 1fr);
align-items: center;
gap: 16px;
text-align: center;
margin-bottom: 22px;
}
.award {
min-width: 0;
border: 1px solid rgba(14,42,68,.45);
padding: 13px 10px;
min-height: 84px;
}
.award small {
display: block;
color: #c23833;
font-size: 12px;
letter-spacing: .09em;
font-weight: 900;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.award b {
display: block;
margin-top: 7px;
font-size: 20px;
font-weight: 900;
color: #0e2a44;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.award span {
display: block;
margin-top: 5px;
font-family: Arial, sans-serif;
color: rgba(14,42,68,.65);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.ball-wrap {
position: relative;
width: 72px;
height: 72px;
display: grid;
place-items: center;
}
.burst {
position: absolute;
inset: 0;
background:
conic-gradient(
from 0deg,
transparent 0 8deg,
rgba(14,42,68,.28) 8deg 10deg,
transparent 10deg 22deg
);
border-radius: 50%;
opacity: .75;
}
.baseball {
position: relative;
z-index: 1;
font-size: 42px;
filter: drop-shadow(0 2px 2px rgba(0,0,0,.18));
}
.bottom-band {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 12px;
background: #0e2a44;
color: #f8ebc9;
border-top: 5px solid #c23833;
border-bottom: 5px solid #c23833;
padding: 11px 14px;
font-size: 7px;
font-weight: 900;
letter-spacing: .10em;
overflow: hidden;
}
.bottom-band span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.bottom-band b {
background: #f8ebc9;
color: #c23833;
border: 1px solid rgba(14,42,68,.45);
padding: 7px 14px;
font-size: 13px;
line-height: 1.05;
white-space: nowrap;
text-align: center;
}