Cinematic UI overhaul: focus states, full-bleed hero, score bug cards, grid Intel

Phase 1 - Focus & Typography: New TVFocusButtonStyle with 1.04x scale +
white glow border on focus, 0.97x press. Enforced 22px minimum text on
tvOS across DesignSystem (tvCaption 22px, tvBody 24px, tvDataValue 24px).
DataLabelStyle uses tvOS caption with reduced kerning.

Phase 2 - Today Tab: FeaturedGameCard redesigned as full-bleed hero with
away team left, home team right, 96pt score centered, DiamondView for
live count/outs. Removed side panel, replaced with single subtitle row.
GameCardView redesigned as horizontal score-bug style (~120px tall vs
320px) with team color accent bar, stacked logos, centered score, inline
mini linescore. Both show record + streak on every card.

Phase 3 - Intel Tab: Side-by-side layout on tvOS with standings
horizontal scroll on left (60%) and leaders vertical column on right
(40%). Both visible without scrolling past each other. iOS keeps the
stacked layout.

Phase 4 - Feed: Cards now horizontal with thumbnail left (300px tvOS),
info right. Added timestamp ("2h ago") to every card. All text meets
22px minimum on tvOS. Condensed game badge uses larger font.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-12 13:57:17 -05:00
parent 39092e5f3d
commit 69d84fd09b
7 changed files with 503 additions and 790 deletions

View File

