Files
Sportstime/docs/plans/2026-01-16-brutalist-app-wide-implementation.md
Trey t 8bba5a1592 docs(plans): add brutalist app-wide implementation plan
Detailed step-by-step plan for extending brutalist style to:
- TripDetailView
- SavedTripsListView
- ScheduleListView
- SettingsView

Includes StyleProvider protocol, adaptive routers, and complete
code snippets for each task.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 09:13:10 -06:00

35 KiB

Brutalist App-Wide Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Extend the Brutalist design style from home-screen-only to work app-wide (TripDetailView, ScheduleListView, SavedTripsListView, SettingsView).

Architecture: Adaptive view routers that switch between Classic and Brutalist variants based on DesignStyleManager.shared.currentStyle. StyleProvider protocol provides shared styling constants. Brutalist uses Theme colors but with monospace fonts, sharp corners, and borders.

Tech Stack: SwiftUI, @Observable pattern, Theme system, DesignStyleManager


Task 1: Create StyleProvider Protocol

Files:

  • Create: SportsTime/Core/Design/StyleProvider.swift

Step 1: Create the StyleProvider file

//
//  StyleProvider.swift
//  SportsTime
//
//  Provides style-specific constants for shared components.
//

import SwiftUI

/// Protocol for style-specific visual properties
protocol StyleProvider {
    // Shape
    var cornerRadius: CGFloat { get }
    var borderWidth: CGFloat { get }

    // Typography
    var fontDesign: Font.Design { get }
    var usesUppercase: Bool { get }
    var headerTracking: CGFloat { get }

    // Effects
    var usesGradientBackgrounds: Bool { get }
    var usesSoftShadows: Bool { get }

    // Helpers
    func flatBackground(_ colorScheme: ColorScheme) -> Color
}

// MARK: - Classic Style

struct ClassicStyle: StyleProvider {
    let cornerRadius: CGFloat = 12
    let borderWidth: CGFloat = 0
    let fontDesign: Font.Design = .default
    let usesUppercase = false
    let headerTracking: CGFloat = 0
    let usesGradientBackgrounds = true
    let usesSoftShadows = true

    func flatBackground(_ colorScheme: ColorScheme) -> Color {
        Theme.cardBackground(colorScheme)
    }
}

// MARK: - Brutalist Style

struct BrutalistStyle: StyleProvider {
    let cornerRadius: CGFloat = 0
    let borderWidth: CGFloat = 1
    let fontDesign: Font.Design = .monospaced
    let usesUppercase = true
    let headerTracking: CGFloat = 2
    let usesGradientBackgrounds = false
    let usesSoftShadows = false

    func flatBackground(_ colorScheme: ColorScheme) -> Color {
        colorScheme == .dark ? .black : Color(white: 0.95)
    }
}

// MARK: - UIDesignStyle Extension

extension UIDesignStyle {
    /// Returns the appropriate StyleProvider for this design style
    var styleProvider: StyleProvider {
        switch self {
        case .brutalist:
            return BrutalistStyle()
        default:
            return ClassicStyle()
        }
    }
}

Step 2: Verify the file compiles

Run: xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20 Expected: BUILD SUCCEEDED

Step 3: Commit

git add SportsTime/Core/Design/StyleProvider.swift
git commit -m "$(cat <<'EOF'
feat(design): add StyleProvider protocol for style-specific constants

Introduces ClassicStyle and BrutalistStyle implementations for shared
component styling across the app.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"

Task 2: Create TripDetailView Brutalist Variant

Files:

  • Create: SportsTime/Features/Trip/Views/Variants/Brutalist/TripDetailView_Brutalist.swift
  • Create: SportsTime/Features/Trip/Views/Variants/Classic/TripDetailView_Classic.swift
  • Create: SportsTime/Features/Trip/Views/AdaptiveTripDetailView.swift
  • Modify: SportsTime/Features/Trip/Views/TripDetailView.swift (extract to Classic)

Step 1: Create directory structure

mkdir -p SportsTime/Features/Trip/Views/Variants/Classic
mkdir -p SportsTime/Features/Trip/Views/Variants/Brutalist

Step 2: Create AdaptiveTripDetailView router

Create SportsTime/Features/Trip/Views/AdaptiveTripDetailView.swift:

//
//  AdaptiveTripDetailView.swift
//  SportsTime
//
//  Routes to the appropriate trip detail variant based on the selected design style.
//

import SwiftUI

struct AdaptiveTripDetailView: View {
    let trip: Trip
    let games: [String: RichGame]?

    /// Initialize with trip and games dictionary
    init(trip: Trip, games: [String: RichGame]) {
        self.trip = trip
        self.games = games
    }

    /// Initialize with just trip - games will be loaded from AppDataProvider
    init(trip: Trip) {
        self.trip = trip
        self.games = nil
    }

    var body: some View {
        switch DesignStyleManager.shared.currentStyle {
        case .brutalist:
            if let games = games {
                TripDetailView_Brutalist(trip: trip, games: games)
            } else {
                TripDetailView_Brutalist(trip: trip)
            }
        default:
            if let games = games {
                TripDetailView_Classic(trip: trip, games: games)
            } else {
                TripDetailView_Classic(trip: trip)
            }
        }
    }
}

Step 3: Copy existing TripDetailView to Classic variant

Copy TripDetailView.swift to Variants/Classic/TripDetailView_Classic.swift and rename the struct to TripDetailView_Classic.

Key changes:

  • Rename struct TripDetailViewstruct TripDetailView_Classic
  • Rename struct DaySectionstruct DaySection_Classic
  • Rename struct GameRowstruct GameRow_Classic
  • Rename struct TravelSectionstruct TravelSection_Classic
  • Rename struct EVChargerRowstruct EVChargerRow_Classic
  • Keep struct ShareSheet as-is (shared utility)
  • Keep enum ItinerarySection as-is (shared model)
  • Remove the #Preview at the bottom

Step 4: Create TripDetailView_Brutalist

Create SportsTime/Features/Trip/Views/Variants/Brutalist/TripDetailView_Brutalist.swift:

//
//  TripDetailView_Brutalist.swift
//  SportsTime
//
//  BRUTALIST: Raw, unpolished, anti-design trip details.
//  Monospace typography, harsh borders, ticket stub aesthetics.
//

import SwiftUI
import SwiftData
import MapKit