@@ -7,14 +7,9 @@ struct FeaturedGameCard: View {
private var awayColor: Color { TeamAssets.color(for: game.awayTeam.code) }
private var homeColor: Color { TeamAssets.color(for: game.homeTeam.code) }
private var hasLinescore: Bool {
!game.status.isScheduled && (game.linescore?.hasData ?? false)
}
private var awayPitcherName: String? {
guard let pitchers = game.pitchers else { return nil }
let parts = pitchers.components(separatedBy: " vs ")
return parts.first
return pitchers.components(separatedBy: " vs ").first
}
private var homePitcherName: String? {
@@ -26,370 +21,229 @@ struct FeaturedGameCard: View {
var body: some View {
Button(action: onSelect) {
VStack(spacing: 0) {
ViewThatFits {
HStack(alignment: .top, spacing: 28) {
matchupColumn
.frame(maxWidth: .infinity, alignment: .leading)
// Full-bleed matchup area
ZStack {
heroBackground
sidePanel
.frame(width: 760, alignment: .leading)
VStack(spacing: heroSpacing) {
// Status + game type
HStack {
Text((game.gameType ?? "Featured").uppercased())
.font(labelFont)
.foregroundStyle(.white.opacity(0.5))
.kerning(1.5)
Spacer()
statusChip
}
// Main matchup: Away Score Home
HStack(spacing: 0) {
teamSide(
team: game.awayTeam,
color: awayColor,
pitcherURL: game.awayPitcherHeadshotURL,
pitcherName: awayPitcherName,
alignment: .leading
)
.frame(maxWidth: .infinity)
scoreCenter
.frame(width: scoreCenterWidth)
teamSide(
team: game.homeTeam,
color: homeColor,
pitcherURL: game.homePitcherHeadshotURL,
pitcherName: homePitcherName,
alignment: .trailing
)
.frame(maxWidth: .infinity)
}
// Subtitle line: venue + coverage
subtitleRow
}
VStack(alignment: .leading, spacing: 24) {
matchupColumn
sidePanel
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.horizontal, 34)
.padding(.top, 30)
.padding(.bottom, 24)
footerBar
.padding(.horizontal, 34)
.padding(.vertical, 16)
.background(.white.opacity(0.04))
}
.background(cardBackground)
.overlay(alignment: .top) {
HStack(spacing: 0) {
Rectangle()
.fill(awayColor.opacity(0.92))
Rectangle()
.fill(homeColor.opacity(0.92))
}
.frame(height: 5)
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
}
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 28, style: .continuous)
.strokeBorder(borderColor, lineWidth: borderWidth)
)
.shadow(color: shadowColor, radius: 24, y: 10)
}
.platformCardStyle()
}
@ViewBuilder
private var matchupColumn: some View {
VStack(alignment: .leading, spacing: 18) {
headerBar
featuredTeamRow(
team: game.awayTeam,
color: awayColor,
pitcherURL: game.awayPitcherHeadshotURL,
pitcherName: awayPitcherName
)
scorePanel
featuredTeamRow(
team: game.homeTeam,
color: homeColor,
pitcherURL: game.homePitcherHeadshotURL,
pitcherName: homePitcherName
)
}
}
@ViewBuilder
private var headerBar: some View {
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text((game.gameType ?? "Featured Game").uppercased())
.font(.system(size: 13, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(0.6))
.kerning(1.8)
if let venue = game.venue {
Label(venue, systemImage: "mappin.and.ellipse")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.white.opacity(0.8))
}
}
Spacer()
HStack(spacing: 10) {
if game.hasStreams {
headerChip(title: "WATCH", color: .blue)
} else if game.isBlackedOut {
headerChip(title: "BLACKOUT", color: .red)
.padding(.horizontal, heroPadH)
.padding(.vertical, heroPadV)
}
statusChip
}
}
}
@ViewBuilder
private func featuredTeamRow(team: TeamInfo, color: Color, pitcherURL: URL?, pitcherName: String?) -> some View {
HStack(spacing: 18) {
TeamLogoView(team: team, size: 82)
.frame(width: 88, height: 88)
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 12) {
Text(team.code)
.font(.system(size: 34, weight: .black, design: .rounded))
.foregroundStyle(.white)
if let record = team.record {
Text(record)
.font(.system(size: 13, weight: .bold, design: .monospaced))
.foregroundStyle(.white.opacity(0.72))
.padding(.horizontal, 11)
.padding(.vertical, 6)
.background(.white.opacity(0.08))
.clipShape(Capsule())
}
}
Text(team.displayName)
.font(.system(size: 26, weight: .bold))
.foregroundStyle(.white.opacity(0.96))
.lineLimit(1)
.minimumScaleFactor(0.82)
if let standing = team.standingSummary {
Text(standing)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(.white.opacity(0.58))
.lineLimit(1)
}
}
Spacer(minLength: 14)
if pitcherURL != nil || pitcherName != nil {
HStack(spacing: 12) {
if pitcherURL != nil {
PitcherHeadshotView(
url: pitcherURL,
teamCode: team.code,
name: nil,
size: 46
)
}
VStack(alignment: .trailing, spacing: 4) {
Text("Probable")
.font(.system(size: 12, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(0.42))
.textCase(.uppercase)
Text(pitcherName ?? "TBD")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.white.opacity(0.86))
.lineLimit(1)
}
}
}
}
.padding(.horizontal, 22)
.padding(.vertical, 20)
.background {
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(teamPanelBackground(color: color))
}
.overlay {
RoundedRectangle(cornerRadius: 24, style: .continuous)
.strokeBorder(.white.opacity(0.08), lineWidth: 1)
}
}
@ViewBuilder
private var scorePanel: some View {
VStack(spacing: 10) {
if let summary = scoreSummaryText {
Text(summary)
.font(.system(size: 74, weight: .black, design: .rounded))
.foregroundStyle(.white)
.monospacedDigit()
.contentTransition(.numericText())
} else {
Text(game.status.label)
.font(.system(size: 54, weight: .black, design: .rounded))
.foregroundStyle(.white)
.multilineTextAlignment(.center)
}
Text(stateHeadline)
.font(.system(size: 22, weight: .semibold, design: .rounded))
.foregroundStyle(stateHeadlineColor)
if let detail = stateDetailText {
Text(detail)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(.white.opacity(0.6))
.lineLimit(2)
.multilineTextAlignment(.center)
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal, 24)
.padding(.vertical, 26)
.background {
RoundedRectangle(cornerRadius: 26, style: .continuous)
.fill(.white.opacity(0.06))
}
.overlay {
RoundedRectangle(cornerRadius: 26, style: .continuous)
.strokeBorder(.white.opacity(0.08), lineWidth: 1)
}
}
@ViewBuilder
private var sidePanel: some View {
VStack(alignment: .leading, spacing: 18) {
HStack(spacing: 14) {
detailTile(
title: "Venue",
value: game.venue ?? "TBD",
icon: "mappin.and.ellipse",
accent: nil
)
detailTile(
title: "Pitching",
value: game.pitchers ?? "Pitchers TBD",
icon: "baseball",
accent: nil
)
detailTile(
title: "Coverage",
value: coverageSummary,
icon: coverageIconName,
accent: coverageAccent
)
}
if hasLinescore,
let linescore = game.linescore {
VStack(alignment: .leading, spacing: 14) {
HStack {
Text("Linescore")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(.white)
Spacer()
Text(linescoreLabel)
.font(.system(size: 13, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(0.56))
}
// Linescore bar (if game started)
if let linescore = game.linescore, !game.status.isScheduled {
LinescoreView(
linescore: linescore,
awayCode: game.awayTeam.code,
homeCode: game.homeTeam.code
)
.padding(.horizontal, heroPadH)
.padding(.vertical, 16)
.background(.black.opacity(0.3))
}
.padding(20)
.background(panelBackground)
}
.clipShape(RoundedRectangle(cornerRadius: heroRadius, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: heroRadius, style: .continuous)
.strokeBorder(borderColor, lineWidth: game.isLive ? 2 : 1)
)
.shadow(color: game.isLive ? .red.opacity(0.2) : .black.opacity(0.3), radius: 24, y: 10)
}
.platformCardStyle()
}
// MARK: - Team Side
@ViewBuilder
private func teamSide(
team: TeamInfo,
color: Color,
pitcherURL: URL?,
pitcherName: String?,
alignment: HorizontalAlignment
) -> some View {
VStack(alignment: alignment, spacing: teamInfoGap) {
TeamLogoView(team: team, size: logoSize)
VStack(alignment: alignment, spacing: 4) {
Text(team.code)
.font(teamCodeFont)
.foregroundStyle(.white)
Text(team.displayName)
.font(teamNameFont)
.foregroundStyle(.white.opacity(0.8))
.lineLimit(1)
}
if let record = team.record {
HStack(spacing: 6) {
Text(record)
.font(recordFont)
.foregroundStyle(.white.opacity(0.6))
if let streak = team.streak {
Text(streak)
.font(recordFont)
.foregroundStyle(streak.hasPrefix("W") ? DS.Colors.positive : DS.Colors.live)
}
}
}
if let name = pitcherName {
VStack(alignment: alignment, spacing: 2) {
Text("PROBABLE")
.font(probableLabelFont)
.foregroundStyle(.white.opacity(0.35))
.kerning(1)
HStack(spacing: 6) {
if let url = pitcherURL {
PitcherHeadshotView(url: url, teamCode: team.code, name: nil, size: pitcherHeadshotSize)
}
Text(name)
.font(pitcherNameFont)
.foregroundStyle(.white.opacity(0.8))
.lineLimit(1)
}
}
}
}
}
// MARK: - Score Center
@ViewBuilder
private var scoreCenter: some View {
VStack(spacing: 8) {
if let away = game.awayTeam.score, let home = game.homeTeam.score {
Text("\(away) - \(home)")
.font(mainScoreFont)
.foregroundStyle(.white)
.monospacedDigit()
.contentTransition(.numericText())
} else {
fallbackInfoPanel
Text(game.status.label)
.font(statusTimeFont)
.foregroundStyle(.white)
}
if case .live = game.status {
if let inning = game.currentInningDisplay {
Text(inning)
.font(inningFont)
.foregroundStyle(DS.Colors.live)
}
// Count + outs for live games
if let linescore = game.linescore {
DiamondView(
balls: linescore.balls ?? 0,
strikes: linescore.strikes ?? 0,
outs: linescore.outs ?? 0
)
}
}
}
}
@ViewBuilder
private func detailTile(title: String, value: String, icon: String, accent: Color?) -> some View {
VStack(alignment: .leading, spacing: 10) {
Label(title, systemImage: icon)
.font(.system(size: 12, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(0.52))
Text(value)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(accent ?? .white)
.lineLimit(2)
.minimumScaleFactor(0.82)
}
.frame(maxWidth: .infinity, minHeight: 102, alignment: .topLeading)
.padding(18)
.background(panelBackground)
}
// MARK: - Subtitle Row
@ViewBuilder
private var fallbackInfoPanel: some View {
VStack(alignment: .leading, spacing: 12) {
Text(hasLinescore ? "Game Flow" : "Matchup Notes")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(.white)
Text(fallbackSummary)
.font(.system(size: 18, weight: .medium))
.foregroundStyle(.white.opacity(0.78))
.lineLimit(3)
private var subtitleRow: some View {
HStack(spacing: 16) {
if let venue = game.venue {
Text(venue)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.white.opacity(0.52))
Label(venue, systemImage: "mappin.and.ellipse")
.font(subtitleFont)
.foregroundStyle(.white.opacity(0.5))
.lineLimit(1)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(22)
.background(panelBackground)
}
@ViewBuilder
private var footerBar: some View {
HStack(spacing: 14) {
Label(footerSummary, systemImage: footerIconName)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(.white.opacity(0.72))
.lineLimit(1)
Spacer()
HStack(spacing: 8) {
Image(systemName: game.hasStreams ? "play.fill" : "rectangle.and.text.magnifyingglass")
.font(.system(size: 13, weight: .bold))
Text(game.hasStreams ? "Watch Game" : "Open Matchup")
.font(.system(size: 15, weight: .bold))
if game.hasStreams {
Label("\(game.broadcasts.count) feed\(game.broadcasts.count == 1 ? "" : "s")", systemImage: "tv.fill")
.font(subtitleFont)
.foregroundStyle(DS.Colors.interactive.opacity(0.9))
} else if game.isBlackedOut {
Label("Blacked Out", systemImage: "eye.slash.fill")
.font(subtitleFont)
.foregroundStyle(DS.Colors.live.opacity(0.8))
}
.foregroundStyle(game.hasStreams ? .blue : .white.opacity(0.82))
}
}
// MARK: - Status Chip
@ViewBuilder
private var statusChip: some View {
switch game.status {
case .live(let inning):
HStack(spacing: 8) {
Circle()
.fill(.red)
.frame(width: 8, height: 8)
HStack(spacing: 6) {
Circle().fill(DS.Colors.live).frame(width: 8, height: 8)
Text(inning ?? "LIVE")
.font(.system(size: 15, weight: .black, design: .rounded))
.font(chipFont)
.foregroundStyle(.white)
}
.padding(.horizontal, 15)
.padding(.vertical, 10)
.background(.red.opacity(0.18))
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(DS.Colors.live.opacity(0.2))
.clipShape(Capsule())
case .scheduled(let time):
Text(time)
.font(.system(size: 15, weight: .black, design: .rounded))
.font(chipFont)
.foregroundStyle(.white)
.padding(.horizontal, 15)
.padding(.vertical, 10)
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(.white.opacity(0.1))
.clipShape(Capsule())
case .final_:
Text("FINAL")
.font(.system(size: 15, weight: .black, design: .rounded))
.foregroundStyle(.white.opacity(0.96))
.padding(.horizontal, 15)
.padding(.vertical, 10)
.background(.white.opacity(0.12))
.font(chipFont)
.foregroundStyle(.white)
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(.white.opacity(0.1))
.clipShape(Capsule())
case .unknown:
@@ -397,196 +251,85 @@ struct FeaturedGameCard: View {
}
}
@ViewBuilder
private func headerChip(title: String, color: Color) -> some View {
Text(title)
.font(.system(size: 13, weight: .bold, design: .rounded))
.foregroundStyle(color)
.padding(.horizontal, 12)
.padding(.vertical, 9)
.background(color.opacity(0.12))
.clipShape(Capsule())
}
private var scoreSummaryText: String? {
guard let away = game.awayTeam.score, let home = game.homeTeam.score else { return nil }
return "\(away) - \(home)"
}
private var stateHeadline: String {
switch game.status {
case .scheduled:
return "First Pitch"
case .live(let inning):
return inning ?? "Live"
case .final_:
return "Final"
case .unknown:
return "Game Day"
}
}
private var stateHeadlineColor: Color {
switch game.status {
case .live:
return .red.opacity(0.92)
case .scheduled:
return .blue.opacity(0.92)
case .final_:
return .white.opacity(0.82)
case .unknown:
return .white.opacity(0.72)
}
}
private var stateDetailText: String? {
if game.status.isScheduled {
return game.pitchers ?? game.venue
}
return game.venue
}
private var coverageSummary: String {
if game.isBlackedOut {
return "Unavailable in your area"
}
if !game.broadcasts.isEmpty {
let names = game.broadcasts.map(\.displayLabel)
return names.joined(separator: " / ")
}
if game.status.isScheduled {
return "Feeds closer to game time"
}
return "No feeds listed"
}
private var coverageIconName: String {
if game.isBlackedOut { return "eye.slash.fill" }
if !game.broadcasts.isEmpty { return "tv.fill" }
return "dot.radiowaves.left.and.right"
}
private var coverageAccent: Color {
if game.isBlackedOut { return .red }
if !game.broadcasts.isEmpty { return .blue }
return .white
}
private var fallbackSummary: String {
if let pitchers = game.pitchers, !pitchers.isEmpty {
return pitchers
}
if game.isBlackedOut {
return "This game is blacked out in your area."
}
if !game.broadcasts.isEmpty {
return game.broadcasts.map(\.displayLabel).joined(separator: " / ")
}
return "Select the matchup to view streams and full game details."
}
private var footerSummary: String {
if game.isBlackedOut {
return "Blackout restrictions apply"
}
if !game.broadcasts.isEmpty {
let feeds = game.broadcasts.map(\.teamCode).joined(separator: " / ")
return "Coverage: \(feeds)"
}
return game.venue ?? "Game details"
}
private var footerIconName: String {
if game.isBlackedOut { return "eye.slash.fill" }
if !game.broadcasts.isEmpty { return "tv.fill" }
return "sportscourt.fill"
}
private var linescoreLabel: String {
if let inning = game.currentInningDisplay, !inning.isEmpty {
return inning.uppercased()
}
if game.isFinal {
return "FINAL"
}
return "GAME"
}
private func teamPanelBackground(color: Color) -> some ShapeStyle {
LinearGradient(
colors: [
color.opacity(0.22),
.white.opacity(0.05)
],
startPoint: .leading,
endPoint: .trailing
)
}
// MARK: - Background
@ViewBuilder
private var panelBackground: some View {
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(.black.opacity(0.22))
.overlay {
RoundedRectangle(cornerRadius: 24, style: .continuous)
.strokeBorder(.white.opacity(0.08), lineWidth: 1)
}
}
@ViewBuilder
private var cardBackground: some View {
private var heroBackground: some View {
ZStack {
RoundedRectangle(cornerRadius: 28, style: .continuous)
.fill(Color(red: 0.05, green: 0.07, blue: 0.11))
// Base
Color(red: 0.04, green: 0.05, blue: 0.08)
LinearGradient(
colors: [
awayColor.opacity(0.2),
Color(red: 0.05, green: 0.07, blue: 0.11),
homeColor.opacity(0.22)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
// Team color washes
HStack(spacing: 0) {
awayColor.opacity(0.25)
Color.clear
homeColor.opacity(0.25)
}
// Glow orbs
Circle()
.fill(awayColor.opacity(0.2))
.frame(width: 400, height: 400)
.blur(radius: 100)
.offset(x: -300, y: -50)
Circle()
.fill(awayColor.opacity(0.18))
.frame(width: 320, height: 320)
.blur(radius: 64)
.offset(x: -280, y: -70)
Circle()
.fill(homeColor.opacity(0.18))
.frame(width: 360, height: 360)
.blur(radius: 72)
.offset(x: 320, y: 40)
Rectangle()
.fill(.white.opacity(0.03))
.frame(width: 1)
.padding(.vertical, 28)
.offset(x: 86)
.fill(homeColor.opacity(0.2))
.frame(width: 400, height: 400)
.blur(radius: 100)
.offset(x: 300, y: 50)
}
}
private var borderColor: Color {
if game.isLive {
return .red.opacity(0.32)
}
if game.hasStreams {
return .blue.opacity(0.24)
}
return .white.opacity(0.08)
if game.isLive { return DS.Colors.live.opacity(0.35) }
if game.hasStreams { return DS.Colors.interactive.opacity(0.2) }
return .white.opacity(0.06)
}
private var borderWidth: CGFloat {
game.isLive || game.hasStreams ? 2 : 1
}
// MARK: - Platform Sizing
private var shadowColor: Color {
if game.isLive {
return .red.opacity(0.18)
}
return .black.opacity(0.26)
}
#if os(tvOS)
private var heroRadius: CGFloat { 28 }
private var heroPadH: CGFloat { 50 }
private var heroPadV: CGFloat { 40 }
private var heroSpacing: CGFloat { 24 }
private var logoSize: CGFloat { 120 }
private var scoreCenterWidth: CGFloat { 280 }
private var teamInfoGap: CGFloat { 12 }
private var pitcherHeadshotSize: CGFloat { 36 }
private var mainScoreFont: Font { .system(size: 96, weight: .black, design: .rounded) }
private var statusTimeFont: Font { .system(size: 48, weight: .black, design: .rounded) }
private var inningFont: Font { .system(size: 24, weight: .bold, design: .rounded) }
private var teamCodeFont: Font { .system(size: 38, weight: .black, design: .rounded) }
private var teamNameFont: Font { .system(size: 24, weight: .bold) }
private var recordFont: Font { .system(size: 22, weight: .bold, design: .monospaced) }
private var subtitleFont: Font { .system(size: 22, weight: .medium) }
private var chipFont: Font { .system(size: 22, weight: .black, design: .rounded) }
private var labelFont: Font { .system(size: 22, weight: .bold, design: .rounded) }
private var probableLabelFont: Font { .system(size: 18, weight: .bold, design: .rounded) }
private var pitcherNameFont: Font { .system(size: 22, weight: .semibold) }
#else
private var heroRadius: CGFloat { 22 }
private var heroPadH: CGFloat { 24 }
private var heroPadV: CGFloat { 24 }
private var heroSpacing: CGFloat { 16 }
private var logoSize: CGFloat { 64 }
private var scoreCenterWidth: CGFloat { 160 }
private var teamInfoGap: CGFloat { 8 }
private var pitcherHeadshotSize: CGFloat { 28 }
private var mainScoreFont: Font { .system(size: 56, weight: .black, design: .rounded) }
private var statusTimeFont: Font { .system(size: 32, weight: .black, design: .rounded) }
private var inningFont: Font { .system(size: 16, weight: .bold, design: .rounded) }
private var teamCodeFont: Font { .system(size: 24, weight: .black, design: .rounded) }
private var teamNameFont: Font { .system(size: 16, weight: .bold) }
private var recordFont: Font { .system(size: 13, weight: .bold, design: .monospaced) }
private var subtitleFont: Font { .system(size: 14, weight: .medium) }
private var chipFont: Font { .system(size: 14, weight: .black, design: .rounded) }
private var labelFont: Font { .system(size: 13, weight: .bold, design: .rounded) }
private var probableLabelFont: Font { .system(size: 11, weight: .bold, design: .rounded) }
private var pitcherNameFont: Font { .system(size: 14, weight: .semibold) }
#endif
}