struct TripDetailView_Brutalist: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.colorScheme) private var colorScheme

    let trip: Trip
    private let providedGames: [String: RichGame]?

    @Query private var savedTrips: [SavedTrip]
    @State private var showProPaywall = false
    @State private var showExportSheet = false
    @State private var exportURL: URL?
    @State private var isExporting = false
    @State private var exportProgress: PDFAssetPrefetcher.PrefetchProgress?
    @State private var mapCameraPosition: MapCameraPosition = .automatic
    @State private var isSaved = false
    @State private var routePolylines: [MKPolyline] = []
    @State private var isLoadingRoutes = false
    @State private var loadedGames: [String: RichGame] = [:]
    @State private var isLoadingGames = false

    private let exportService = ExportService()
    private let dataProvider = AppDataProvider.shared

    private var games: [String: RichGame] {
        providedGames ?? loadedGames
    }

    private var bgColor: Color {
        colorScheme == .dark ? .black : Color(white: 0.95)
    }

    private var textColor: Color {
        colorScheme == .dark ? .white : .black
    }

    init(trip: Trip, games: [String: RichGame]) {
        self.trip = trip
        self.providedGames = games
    }

    init(trip: Trip) {
        self.trip = trip
        self.providedGames = nil
    }

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {
                // HERO MAP - Sharp edges
                heroMapSection
                    .frame(height: 280)

                // HEADER
                tripHeader
                    .padding(.horizontal, 16)
                    .padding(.top, 24)

                perforatedDivider
                    .padding(.vertical, 24)

                // STATS ROW
                statsRow
                    .padding(.horizontal, 16)

                perforatedDivider
                    .padding(.vertical, 24)

                // SCORE CARD
                if let score = trip.score {
                    scoreCard(score)
                        .padding(.horizontal, 16)

                    perforatedDivider
                        .padding(.vertical, 24)
                }

                // ITINERARY
                itinerarySection
                    .padding(.horizontal, 16)
                    .padding(.bottom, 32)
            }
        }
        .background(bgColor)
        .toolbar {
            ToolbarItemGroup(placement: .primaryAction) {
                ShareButton(trip: trip, style: .icon)
                    .foregroundStyle(Theme.warmOrange)

                Button {
                    if StoreManager.shared.isPro {
                        Task { await exportPDF() }
                    } else {
                        showProPaywall = true
                    }
                } label: {
                    HStack(spacing: 2) {
                        Image(systemName: "doc.fill")
                        if !StoreManager.shared.isPro {
                            ProBadge()
                        }
                    }
                    .foregroundStyle(Theme.warmOrange)
                }
            }
        }
        .sheet(isPresented: $showExportSheet) {
            if let url = exportURL {
                ShareSheet(items: [url])
            }
        }
        .sheet(isPresented: $showProPaywall) {
            PaywallView()
        }
        .onAppear { checkIfSaved() }
        .task { await loadGamesIfNeeded() }
        .overlay {
            if isExporting {
                exportProgressOverlay
            }
        }
    }

    // MARK: - Perforated Divider

    private var perforatedDivider: some View {
        HStack(spacing: 8) {
            ForEach(0..<25, id: \.self) { _ in
                Circle()
                    .fill(textColor.opacity(0.3))
                    .frame(width: 6, height: 6)
            }
        }
        .frame(maxWidth: .infinity)
        .padding(.horizontal, 16)
    }

    // MARK: - Hero Map Section

    private var heroMapSection: some View {
        ZStack(alignment: .topTrailing) {
            Map(position: $mapCameraPosition, interactionModes: []) {
                ForEach(stopCoordinates.indices, id: \.self) { index in
                    let stop = stopCoordinates[index]
                    Annotation(stop.name, coordinate: stop.coordinate) {
                        Rectangle()
                            .fill(index == 0 ? Theme.warmOrange : Theme.routeGold)
                            .frame(width: 12, height: 12)
                    }
                }

                ForEach(routePolylines.indices, id: \.self) { index in
                    MapPolyline(routePolylines[index])
                        .stroke(Theme.routeGold, lineWidth: 3)
                }
            }
            .mapStyle(.standard(elevation: .flat))

            // Save button - sharp rectangle
            Button { toggleSaved() } label: {
                Image(systemName: isSaved ? "heart.fill" : "heart")
                    .font(.title3)
                    .foregroundStyle(isSaved ? .red : textColor)
                    .padding(12)
                    .background(bgColor)
                    .border(textColor.opacity(0.3), width: 1)
            }
            .padding(12)
        }
        .task {
            updateMapRegion()
            await fetchDrivingRoutes()
        }
    }

    // MARK: - Header

    private var tripHeader: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Date range
            Text(trip.formattedDateRange.uppercased())
                .font(.system(.caption, design: .monospaced))
                .foregroundStyle(textColor.opacity(0.5))
                .tracking(2)

            // Route as arrow chain
            Text(trip.stops.map { $0.city.uppercased() }.joined(separator: " → "))
                .font(.system(.title3, design: .monospaced).bold())
                .foregroundStyle(textColor)
                .lineLimit(2)

            // Sport badges - bordered rectangles
            HStack(spacing: 8) {
                ForEach(Array(trip.uniqueSports), id: \.self) { sport in
                    HStack(spacing: 4) {
                        Image(systemName: sport.iconName)
                            .font(.caption2)
                        Text(sport.rawValue.uppercased())
                            .font(.system(.caption2, design: .monospaced))
                    }
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .foregroundStyle(sport.themeColor)
                    .border(sport.themeColor.opacity(0.5), width: 1)
                }
            }
        }
    }

    // MARK: - Stats Row

    private var statsRow: some View {
        HStack(spacing: 12) {
            brutalistStat(value: "\(trip.tripDuration)", label: "DAYS")
            brutalistStat(value: "\(trip.stops.count)", label: "CITIES")
            brutalistStat(value: "\(trip.totalGames)", label: "GAMES")
        }
    }

    private func brutalistStat(value: String, label: String) -> some View {
        VStack(spacing: 4) {
            Text(value)
                .font(.system(.title, design: .monospaced).bold())
                .foregroundStyle(textColor)
            Text("[ \(label) ]")
                .font(.system(.caption2, design: .monospaced))
                .foregroundStyle(textColor.opacity(0.5))
        }
        .frame(maxWidth: .infinity)
        .padding(.vertical, 16)
        .border(textColor.opacity(0.2), width: 1)
    }

    // MARK: - Score Card

    private func scoreCard(_ score: TripScore) -> some View {
        VStack(spacing: 16) {
            HStack {
                Text("[ TRIP SCORE ]")
                    .font(.system(.caption, design: .monospaced))
                    .foregroundStyle(textColor.opacity(0.5))
                Spacer()
                Text(score.scoreGrade)
                    .font(.system(size: 48, weight: .black, design: .monospaced))
                    .foregroundStyle(Theme.warmOrange)
            }

            HStack(spacing: 8) {
                scoreItem(label: "GAMES", value: score.gameQualityScore)
                scoreItem(label: "ROUTE", value: score.routeEfficiencyScore)
                scoreItem(label: "BALANCE", value: score.leisureBalanceScore)
                scoreItem(label: "PREFS", value: score.preferenceAlignmentScore)
            }
        }
        .padding(16)
        .border(textColor.opacity(0.2), width: 2)
    }

    private func scoreItem(label: String, value: Double) -> some View {
        VStack(spacing: 4) {
            Text(String(format: "%.0f", value))
                .font(.system(.headline, design: .monospaced).bold())
                .foregroundStyle(textColor)
            Text(label)
                .font(.system(.caption2, design: .monospaced))
                .foregroundStyle(textColor.opacity(0.4))
        }
        .frame(maxWidth: .infinity)
    }

    // MARK: - Itinerary

    private var itinerarySection: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("[ ITINERARY ]")
                .font(.system(.caption, design: .monospaced))
                .foregroundStyle(textColor.opacity(0.5))

            if isLoadingGames {
                Text("LOADING...")
                    .font(.system(.body, design: .monospaced))
                    .foregroundStyle(textColor.opacity(0.5))
                    .frame(maxWidth: .infinity)
                    .padding(.vertical, 32)
            } else {
                ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in
                    switch section {
                    case .day(let dayNumber, let date, let gamesOnDay):
                        DaySection_Brutalist(
                            dayNumber: dayNumber,
                            date: date,
                            games: gamesOnDay
                        )
                    case .travel(let segment):
                        TravelSection_Brutalist(segment: segment)
                    }
                }
            }
        }
    }

    // MARK: - Export Progress Overlay

    private var exportProgressOverlay: some View {
        ZStack {
            Color.black.opacity(0.8)
                .ignoresSafeArea()

            VStack(spacing: 16) {
                Text("CREATING PDF")
                    .font(.system(.headline, design: .monospaced))
                    .foregroundStyle(.white)

                Text(exportProgress?.currentStep.uppercased() ?? "PREPARING...")
                    .font(.system(.caption, design: .monospaced))
                    .foregroundStyle(.white.opacity(0.7))

                if let progress = exportProgress {
                    Text("\(Int(progress.percentComplete * 100))%")
                        .font(.system(.title, design: .monospaced).bold())
                        .foregroundStyle(Theme.warmOrange)
                }
            }
            .padding(32)
            .border(Color.white.opacity(0.3), width: 2)
        }
    }

    // MARK: - Computed Properties (same as Classic)

    private var stopCoordinates: [(name: String, coordinate: CLLocationCoordinate2D)] {
        trip.stops.compactMap { stop -> (String, CLLocationCoordinate2D)? in
            if let coord = stop.coordinate { return (stop.city, coord) }
            if let stadiumId = stop.stadium,
               let stadium = dataProvider.stadium(for: stadiumId) {
                return (stadium.name, stadium.coordinate)
            }
            return nil
        }
    }

    private var itinerarySections: [ItinerarySection] {
        // Same logic as Classic variant
        var sections: [ItinerarySection] = []
        var dayCitySections: [(dayNumber: Int, date: Date, city: String, games: [RichGame])] = []
        let days = tripDays

        for (index, dayDate) in days.enumerated() {
            let dayNum = index + 1
            let gamesOnDay = gamesOn(date: dayDate)
            guard !gamesOnDay.isEmpty else { continue }

            var gamesByCity: [(city: String, games: [RichGame])] = []
            for game in gamesOnDay {
                let city = game.stadium.city
                if let lastIndex = gamesByCity.indices.last, gamesByCity[lastIndex].city == city {
                    gamesByCity[lastIndex].games.append(game)
                } else {
                    gamesByCity.append((city, [game]))
                }
            }

            for cityGroup in gamesByCity {
                dayCitySections.append((dayNum, dayDate, cityGroup.city, cityGroup.games))
            }
        }

        for (index, section) in dayCitySections.enumerated() {
            if index > 0 {
                let prevCity = dayCitySections[index - 1].city
                let currentCity = section.city
                if !prevCity.isEmpty && !currentCity.isEmpty && prevCity != currentCity {
                    if let travelSegment = findTravelSegment(from: prevCity, to: currentCity) {
                        sections.append(.travel(travelSegment))
                    }
                }
            }
            sections.append(.day(dayNumber: section.dayNumber, date: section.date, games: section.games))
        }

        return sections
    }

    private var tripDays: [Date] {
        let calendar = Calendar.current
        guard let startDate = trip.stops.first?.arrivalDate,
              let endDate = trip.stops.last?.departureDate else { return [] }

        var days: [Date] = []
        var current = calendar.startOfDay(for: startDate)
        let end = calendar.startOfDay(for: endDate)

        while current <= end {
            days.append(current)
            current = calendar.date(byAdding: .day, value: 1, to: current)!
        }
        return days
    }

    private func gamesOn(date: Date) -> [RichGame] {
        let calendar = Calendar.current
        let dayStart = calendar.startOfDay(for: date)
        let allGameIds = trip.stops.flatMap { $0.games }
        let foundGames = allGameIds.compactMap { games[$0] }

        return foundGames.filter { richGame in
            calendar.startOfDay(for: richGame.game.dateTime) == dayStart
        }.sorted { $0.game.dateTime < $1.game.dateTime }
    }

    private func findTravelSegment(from fromCity: String, to toCity: String) -> TravelSegment? {
        let fromLower = fromCity.lowercased().trimmingCharacters(in: .whitespaces)
        let toLower = toCity.lowercased().trimmingCharacters(in: .whitespaces)

        return trip.travelSegments.first { segment in
            let segmentFrom = segment.fromLocation.name.lowercased().trimmingCharacters(in: .whitespaces)
            let segmentTo = segment.toLocation.name.lowercased().trimmingCharacters(in: .whitespaces)
            return segmentFrom == fromLower && segmentTo == toLower
        }
    }

    // MARK: - Actions (same as Classic)

    private func loadGamesIfNeeded() async {
        guard providedGames == nil else { return }
        let gameIds = trip.stops.flatMap { $0.games }
        guard !gameIds.isEmpty else { return }

        isLoadingGames = true
        var loaded: [String: RichGame] = [:]
        for gameId in gameIds {
            if let game = try? await dataProvider.fetchGame(by: gameId),
               let richGame = dataProvider.richGame(from: game) {
                loaded[gameId] = richGame
            }
        }
        loadedGames = loaded
        isLoadingGames = false
    }

    private func exportPDF() async {
        isExporting = true
        exportProgress = nil

        do {
            let url = try await exportService.exportToPDF(trip: trip, games: games) { progress in
                await MainActor.run { self.exportProgress = progress }
            }
            exportURL = url
            showExportSheet = true
        } catch { }

        isExporting = false
    }

    private func toggleSaved() {
        if isSaved { unsaveTrip() } else { saveTrip() }
    }

    private func saveTrip() {
        if !StoreManager.shared.isPro && savedTrips.count >= StoreManager.freeTripLimit {
            showProPaywall = true
            return
        }

        guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else { return }
        modelContext.insert(savedTrip)

        if let _ = try? modelContext.save() {
            withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
                isSaved = true
            }
        }
    }

    private func unsaveTrip() {
        let tripId = trip.id
        let descriptor = FetchDescriptor<SavedTrip>(predicate: #Predicate { $0.id == tripId })

        if let savedTrips = try? modelContext.fetch(descriptor) {
            for savedTrip in savedTrips { modelContext.delete(savedTrip) }
            try? modelContext.save()
            withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
                isSaved = false
            }
        }
    }

    private func checkIfSaved() {
        let tripId = trip.id
        let descriptor = FetchDescriptor<SavedTrip>(predicate: #Predicate { $0.id == tripId })
        if let count = try? modelContext.fetchCount(descriptor), count > 0 {
            isSaved = true
        }
    }

    private func updateMapRegion() {
        guard !stopCoordinates.isEmpty else { return }

        let coordinates = stopCoordinates.map(\.coordinate)
        let lats = coordinates.map(\.latitude)
        let lons = coordinates.map(\.longitude)

        guard let minLat = lats.min(), let maxLat = lats.max(),
              let minLon = lons.min(), let maxLon = lons.max() else { return }

        let center = CLLocationCoordinate2D(
            latitude: (minLat + maxLat) / 2,
            longitude: (minLon + maxLon) / 2
        )

        let latSpan = (maxLat - minLat) * 1.3 + 0.5
        let lonSpan = (maxLon - minLon) * 1.3 + 0.5

        mapCameraPosition = .region(MKCoordinateRegion(
            center: center,
            span: MKCoordinateSpan(latitudeDelta: max(latSpan, 1), longitudeDelta: max(lonSpan, 1))
        ))
    }

    private func fetchDrivingRoutes() async {
        let stops = stopCoordinates
        guard stops.count >= 2 else { return }

        isLoadingRoutes = true
        var polylines: [MKPolyline] = []

        for i in 0..<(stops.count - 1) {
            let source = stops[i]
            let destination = stops[i + 1]

            let request = MKDirections.Request()
            let sourceLocation = CLLocation(latitude: source.coordinate.latitude, longitude: source.coordinate.longitude)
            let destLocation = CLLocation(latitude: destination.coordinate.latitude, longitude: destination.coordinate.longitude)
            request.source = MKMapItem(location: sourceLocation, address: nil)
            request.destination = MKMapItem(location: destLocation, address: nil)
            request.transportType = .automobile

            let directions = MKDirections(request: request)

            do {
                let response = try await directions.calculate()
                if let route = response.routes.first {
                    polylines.append(route.polyline)
                }
            } catch {
                let straightLine = MKPolyline(coordinates: [source.coordinate, destination.coordinate], count: 2)
                polylines.append(straightLine)
            }
        }

        routePolylines = polylines
        isLoadingRoutes = false
    }
}

// MARK: - Day Section (Brutalist)

struct DaySection_Brutalist: View {
    let dayNumber: Int
    let date: Date
    let games: [RichGame]
    @Environment(\.colorScheme) private var colorScheme

    private var textColor: Color {
        colorScheme == .dark ? .white : .black
    }

    private var formattedDate: String {
        date.formatted(.dateTime.weekday(.wide).month().day()).uppercased()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // Day header
            HStack {
                Text("DAY \(dayNumber)")
                    .font(.system(.title2, design: .monospaced).bold())
                    .foregroundStyle(textColor)

                Spacer()

                Text("\(games.count) GAME\(games.count > 1 ? "S" : "")")
                    .font(.system(.caption, design: .monospaced))
                    .foregroundStyle(Theme.warmOrange)
            }

            Text(formattedDate)
                .font(.system(.caption, design: .monospaced))
                .foregroundStyle(textColor.opacity(0.5))
                .tracking(1)

            // City
            if let city = games.first?.stadium.city {
                HStack(spacing: 4) {
                    Image(systemName: "mappin")
                        .font(.caption2)
                    Text(city.uppercased())
                        .font(.system(.caption, design: .monospaced))
                }
                .foregroundStyle(textColor.opacity(0.6))
            }

            // Games
            ForEach(games, id: \.game.id) { richGame in
                GameRow_Brutalist(game: richGame)
            }
        }
        .padding(16)
        .border(textColor.opacity(0.2), width: 1)
    }
}

// MARK: - Game Row (Brutalist)

struct GameRow_Brutalist: View {
    let game: RichGame
    @Environment(\.colorScheme) private var colorScheme

    private var textColor: Color {
        colorScheme == .dark ? .white : .black
    }

    var body: some View {
        HStack(spacing: 12) {
            // Sport indicator - vertical bar
            Rectangle()
                .fill(game.game.sport.themeColor)
                .frame(width: 4)

            VStack(alignment: .leading, spacing: 4) {
                // Matchup
                HStack(spacing: 8) {
                    Text(game.awayTeam.abbreviation)
                        .font(.system(.body, design: .monospaced).bold())
                    Text("@")
                        .font(.system(.caption, design: .monospaced))
                        .foregroundStyle(textColor.opacity(0.5))
                    Text(game.homeTeam.abbreviation)
                        .font(.system(.body, design: .monospaced).bold())
                }
                .foregroundStyle(textColor)

                // Stadium
                Text(game.stadium.name.uppercased())
                    .font(.system(.caption2, design: .monospaced))
                    .foregroundStyle(textColor.opacity(0.5))
            }

            Spacer()

            // Time
            Text(game.localGameTimeShort)
                .font(.system(.subheadline, design: .monospaced).bold())
                .foregroundStyle(Theme.warmOrange)
        }
        .padding(12)
        .background(textColor.opacity(0.03))
        .border(textColor.opacity(0.1), width: 1)
    }
}

// MARK: - Travel Section (Brutalist)

struct TravelSection_Brutalist: View {
    let segment: TravelSegment
    @Environment(\.colorScheme) private var colorScheme

    private var textColor: Color {
        colorScheme == .dark ? .white : .black
    }

    var body: some View {
        VStack(spacing: 0) {
            // Connector
            Rectangle()
                .fill(Theme.routeGold)
                .frame(width: 2, height: 20)

            // Travel card
            HStack(spacing: 12) {
                Image(systemName: "car.fill")
                    .foregroundStyle(Theme.routeGold)

                VStack(alignment: .leading, spacing: 2) {
                    Text("[ TRAVEL ]")
                        .font(.system(.caption2, design: .monospaced))
                        .foregroundStyle(textColor.opacity(0.4))

                    Text("\(segment.fromLocation.name.uppercased())\(segment.toLocation.name.uppercased())")
                        .font(.system(.caption, design: .monospaced))
                        .foregroundStyle(textColor)
                }

                Spacer()

                VStack(alignment: .trailing, spacing: 2) {
                    Text(segment.formattedDistance.uppercased())
                        .font(.system(.caption, design: .monospaced).bold())
                    Text(segment.formattedDuration.uppercased())
                        .font(.system(.caption2, design: .monospaced))
                        .foregroundStyle(textColor.opacity(0.5))
                }
                .foregroundStyle(textColor)
            }
            .padding(12)
            .border(Theme.routeGold.opacity(0.3), width: 1)

            // Connector
            Rectangle()
                .fill(Theme.routeGold)
                .frame(width: 2, height: 20)
        }
    }
}

Step 5: Update original TripDetailView to be a typealias

Replace TripDetailView.swift with a simple redirect:

//
//  TripDetailView.swift
//  SportsTime
//
//  Redirects to AdaptiveTripDetailView for backward compatibility.
//

import SwiftUI

typealias TripDetailView = AdaptiveTripDetailView

Step 6: Verify the build

Run: xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -30 Expected: BUILD SUCCEEDED

Step 7: Commit

git add SportsTime/Features/Trip/Views/
git commit -m "$(cat <<'EOF'
feat(trip): add brutalist variant for TripDetailView

- Create AdaptiveTripDetailView router
- Add TripDetailView_Classic (extracted from original)
- Add TripDetailView_Brutalist with monospace fonts, borders, perforated dividers
- Update TripDetailView as typealias for backward compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"

Task 3: Create SavedTripsListView Variants

Files:

  • Create: SportsTime/Features/Home/Views/Variants/Classic/SavedTripsListView_Classic.swift
  • Create: SportsTime/Features/Home/Views/Variants/Brutalist/SavedTripsListView_Brutalist.swift
  • Create: SportsTime/Features/Home/Views/AdaptiveSavedTripsListView.swift
  • Modify: SportsTime/Features/Home/Views/HomeView.swift (extract SavedTripsListView, update to use Adaptive)

Step 1: Create directory structure

mkdir -p SportsTime/Features/Home/Views/Variants/Classic
mkdir -p SportsTime/Features/Home/Views/Variants/Brutalist

Step 2: Extract SavedTripsListView to Classic variant

Move SavedTripsListView (lines 406-581 from HomeView.swift) to Variants/Classic/SavedTripsListView_Classic.swift, renaming to SavedTripsListView_Classic.

Step 3: Create SavedTripsListView_Brutalist

Create SportsTime/Features/Home/Views/Variants/Brutalist/SavedTripsListView_Brutalist.swift with brutalist styling.

Step 4: Create AdaptiveSavedTripsListView router

Step 5: Update HomeView.swift

Replace inline SavedTripsListView with AdaptiveSavedTripsListView.

Step 6: Verify build and commit


Task 4: Create ScheduleListView Variants

Files:

  • Create: SportsTime/Features/Schedule/Views/Variants/Classic/ScheduleListView_Classic.swift
  • Create: SportsTime/Features/Schedule/Views/Variants/Brutalist/ScheduleListView_Brutalist.swift
  • Create: SportsTime/Features/Schedule/Views/AdaptiveScheduleListView.swift
  • Modify: SportsTime/Features/Schedule/Views/ScheduleListView.swift (typealias)
  • Modify: SportsTime/Features/Home/Views/HomeView.swift (update Schedule tab)

Step 1: Create directory structure

mkdir -p SportsTime/Features/Schedule/Views/Variants/Classic
mkdir -p SportsTime/Features/Schedule/Views/Variants/Brutalist

Step 2-6: Same pattern as TripDetailView


Task 5: Create SettingsView Variants

Files:

  • Create: SportsTime/Features/Settings/Views/Variants/Classic/SettingsView_Classic.swift
  • Create: SportsTime/Features/Settings/Views/Variants/Brutalist/SettingsView_Brutalist.swift
  • Create: SportsTime/Features/Settings/Views/AdaptiveSettingsView.swift
  • Modify: SportsTime/Features/Settings/Views/SettingsView.swift (typealias)
  • Modify: SportsTime/Features/Home/Views/HomeView.swift (update Settings tab)

Step 1: Create directory structure

mkdir -p SportsTime/Features/Settings/Views/Variants/Classic
mkdir -p SportsTime/Features/Settings/Views/Variants/Brutalist

Step 2-6: Same pattern as TripDetailView


Task 6: Run Tests and Fix Issues

Step 1: Run full test suite

Run: xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test Expected: All tests pass

Step 2: Fix any failing tests

Update test files that reference view types directly.

Step 3: Commit fixes


Task 7: Final Verification and Documentation Update

Step 1: Test all design styles in simulator

  1. Build and run app
  2. Go to Settings → Home Screen Style
  3. Select "Classic" - verify all screens look correct
  4. Select "Brutalist" - verify all screens have brutalist styling

Step 2: Update CLAUDE.md if needed

Add note about adaptive view pattern if helpful for future development.

Step 3: Final commit

git add -A
git commit -m "$(cat <<'EOF'
docs: complete brutalist app-wide implementation

All main screens now support Classic and Brutalist design variants:
- TripDetailView
- SavedTripsListView
- ScheduleListView
- SettingsView

Other 22 design styles fall back to Classic for non-home screens.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"