#import "MapViewController.h"
#import "SettingsViewController.h"
#import "SettingsStore.h"
#import "BusViewController.h"
#import <WebKit/WebKit.h>
#import <AVFoundation/AVFoundation.h>
#import <QuartzCore/QuartzCore.h>

@interface MapViewController () <CLLocationManagerDelegate, WKNavigationDelegate, WKScriptMessageHandler, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate>
@end

@implementation MapViewController {
    CLLocation *_lastLocation;
    BOOL _busWasVisible;
    BOOL _modalitaWasVisible, _searchWasVisible, _trackingWasVisible, _settingsWasVisible, _compassWasVisible, _logWasVisible;
    UIButton *_goButton;
    NSDictionary *_pendingDestDict; // {lat, lon, name}
    CLLocationSpeed _currentSpeed;
    NSTimer *_navTimer;
    CGFloat _currentHeading;
    CGFloat _currentCourse; // direzione di marcia dal GPS
    CGFloat _cameraHeading; // ultimo heading applicato alla camera
    CGFloat _arrowBaseScale;
    CLLocation *_prevLocation;
    CLLocationCoordinate2D _prevSnapped;
    CLLocationCoordinate2D _lastSnapped;
    BOOL _hasSnapped;
    NSTimeInterval _interpStart;
    BOOL _isRecalculating;
    NSTimer *_arrowTimer;
    NSMutableArray *_busStopAnnotations; // array of NSDictionary {lat, lon, name}
    NSString *_lastSpokenInstruction;
    UIButton *_simButton;
    UIButton *_retButton;
    BOOL _hasReturnTrip;
    NSDictionary *_pendingSimDestDict;
    BOOL _simPending; // flag: avvia simulation quando simCoords arriva da startBusNavigation
    // Simulazione
    BOOL _isSimulating;
    NSTimer *_simTimer;
    NSArray *_simCoords; // array di {lat, lon}
    NSInteger _simIndex;
    double _simStepPerTick; // metri per tick simulazione
    double _simLat, _simLon, _simCourse; // posizione interpolata (scritta da simTick, letta da pushPosition)
    BOOL _hasSimPos;
    int _posUpdateCounter;
    // Movimento fluido (20 fps)
    BOOL _isInterpolating;
    CLLocationCoordinate2D _interpFrom;
    CLLocationCoordinate2D _interpTo;
    NSTimeInterval _interpStartTime;
    NSTimer *_smoothTimer;
    // Bus simulazione (parallela, NON tocca simulazione auto)
    BOOL _isBusSimulating;
    NSTimer *_busSimTimer;
    NSArray *_busSimCoords; // array di {lat, lon}
    NSInteger _busSimIndex;
    double _busSimStepPerTick;
    double _busSimLat, _busSimLon, _busSimCourse;
    BOOL _hasBusSimPos;
    UIButton *_busSimButton;
    // Ricerca per orario
    int _searchHour;
    int _searchMin;
    int _selectedDay;    // 1-31
    int _selectedMonth;  // 1-12
    int _searchSeq;      // contatore per ignorare callback stale
    // Ricerca auto (Nominatim) lato ObjC — bypassa WKWebView
    NSTimer *_osmSearchTimer;
    int _osmSearchSeq;
    NSString *_userCity;
    NSString *_lastSearchCivico; // civico esatto richiesto (es. "60", "60c")
    UIPickerView *_hourPicker;
    UILabel *_colonLabel;
    UIPickerView *_minPicker;
    UIPickerView *_dayPicker;
    UIPickerView *_monthPicker;
    // Custom barra unica: search + giorni + orario
    UIView *_searchContainer;
    UITextField *_searchField;
    BOOL _isBusSearchMode;
    UIButton *_annullaButton;
    // Bus info panel — finestra avorio con prossima fermata, anticipo/ritardo
    UIView *_busInfoPanel;
    UILabel *_busNextStopLabel;       // "Prossima fermata: NOME"
    UILabel *_busArrivoTitle;         // "Arrivo"
    UILabel *_busAnticipoTitle;       // "Anticipo"
    UILabel *_busRitardoTitle;        // "Ritardo"
    UILabel *_busDistanzaTitle;       // "Distanza"
    UIView *_busArrivalPill;          // pillola neumorphism
    UILabel *_busArrivalNumber;       // numero distanza arrivo
    UILabel *_busArrivalUnitLabel;    // "metri"
    UIView *_busAnticipoPill;
    UILabel *_busAnticipoNumber;
    UILabel *_busAnticipoUnitLabel;
    UIView *_busRitardoPill;
    UILabel *_busRitardoNumber;
    UILabel *_busRitardoUnitLabel;
    UIView *_busDistanzaPill;
    UILabel *_busDistanzaNumber;
    UILabel *_busDistanzaUnitLabel;
    NSArray *_busTripStops;           // [{name, lat, lon, arrival, departure}, ...] dal JS
    NSInteger _busCurrentStopIdx;     // indice prossima fermata
    double _busTripStartDist;         // distanza cumulativa all'inizio della simulazione
    NSTimeInterval _busSimStartTime;  // quando è partita la simulazione
    UIButton *_busSpeedButton;        // pulsante velocità simulatore (30/50/70 km/h)
    UIView *_busSpeedPill;           // pallina velocità (fuori dal panel, sotto log)
    UILabel *_busSpeedPillLabel;     // label dentro la pallina velocità
    UILabel *_busStopNameLabel;      // nome fermata centrato grande (sotto "Prossima fermata")
    int _busSpeedLevel;               // 0=30, 1=50, 2=70 km/h
    double _busLastUpdateDist;        // distanza percorsa all'ultimo update panel
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self generateArrow3D]; // DEVE essere prima di setupMapView!
    self.view.backgroundColor = [UIColor blackColor];
    // Carica impostazioni camera salvate, o default
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    self.cameraAltitude = [ud objectForKey:@"autista_cam_alt"] ? [ud floatForKey:@"autista_cam_alt"] : 500;
    self.cameraPitch = [ud objectForKey:@"autista_cam_pit"] ? [ud floatForKey:@"autista_cam_pit"] : 60;
    self.cameraOffset = [ud objectForKey:@"autista_cam_off"] ? [ud floatForKey:@"autista_cam_off"] : 0;
    self.cameraVerticalOffset = [ud objectForKey:@"autista_cam_voff"] ? [ud floatForKey:@"autista_cam_voff"] : 0;
    self.menuOpacity = [ud objectForKey:@"autista_menu_op"] ? [ud floatForKey:@"autista_menu_op"] : 1.0;
    self.speechSynthesizer = [[AVSpeechSynthesizer alloc] init];
    [self setupMapView];
    [self applyArrowTransform];
    [self setupLocationManager];
    [self setupSearchOverlay];
    [self setupButtons];
    [self applyMenuOpacity];
    [self loadSavedLayout];
    // Tap sulla mappa chiude ricerca/tastiera
    UITapGestureRecognizer *mapTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissSearch)];
    mapTap.cancelsTouchesInView = NO;
    [self.webView addGestureRecognizer:mapTap];
    [self setupSearchTapToDismiss];
    [self setupNavBar];
    [self setupBusInfoPanel];
    
    // Inizializza bus stops
    _busStopAnnotations = [NSMutableArray array];
    self.busStopsFetched = [NSMutableSet set];
    self.pendingBusFetch = NO;
    
    // Inizializza ricerca auto
    _userCity = @"Bologna";
    _osmSearchSeq = 0;
    // Reverse geocode per città reale (asincrono, aggiorna _userCity)
    dispatch_async(dispatch_get_main_queue(), ^{
        double lat = 44.49, lng = 11.34;
        NSURLComponents *rc = [NSURLComponents componentsWithString:@"https://nominatim.openstreetmap.org/reverse"];
        rc.queryItems = @[
            [NSURLQueryItem queryItemWithName:@"format" value:@"json"],
            [NSURLQueryItem queryItemWithName:@"lat" value:[NSString stringWithFormat:@"%.6f", lat]],
            [NSURLQueryItem queryItemWithName:@"lon" value:[NSString stringWithFormat:@"%.6f", lng]],
            [NSURLQueryItem queryItemWithName:@"zoom" value:@"10"]
        ];
        NSMutableURLRequest *rr = [NSMutableURLRequest requestWithURL:rc.URL];
        [rr setValue:@"aNavigator/3.40 (iOS)" forHTTPHeaderField:@"User-Agent"];
        [[[NSURLSession sharedSession] dataTaskWithRequest:rr completionHandler:^(NSData *d, NSURLResponse *r, NSError *e) {
            if (d && !e) {
                NSDictionary *j = [NSJSONSerialization JSONObjectWithData:d options:0 error:nil];
                NSString *city = j[@"address"][@"city"] ?: j[@"address"][@"town"] ?: j[@"address"][@"village"];
                if (city.length > 0) _userCity = city;
            }
        }] resume];
    });
    
    // Crea BusViewController — child VC per finestra autobus 3D
    BusViewController *busVC = [[BusViewController alloc] init];
    busVC.mapVC = self;
    [self addChildViewController:busVC];
    busVC.view.frame = self.view.bounds;
    busVC.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:busVC.view];
    [busVC didMoveToParentViewController:self];
    busVC.view.hidden = YES;
    busVC.view.alpha = 0;
    self.busVC = busVC;
    
    // Pannello log debugging
    [self setupLogPanel];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mapSettingsChanged) name:@"MapSettingsChanged" object:nil];
    
    // Timer per istruzioni vocali (ogni 2 secondi durante navigazione)
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(speakCurrentInstruction) userInfo:nil repeats:YES];
    
    [self applyCameraSettings];
    [self applyAllSettings];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.navigationController setNavigationBarHidden:YES animated:animated];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    // Bussola: center + bounds (NON frame, che resetta la transform)
    CGFloat h = self.view.bounds.size.height;
    self.compassButton.center = CGPointMake(12 + 26, h - 110 + 26);
    self.compassButton.bounds = CGRectMake(0, 0, 52, 52);
    self.compassButton.layer.cornerRadius = 26;
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    // Non facciamo nulla — il rAF engine (JS) gestisce già resize e camera da solo
}

#pragma mark - Setup

- (void)setupMapView {
    WKUserContentController *userCtrl = [[WKUserContentController alloc] init];
    [userCtrl addScriptMessageHandler:self name:@"speak"];
    [userCtrl addScriptMessageHandler:self name:@"navigationEnd"];
    [userCtrl addScriptMessageHandler:self name:@"navUpdate"];
    [userCtrl addScriptMessageHandler:self name:@"error"];
    [userCtrl addScriptMessageHandler:self name:@"searchResults"];
    [userCtrl addScriptMessageHandler:self name:@"routeReady"];
    [userCtrl addScriptMessageHandler:self name:@"simCoords"];
    [userCtrl addScriptMessageHandler:self name:@"appLog"];
    [userCtrl addScriptMessageHandler:self name:@"navigationStart"];
    [userCtrl addScriptMessageHandler:self name:@"requestGtfs"];
    
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    config.userContentController = userCtrl;
    
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.webView.navigationDelegate = self;
    self.webView.scrollView.bounces = NO;
    self.webView.scrollView.bouncesZoom = NO;
    self.webView.opaque = NO;
    self.webView.backgroundColor = [UIColor clearColor];
    
    // Load map.html from main bundle
    NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"map" ofType:@"html"];
    if (htmlPath) {
        NSURL *htmlURL = [NSURL fileURLWithPath:htmlPath];
        NSURL *bundleDir = [htmlURL URLByDeletingLastPathComponent];
        [self.webView loadFileURL:htmlURL allowingReadAccessToURL:bundleDir];
    }
    
    [self.view addSubview:self.webView];
}

- (void)setupLocationManager {
    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
    self.locationManager.distanceFilter = 1.0;
    [self.locationManager requestWhenInUseAuthorization];
    [self.locationManager startUpdatingLocation];
    [self.locationManager startUpdatingHeading];
    self.locationManager.headingFilter = 5.0;
}

- (void)setupSearchOverlay {
    CGFloat w = self.view.bounds.size.width;
    CGFloat topH = 120;
    
    self.searchOverlay = [[UIView alloc] initWithFrame:CGRectMake(0, -topH, w, topH)];
    self.searchOverlay.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    self.searchOverlay.clipsToBounds = NO;
    self.searchOverlay.hidden = YES;
    
    // Glass background — STESSO STILE menu impostazioni
    self.searchBlur = [[UIView alloc] init];
    self.searchBlur.backgroundColor = [UIColor colorWithWhite:0.18 alpha:0.93];
    self.searchBlur.frame = self.searchOverlay.bounds;
    self.searchBlur.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.searchBlur.layer.cornerRadius = 28;
    self.searchBlur.layer.masksToBounds = YES;
    self.searchBlur.layer.maskedCorners = kCALayerMinXMaxYCorner | kCALayerMaxXMaxYCorner;
    self.searchBlur.layer.borderWidth = 1.5;
    self.searchBlur.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.25].CGColor;
    [self.searchOverlay addSubview:self.searchBlur];
    
    self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(12, 50, w - 70, 44)];
    self.searchBar.delegate = self;
    self.searchBar.placeholder = @"";
    self.searchBar.barStyle = UIBarStyleDefault;
    self.searchBar.searchBarStyle = UISearchBarStyleMinimal;
    self.searchBar.searchTextField.textColor = [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0];
    self.searchBar.searchTextField.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.92];
    self.searchBar.searchTextField.layer.cornerRadius = 12;
    self.searchBar.searchTextField.layer.masksToBounds = YES;
    self.searchBar.searchTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"Cerca indirizzo o luogo..." attributes:@{NSForegroundColorAttributeName: [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0]}];
    self.searchBar.tintColor = [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0];
    self.searchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    // Pulsante X per cancellare testo
    self.searchBar.searchTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
    // 🔍 Icona lente nera (più visibile su sfondo chiaro)
    self.searchBar.searchTextField.leftView.tintColor = [UIColor darkGrayColor];
    [self.searchOverlay addSubview:self.searchBar];
    
    // 🎯 Custom barra unica: search + giorni + orario (solo bus mode)
    _selectedDay = 1; // default, sara sovrascritto da data attuale all'apertura
    _selectedMonth = 6;
    _searchHour = 0;
    _searchMin = 0;
    _isBusSearchMode = NO;
    
    _searchContainer = [[UIView alloc] initWithFrame:CGRectMake(12, 50, w - 24, 44)];
    _searchContainer.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.92];
    _searchContainer.layer.cornerRadius = 14;
    _searchContainer.layer.masksToBounds = NO;  // ← fondamentale: lascia vedere le rotelline sotto la barra
    _searchContainer.hidden = YES;
    [self.searchOverlay addSubview:_searchContainer];
    
    // Icona lente — piu grande
    UIImageView *magIcon = [[UIImageView alloc] initWithFrame:CGRectMake(8, 9, 26, 26)];
    magIcon.contentMode = UIViewContentModeScaleAspectFit;
    magIcon.tintColor = [UIColor colorWithWhite:0.40 alpha:1.0];
    magIcon.image = [UIImage systemImageNamed:@"magnifyingglass"];
    [_searchContainer addSubview:magIcon];
    
    // TextField — piu grande
    _searchField = [[UITextField alloc] initWithFrame:CGRectMake(40, 0, 0, 44)];
    _searchField.placeholder = @"Cerca linea autobus (es. 93)...";
    _searchField.textColor = [UIColor colorWithWhite:0.30 alpha:1.0];
    _searchField.font = [UIFont systemFontOfSize:18];
    _searchField.tintColor = [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0];
    _searchField.autocapitalizationType = UITextAutocapitalizationTypeNone;
    _searchField.autocorrectionType = UITextAutocorrectionTypeNo;
    _searchField.returnKeyType = UIReturnKeySearch;
    _searchField.delegate = self;
    [_searchField addTarget:self action:@selector(searchFieldChanged) forControlEvents:UIControlEventEditingChanged];
    [_searchContainer addSubview:_searchField];
    
    // 🎡 Day picker (1-31)
    _dayPicker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, -48, 50, 140)];
    _dayPicker.delegate = self;
    _dayPicker.dataSource = self;
    _dayPicker.backgroundColor = [UIColor clearColor];
    [_searchContainer addSubview:_dayPicker];
    
    // 📅 Month picker (Gen-Dic)
    _monthPicker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, -48, 68, 140)];
    _monthPicker.delegate = self;
    _monthPicker.dataSource = self;
    _monthPicker.backgroundColor = [UIColor clearColor];
    [_searchContainer addSubview:_monthPicker];
    
    // ⏰ Hour picker (00-23) — picker separato, pillola sua
    _hourPicker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, -48, 44, 140)];
    _hourPicker.delegate = self;
    _hourPicker.dataSource = self;
    _hourPicker.backgroundColor = [UIColor clearColor];
    [_searchContainer addSubview:_hourPicker];
    
    // Colon label ":" tra ore e minuti
    _colonLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 16, 44)];
    _colonLabel.text = @":";
    _colonLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightBold];
    _colonLabel.textColor = [UIColor colorWithWhite:0.30 alpha:1.0];
    _colonLabel.textAlignment = NSTextAlignmentCenter;
    _colonLabel.backgroundColor = [UIColor clearColor];
    [_searchContainer addSubview:_colonLabel];
    
    // ⏰ Minute picker (00-59) — picker separato, pillola sua
    _minPicker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, -48, 44, 140)];
    _minPicker.delegate = self;
    _minPicker.dataSource = self;
    _minPicker.backgroundColor = [UIColor clearColor];
    [_searchContainer addSubview:_minPicker];
    
    // Annulla button (top-right, sopra la barra di ricerca)
    _annullaButton = [UIButton buttonWithType:UIButtonTypeSystem];
    _annullaButton.tintColor = [UIColor whiteColor];
    _annullaButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
    [_annullaButton setTitle:@"Annulla" forState:UIControlStateNormal];
    [_annullaButton sizeToFit];
    [_annullaButton addTarget:self action:@selector(closeSearch) forControlEvents:UIControlEventTouchUpInside];
    [self.searchOverlay addSubview:_annullaButton];
    
    [self.view addSubview:self.searchOverlay];
    
    self.searchResultsTable = [[UITableView alloc] initWithFrame:CGRectMake(0, topH, w, 0) style:UITableViewStylePlain];
    self.searchResultsTable.dataSource = self;
    self.searchResultsTable.delegate = self;
    self.searchResultsTable.backgroundColor = [UIColor colorWithWhite:0.16 alpha:0.95];
    self.searchResultsTable.separatorColor = [[UIColor whiteColor] colorWithAlphaComponent:0.1];
    self.searchResultsTable.hidden = YES;
    self.searchResultsTable.clipsToBounds = YES;
    self.searchResultsTable.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
    self.searchResultsTable.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    [self.searchResultsTable registerClass:[UITableViewCell class] forCellReuseIdentifier:@"SCell"];
    [self.searchOverlay addSubview:self.searchResultsTable];
}

- (void)setupButtons {
    CGFloat margin = 12;
    CGFloat pillW = 100;
    CGFloat pillH = 40;
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    CGFloat radius = pillH / 2;
    
    // Glass pill helper
    void(^glassPill)(UIButton*) = ^(UIButton *btn) {
        btn.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
        btn.layer.cornerRadius = radius;
        btn.layer.borderWidth = 1.5;
        btn.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.25].CGColor;
        btn.layer.shadowColor = [UIColor blackColor].CGColor;
        btn.layer.shadowOpacity = 0.3;
        btn.layer.shadowRadius = 8;
        btn.layer.shadowOffset = CGSizeZero;
    };
    
    // 🚌 Modalita (top-right) — apre/chiude finestra autobus
    self.modalitaButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.modalitaButton.frame = CGRectMake(w - pillW - margin, 54, pillW, pillH);
    self.modalitaButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    glassPill(self.modalitaButton);
    self.modalitaButton.tintColor = [UIColor whiteColor];
    self.modalitaButton.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
    [self.modalitaButton setTitle:@"  Modalità" forState:UIControlStateNormal];
    [self.modalitaButton setImage:[UIImage systemImageNamed:@"bus.fill"] forState:UIControlStateNormal];
    [self.modalitaButton addTarget:self action:@selector(modalitaButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.modalitaButton];
    
    // 🗺️ Mappa / X button (top-right) — Mappa in landscape, X in portrait (chiude bus)
    self.mapButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.mapButton.frame = CGRectMake(w - 48, 54, 40, pillH);
    self.mapButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    glassPill(self.mapButton);
    self.mapButton.tintColor = [UIColor whiteColor];
    self.mapButton.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
    [self.mapButton addTarget:self action:@selector(topRightButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    self.mapButton.hidden = YES; // inizialmente nascosto (bus non visibile)
    [self.view addSubview:self.mapButton];
    
    // 🔍 Search pill (sotto la finestra bus)
    self.searchButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.searchButton.frame = CGRectMake(w - pillW - margin, h * 0.35 + 10, pillW, pillH);
    self.searchButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    glassPill(self.searchButton);
    self.searchButton.tintColor = [UIColor whiteColor];
    self.searchButton.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
    [self.searchButton setTitle:@"  Cerca" forState:UIControlStateNormal];
    [self.searchButton setImage:[UIImage systemImageNamed:@"magnifyingglass"] forState:UIControlStateNormal];
    [self.searchButton addTarget:self action:@selector(openSearch) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.searchButton];
    
    // 📍 Tracking pill
    self.trackingButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.trackingButton.frame = CGRectMake(w - pillW - margin, h - 120, pillW, pillH);
    self.trackingButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin;
    glassPill(self.trackingButton);
    self.trackingButton.tintColor = [UIColor whiteColor];
    self.trackingButton.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
    [self.trackingButton setTitle:@"  Posizione" forState:UIControlStateNormal];
    [self.trackingButton setImage:[UIImage systemImageNamed:@"location.fill"] forState:UIControlStateNormal];
    [self.trackingButton addTarget:self action:@selector(trackingButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.trackingButton];
    
    // ⚙️ Settings pill
    self.settingsButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.settingsButton.frame = CGRectMake(w - pillW - margin, h - 60, pillW, pillH);
    self.settingsButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin;
    glassPill(self.settingsButton);
    self.settingsButton.tintColor = [UIColor whiteColor];
    self.settingsButton.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
    [self.settingsButton setTitle:@"  Impostaz." forState:UIControlStateNormal];
    [self.settingsButton setImage:[UIImage systemImageNamed:@"gearshape.fill"] forState:UIControlStateNormal];
    [self.settingsButton addTarget:self action:@selector(openSettings) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.settingsButton];
    
    // 🧭 Bussola (bottom-left, sotto log)
    self.compassButton = [UIButton buttonWithType:UIButtonTypeCustom];
    self.compassButton.frame = CGRectMake(margin, h - 110, 52, 52);
    // NO autoresizingMask — la transform di rotazione lo rompe
    self.compassButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
    self.compassButton.layer.cornerRadius = 26;
    self.compassButton.layer.borderWidth = 1.5;
    self.compassButton.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.25].CGColor;
    self.compassButton.tintColor = [UIColor whiteColor];
    // Icona bussola personalizzata (PNG fornito)
    NSString *compassPath = [[NSBundle mainBundle] pathForResource:@"compass" ofType:@"png"];
    UIImage *compassImg = compassPath ? [UIImage imageWithContentsOfFile:compassPath] : nil;
    if (!compassImg) compassImg = [UIImage systemImageNamed:@"location.north.line"];
    [self.compassButton setImage:compassImg forState:UIControlStateNormal];
    self.compassButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.compassButton addTarget:self action:@selector(compassButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    self.compassButton.hidden = ![SettingsStore shared].showCompass; // use custom compass (no system compass in Leaflet)
    [self.view addSubview:self.compassButton];
    [self.view bringSubviewToFront:self.compassButton];
    
    // 📋 Log toggle (bottom-left, sopra la bussola)
    self.logButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.logButton.frame = CGRectMake(margin, h - 170, 44, 44);
    self.logButton.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
    self.logButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
    self.logButton.layer.cornerRadius = 22;
    self.logButton.layer.borderWidth = 1;
    self.logButton.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2].CGColor;
    self.logButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
    [self.logButton setImage:[UIImage systemImageNamed:@"terminal.fill"] forState:UIControlStateNormal];
    [self.logButton addTarget:self action:@selector(toggleLogPanel) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.logButton];
    
    // ⚡ Pallina velocità simulatore (centro-destra, draggabile, salvabile)
    _busSpeedPill = [[UIView alloc] initWithFrame:CGRectMake(w - 100, h/2 - 22, 44, 44)];
    _busSpeedPill.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
    _busSpeedPill.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
    _busSpeedPill.layer.cornerRadius = 22;
    _busSpeedPill.layer.borderWidth = 1;
    _busSpeedPill.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2].CGColor;
    _busSpeedPill.hidden = YES;
    UITapGestureRecognizer *speedTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(busSpeedButtonTapped)];
    [_busSpeedPill addGestureRecognizer:speedTap];
    [self.view addSubview:_busSpeedPill];
    
    _busSpeedPillLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
    _busSpeedPillLabel.font = [UIFont boldSystemFontOfSize:12];
    _busSpeedPillLabel.textColor = [UIColor whiteColor];
    _busSpeedPillLabel.textAlignment = NSTextAlignmentCenter;
    _busSpeedPillLabel.text = @"30";
    [_busSpeedPill addSubview:_busSpeedPillLabel];
    _busSpeedLevel = 0;
    _busSpeedButton = nil; // non più usato nel panel
    
    // 🛣️ NUOVO pannello bianco — copre bordo nero in basso
    {
        CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
        CGFloat screenH = [UIScreen mainScreen].bounds.size.height;
        CGFloat panelH = 60;
        
        self.roadNameLabel = [[UIView alloc] initWithFrame:CGRectMake(0, screenH - panelH, screenW, panelH)];
        self.roadNameLabel.backgroundColor = [UIColor whiteColor];
        self.roadNameLabel.hidden = YES;
        [self.view addSubview:self.roadNameLabel];
        
        // Label creata ma NON aggiunta (serve per compatibilità)
        self.roadStreetLabel = [[UILabel alloc] init];
        self.roadStreetLabel.text = @"";
    }
    
    // 🕒 3 pillole separate per ETA, Tempo, Distanza (draggabili)
    UIView *(^makePill)(UILabel*, CGFloat, CGFloat) = ^(UILabel *label, CGFloat px, CGFloat py) {
        UIView *pill = [[UIView alloc] init];
        pill.frame = CGRectMake(px, py, 120, 55);
        pill.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.55];
        pill.layer.cornerRadius = 16;
        pill.layer.masksToBounds = YES;
        pill.layer.borderWidth = 1.5;
        pill.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.25].CGColor;
        pill.hidden = YES;
        [self.view addSubview:pill];
        
        label.frame = CGRectMake(4, 2, 112, 51);
        label.numberOfLines = 2;
        label.font = [UIFont boldSystemFontOfSize:28];
        label.textColor = [UIColor whiteColor];
        label.textAlignment = NSTextAlignmentCenter;
        label.text = @"";
        [pill addSubview:label];
        return pill;
    };
    
    // Crea i 3 label per le pillole (allocati qui, NON dentro la pill via)
    self.roadETALabel = [[UILabel alloc] init];
    self.roadTimeLabel = [[UILabel alloc] init];
    self.roadDistLabel = [[UILabel alloc] init];
    
    self.roadETAPill = makePill(self.roadETALabel, w - 300, h - 220);
    self.roadTimePill = makePill(self.roadTimeLabel, w - 195, h - 220);
    self.roadDistPill = makePill(self.roadDistLabel, w - 90, h - 220);
    
    // ⏹️ Stop button (cerchio rosso, visibile solo in navigazione, in alto a destra)
    self.stopNavButton = [UIButton buttonWithType:UIButtonTypeCustom];
    self.stopNavButton.frame = CGRectMake(w - 56, 54, 44, 44);
    self.stopNavButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin;
    self.stopNavButton.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.7];
    self.stopNavButton.layer.cornerRadius = 22;
    self.stopNavButton.layer.borderWidth = 2;
    self.stopNavButton.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5].CGColor;
    [self.stopNavButton setImage:[UIImage systemImageNamed:@"stop.fill"] forState:UIControlStateNormal];
    self.stopNavButton.tintColor = [UIColor whiteColor];
    self.stopNavButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
    self.stopNavButton.imageEdgeInsets = UIEdgeInsetsMake(12, 12, 12, 12);
    [self.stopNavButton addTarget:self action:@selector(endNavigation) forControlEvents:UIControlEventTouchUpInside];
    self.stopNavButton.hidden = YES;
    [self.view addSubview:self.stopNavButton];
}

- (void)setupBusInfoPanel {
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    CGFloat safeTop = self.view.safeAreaInsets.top;
    CGFloat pillH = 120;
    CGFloat pillGap = 1;
    CGFloat pillMargin = 6;
    CGFloat pillW = (w - pillMargin * 2 - pillGap) / 2;
    CGFloat panelH = MAX(h * 0.18, 76 + pillH * 2 + pillGap + 2);
    
    // Panel avorio — sotto la status bar
    CGFloat panelTop = safeTop;
    _busInfoPanel = [[UIView alloc] initWithFrame:CGRectMake(0, panelTop - panelH, w, panelH)];
    _busInfoPanel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    _busInfoPanel.backgroundColor = [UIColor whiteColor];
    _busInfoPanel.layer.borderWidth = 1.0;
    _busInfoPanel.layer.borderColor = [UIColor colorWithWhite:0.85 alpha:1.0].CGColor;
    _busInfoPanel.layer.shadowColor = [UIColor blackColor].CGColor;
    _busInfoPanel.layer.shadowOpacity = 0.3;
    _busInfoPanel.layer.shadowRadius = 8;
    _busInfoPanel.layer.shadowOffset = CGSizeMake(0, 4);
    _busInfoPanel.hidden = YES;
    [self.view addSubview:_busInfoPanel];
    
    // STOP button (destra, 20% più piccolo, non sovrappone)
    UIButton *stopBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    stopBtn.frame = CGRectMake(w - 64, 4, 56, 24);
    stopBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    stopBtn.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.85];
    stopBtn.layer.cornerRadius = 6;
    stopBtn.tintColor = [UIColor whiteColor];
    stopBtn.titleLabel.font = [UIFont boldSystemFontOfSize:12];
    [stopBtn setTitle:@"STOP" forState:UIControlStateNormal];
    [stopBtn addTarget:self action:@selector(endNavigation) forControlEvents:UIControlEventTouchUpInside];
    [_busInfoPanel addSubview:stopBtn];
    
    // "Prossima fermata" — sopra la pillola
    _busNextStopLabel = [[UILabel alloc] initWithFrame:CGRectMake(12, 4, w - 24, 18)];
    _busNextStopLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    _busNextStopLabel.font = [UIFont boldSystemFontOfSize:16];
    _busNextStopLabel.textColor = [UIColor colorWithWhite:0.15 alpha:1.0];
    _busNextStopLabel.textAlignment = NSTextAlignmentCenter;
    _busNextStopLabel.text = @"Prossima fermata";
    [_busInfoPanel addSubview:_busNextStopLabel];
    
    // Nome fermata — con immagine pillola inviata
    CGFloat stopPillH = 56;
    UIView *stopNamePill = [[UIView alloc] initWithFrame:CGRectMake(16, 22, w - 32, stopPillH)];
    stopNamePill.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    stopNamePill.clipsToBounds = YES;
    stopNamePill.layer.cornerRadius = stopPillH / 2;
    // Immagine di sfondo per la pillola nome fermata
    static UIImage *stopBgImg = nil;
    if (!stopBgImg) {
        NSString *sp = [[NSBundle mainBundle] pathForResource:@"stop_pill_bg" ofType:@"jpg"];
        stopBgImg = [UIImage imageWithContentsOfFile:sp];
    }
    UIImageView *stopBgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, w - 32, stopPillH)];
    stopBgView.image = stopBgImg;
    stopBgView.contentMode = UIViewContentModeScaleAspectFill;
    stopBgView.backgroundColor = [UIColor whiteColor];
    [stopNamePill addSubview:stopBgView];
    [_busInfoPanel addSubview:stopNamePill];
    
    _busStopNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(12, -4, w - 56, stopPillH)];
    _busStopNameLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    _busStopNameLabel.font = [UIFont boldSystemFontOfSize:28];
    _busStopNameLabel.textColor = [UIColor colorWithRed:0.0 green:0.65 blue:0.2 alpha:1.0]; // verde
    _busStopNameLabel.textAlignment = NSTextAlignmentCenter;
    _busStopNameLabel.adjustsFontSizeToFitWidth = YES;
    _busStopNameLabel.minimumScaleFactor = 0.5;
    _busStopNameLabel.text = @"—";
    [stopNamePill addSubview:_busStopNameLabel];
    
    // 4 pillole con immagini 2×2 sotto il nome fermata
    CGFloat row1Y = 78;
    CGFloat row2Y = row1Y + pillH + pillGap;
    
    {
        __autoreleasing UILabel *num;
        _busArrivalPill = [self makePillWithImage:CGRectMake(pillMargin, row1Y, pillW, pillH)
                                        imageName:@"pill_bg"
                                            title:@"ARRIVO"
                                     miniPillColor:[UIColor colorWithRed:0.05 green:0.43 blue:0.95 alpha:1.0]
                                           number:&num];
        _busArrivalNumber = num; _busArrivalUnitLabel = nil;
    }
    [_busInfoPanel addSubview:_busArrivalPill];
    
    {
        __autoreleasing UILabel *num;
        _busDistanzaPill = [self makePillWithImage:CGRectMake(pillMargin + pillW + pillGap, row1Y, pillW, pillH)
                                         imageName:@"pill_bg"
                                             title:@"DISTANZA"
                                      miniPillColor:[UIColor colorWithRed:0.05 green:0.43 blue:0.95 alpha:1.0]
                                            number:&num];
        _busDistanzaNumber = num; _busDistanzaUnitLabel = nil;
    }
    [_busInfoPanel addSubview:_busDistanzaPill];
    
    {
        __autoreleasing UILabel *num;
        _busAnticipoPill = [self makePillWithImage:CGRectMake(pillMargin, row2Y, pillW, pillH)
                                         imageName:@"pill_bg"
                                             title:@"ANTICIPO"
                                      miniPillColor:[UIColor colorWithRed:0.85 green:0.15 blue:0.15 alpha:1.0]
                                           number:&num];
        _busAnticipoNumber = num; _busAnticipoUnitLabel = nil;
    }
    [_busInfoPanel addSubview:_busAnticipoPill];
    
    {
        __autoreleasing UILabel *num;
        _busRitardoPill = [self makePillWithImage:CGRectMake(pillMargin + pillW + pillGap, row2Y, pillW, pillH)
                                        imageName:@"pill_bg"
                                            title:@"RITARDO"
                                     miniPillColor:[UIColor colorWithRed:0.85 green:0.15 blue:0.15 alpha:1.0]
                                           number:&num];
        _busRitardoNumber = num; _busRitardoUnitLabel = nil;
    }
    [_busInfoPanel addSubview:_busRitardoPill];
    
    _busTripStops = nil;
    _busCurrentStopIdx = 0;
    
    // Pallina velocità FUORI dal panel (in setupButtons, sotto log)
}

- (UIView *)makePillWithImage:(CGRect)frame
                     imageName:(NSString *)imageName
                        title:(NSString *)title
                 miniPillColor:(UIColor *)miniPillColor
                       number:(UILabel **)outNumber {
    
    UIView *pill = [[UIView alloc] initWithFrame:frame];
    pill.clipsToBounds = YES;
    pill.layer.cornerRadius = 12;
    
    // Immagine di sfondo (stessa per tutte)
    static UIImage *bgImage = nil;
    if (!bgImage) {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"pill_bg" ofType:@"jpg"];
        bgImage = [UIImage imageWithContentsOfFile:path];
    }
    UIImageView *bgView = [[UIImageView alloc] initWithFrame:pill.bounds];
    bgView.image = bgImage;
    bgView.contentMode = UIViewContentModeScaleAspectFill;
    bgView.backgroundColor = [UIColor whiteColor];
    [pill addSubview:bgView];
    
    CGFloat pw = frame.size.width, ph = frame.size.height;
    
    // Numero — dopo il cerchietto, centrato nello spazio a destra
    UILabel *numLabel = [[UILabel alloc] initWithFrame:CGRectMake(pw * 0.33, 0, pw * 0.62, ph * 0.78)];
    numLabel.font = [UIFont systemFontOfSize:ph * 0.26 weight:UIFontWeightBlack];
    numLabel.textColor = [UIColor blackColor];
    numLabel.textAlignment = NSTextAlignmentCenter;
    numLabel.text = @"—";
    numLabel.adjustsFontSizeToFitWidth = YES;
    numLabel.minimumScaleFactor = 0.5;
    [pill addSubview:numLabel];
    if (outNumber) *outNumber = numLabel;
    
    // Tint sottile SOLO sulla mini-pillola (centrata, stretta, bordi arrotondati)
    CGFloat miniW = pw * 0.70;
    CGFloat miniH = ph * 0.18;
    UIView *miniPillBg = [[UIView alloc] initWithFrame:CGRectMake((pw - miniW)/2, ph * 0.75, miniW, miniH)];
    miniPillBg.backgroundColor = [miniPillColor colorWithAlphaComponent:0.35];
    miniPillBg.layer.cornerRadius = miniH / 2;
    [pill addSubview:miniPillBg];
    
    // Titolo — dentro la mini-pillola in basso
    UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake((pw - miniW)/2, ph * 0.75, miniW, miniH)];
    titleLabel.font = [UIFont systemFontOfSize:miniH * 0.72 weight:UIFontWeightBold];
    titleLabel.textColor = [UIColor blackColor];
    titleLabel.textAlignment = NSTextAlignmentCenter;
    titleLabel.text = title;
    titleLabel.adjustsFontSizeToFitWidth = YES;
    titleLabel.minimumScaleFactor = 0.5;
    [pill addSubview:titleLabel];
    
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(pillPressed:)];
    [pill addGestureRecognizer:tap];
    
    return pill;
}

- (void)pillPressed:(UITapGestureRecognizer *)gesture {
    UIView *pill = gesture.view;
    if (gesture.state == UIGestureRecognizerStateBegan) {
        [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleSoft] impactOccurred];
        [UIView animateWithDuration:0.12 animations:^{ pill.transform = CGAffineTransformMakeScale(0.97, 0.97); }];
    } else if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) {
        [UIView animateWithDuration:0.32 delay:0 usingSpringWithDamping:0.65 initialSpringVelocity:0.7 options:UIViewAnimationOptionAllowUserInteraction animations:^{
            pill.transform = CGAffineTransformIdentity;
        } completion:nil];
    }
}

- (void)setupNavBar {
    CGFloat w = self.view.bounds.size.width;
    self.navBar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 180)];
    self.navBar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    self.navBar.hidden = YES;
    
    // Glass nav background — STESSO STILE menu impostazioni
    UIView *bv = [[UIView alloc] init];
    bv.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
    bv.frame = self.navBar.bounds;
    bv.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    bv.layer.borderWidth = 1.5;
    bv.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.25].CGColor;
    [self.navBar addSubview:bv];
    
    UIView *spacer = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 50)];
    [self.navBar addSubview:spacer];
    
    self.instructionLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 56, w - 32, 36)];
    self.instructionLabel.font = [UIFont boldSystemFontOfSize:18];
    self.instructionLabel.textColor = [UIColor whiteColor];
    self.instructionLabel.numberOfLines = 2;
    self.instructionLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    [self.navBar addSubview:self.instructionLabel];
    
    self.distanceLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 96, w - 32, 20)];
    self.distanceLabel.font = [UIFont systemFontOfSize:14];
    self.distanceLabel.textColor = [UIColor colorWithRed:0.3 green:0.9 blue:0.3 alpha:1.0];
    self.distanceLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    [self.navBar addSubview:self.distanceLabel];
    
    self.etaLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 122, 120, 24)];
    self.etaLabel.font = [UIFont boldSystemFontOfSize:17];
    self.etaLabel.textColor = [UIColor whiteColor];
    [self.navBar addSubview:self.etaLabel];
    
    self.speedLabel = [[UILabel alloc] initWithFrame:CGRectMake(w - 80, 116, 64, 36)];
    self.speedLabel.font = [UIFont boldSystemFontOfSize:26];
    self.speedLabel.textColor = [UIColor whiteColor];
    self.speedLabel.textAlignment = NSTextAlignmentRight;
    self.speedLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    [self.navBar addSubview:self.speedLabel];
    
    self.endNavButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.endNavButton.frame = CGRectMake(w - 68, 156, 56, 24);
    self.endNavButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    [self.endNavButton setTitle:@"Esci" forState:UIControlStateNormal];
    self.endNavButton.titleLabel.font = [UIFont boldSystemFontOfSize:12];
    self.endNavButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
    self.endNavButton.tintColor = [UIColor whiteColor];
    self.endNavButton.layer.cornerRadius = 12;
    self.endNavButton.layer.borderWidth = 1;
    self.endNavButton.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.25].CGColor;
    [self.endNavButton addTarget:self action:@selector(endNavigation) forControlEvents:UIControlEventTouchUpInside];
    [self.navBar addSubview:self.endNavButton];
    
    [self.view addSubview:self.navBar];
}

#pragma mark - Log Panel

- (void)setupLogPanel {
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    CGFloat logH = 230; // pannello ~2x rispetto a prima
    
    self.logBuffer = [NSMutableString string];
    
    // Pannello semi-trasparente in basso
    self.logPanel = [[UIView alloc] initWithFrame:CGRectMake(8, h - logH - 70, w - 16, logH)];
    self.logPanel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
    self.logPanel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.78];
    self.logPanel.layer.cornerRadius = 12;
    self.logPanel.layer.borderWidth = 1;
    self.logPanel.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.15].CGColor;
    self.logPanel.hidden = YES;
    [self.view addSubview:self.logPanel];
    
    CGFloat panelW = self.logPanel.bounds.size.width;
    
    // Etichetta titolo
    UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, 120, 20)];
    title.text = @"📋 LOG";
    title.font = [UIFont boldSystemFontOfSize:12];
    title.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.65];
    [self.logPanel addSubview:title];
    
    // Pulsante copy
    UIButton *copyBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    copyBtn.frame = CGRectMake(panelW - 170, 3, 58, 22);
    copyBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    copyBtn.titleLabel.font = [UIFont systemFontOfSize:11 weight:UIFontWeightSemibold];
    [copyBtn setTitle:@"Copia" forState:UIControlStateNormal];
    copyBtn.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.75];
    [copyBtn addTarget:self action:@selector(copyLog) forControlEvents:UIControlEventTouchUpInside];
    [self.logPanel addSubview:copyBtn];
    
    // Pulsante clear
    UIButton *clearBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    clearBtn.frame = CGRectMake(panelW - 108, 3, 66, 22);
    clearBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    clearBtn.titleLabel.font = [UIFont systemFontOfSize:11 weight:UIFontWeightSemibold];
    [clearBtn setTitle:@"Pulisci" forState:UIControlStateNormal];
    clearBtn.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.75];
    [clearBtn addTarget:self action:@selector(clearLog) forControlEvents:UIControlEventTouchUpInside];
    [self.logPanel addSubview:clearBtn];
    
    // Pulsante X per chiudere
    UIButton *xBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    xBtn.frame = CGRectMake(panelW - 38, 3, 28, 22);
    xBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    xBtn.titleLabel.font = [UIFont systemFontOfSize:12];
    [xBtn setImage:[UIImage systemImageNamed:@"xmark.circle.fill"] forState:UIControlStateNormal];
    xBtn.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.75];
    [xBtn addTarget:self action:@selector(toggleLogPanel) forControlEvents:UIControlEventTouchUpInside];
    [self.logPanel addSubview:xBtn];
    
    // Area testo scrollabile
    self.logTextView = [[UITextView alloc] initWithFrame:CGRectMake(6, 28, panelW - 12, logH - 34)];
    self.logTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.logTextView.backgroundColor = [UIColor clearColor];
    self.logTextView.textColor = [UIColor colorWithRed:0.3 green:0.9 blue:0.3 alpha:1.0];
    self.logTextView.font = [UIFont fontWithName:@"Menlo" size:11] ?: [UIFont systemFontOfSize:11];
    self.logTextView.editable = NO;
    self.logTextView.textContainerInset = UIEdgeInsetsMake(2, 4, 2, 4);
    self.logTextView.text = @"";
    [self.logPanel addSubview:self.logTextView];
}

- (void)appLog:(NSString *)format, ... {
    va_list args;
    va_start(args, format);
    NSString *msg = [[NSString alloc] initWithFormat:format arguments:args];
    va_end(args);
    
    NSLog(@"%@", msg);
    
    // Aggiungi al buffer
    NSDateFormatter *df = [[NSDateFormatter alloc] init];
    df.dateFormat = @"HH:mm:ss";
    NSString *ts = [df stringFromDate:[NSDate date]];
    [self.logBuffer appendFormat:@"[%@] %@\n", ts, msg];
    
    // Mantieni solo ultime 80 righe
    NSArray *lines = [self.logBuffer componentsSeparatedByString:@"\n"];
    if (lines.count > 80) {
        NSArray *last = [lines subarrayWithRange:NSMakeRange(lines.count - 80, 80)];
        self.logBuffer = [[last componentsJoinedByString:@"\n"] mutableCopy];
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        self.logTextView.text = self.logBuffer;
        // Scroll in fondo
        if (self.logTextView.text.length > 0) {
            NSRange bottom = NSMakeRange(self.logTextView.text.length - 1, 1);
            [self.logTextView scrollRangeToVisible:bottom];
        }
    });
}

- (void)copyLog {
    [[UIPasteboard generalPasteboard] setString:self.logBuffer];
    [self appLog:@"📋 Log copiato negli appunti"];
}

- (void)clearLog {
    self.logBuffer = [NSMutableString string];
    self.logTextView.text = @"";
}

- (void)toggleLogPanel {
    self.logPanel.hidden = !self.logPanel.hidden;
    if (!self.logPanel.hidden) {
        [self.view bringSubviewToFront:self.logPanel];
    }
}

#pragma mark - Picker Delegates (Day, Month, Time)

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
    return 1; // tutti i picker hanno 1 componente
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
    if (pickerView == _dayPicker) return 31 * 10;   // 10 cicli (5 su + 5 giù)
    if (pickerView == _monthPicker) return 12 * 10;  // 10 cicli (5 su + 5 giù)
    if (pickerView == _hourPicker) return 24 * 10;   // 10 cicli (5 su + 5 giù)
    if (pickerView == _minPicker) return 60 * 10;    // 10 cicli (5 su + 5 giù)
    return 0;
}

- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component {
    return 32;
}

- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
    if (pickerView == _dayPicker) return 50;
    if (pickerView == _monthPicker) return 68;
    if (pickerView == _hourPicker) return 44;
    if (pickerView == _minPicker) return 44;
    return 0;
}

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
    UILabel *label = (UILabel *)view;
    if (!label) {
        label = [[UILabel alloc] init];
        label.backgroundColor = [UIColor clearColor];
        label.textAlignment = NSTextAlignmentCenter;
    }
    
    UIColor *textColor = [UIColor colorWithWhite:0.30 alpha:1.0];
    UIFont *font = [UIFont systemFontOfSize:18 weight:UIFontWeightBold];
    
    if (pickerView == _dayPicker) {
        label.text = [NSString stringWithFormat:@"%02ld", (long)((row % 31) + 1)];
    } else if (pickerView == _monthPicker) {
        NSArray *months = @[@"Gen",@"Feb",@"Mar",@"Apr",@"Mag",@"Giu",@"Lug",@"Ago",@"Set",@"Ott",@"Nov",@"Dic"];
        label.text = months[row % 12];
    } else if (pickerView == _hourPicker) {
        label.text = [NSString stringWithFormat:@"%02ld", (long)(row % 24)];
    } else if (pickerView == _minPicker) {
        label.text = [NSString stringWithFormat:@"%02ld", (long)(row % 60)];
    }
    
    label.font = font;
    label.textColor = textColor;
    return label;
}

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
    if (pickerView == _dayPicker) {
        _selectedDay = (int)((row % 31) + 1);
    } else if (pickerView == _monthPicker) {
        _selectedMonth = (int)((row % 12) + 1);
    } else if (pickerView == _hourPicker) {
        _searchHour = (int)(row % 24);
    } else if (pickerView == _minPicker) {
        _searchMin = (int)(row % 60);
    }
    
    NSString *currentText = _isBusSearchMode ? _searchField.text : self.searchBar.text;
    if (currentText.length >= 1) {
        _searchSeq++;
        [self triggerBusSearch:currentText];
    }
}

- (void)searchFieldChanged {
    NSString *text = _searchField.text;
    if (text.length >= 1) {
        _searchSeq++;
        // DISTRUGGE completamente la tabella — frame zero + array vuoto + hidden
        self.searchResultsTable.frame = CGRectZero;
        self.searchResultsTable.hidden = YES;
        self.searchResults = [NSMutableArray array];
        [self.searchResultsTable reloadData];
        [self triggerBusSearch:text];
    } else {
        // Campo vuoto: mostra recents
        CGFloat w = self.searchOverlay.bounds.size.width;
        CGFloat h = self.searchOverlay.bounds.size.height;
        CGFloat tableY;
        if (_searchContainer && !_searchContainer.hidden) {
            tableY = CGRectGetMaxY(_searchContainer.frame) + 8;
        } else {
            CGFloat safeTop = self.view.safeAreaInsets.top;
            tableY = safeTop + 8 + 44 + 8;
        }
        CGFloat tableH = h - tableY - 20;
        self.searchResultsTable.frame = CGRectMake(0, tableY, w, tableH);
        TransportMode mode = [SettingsStore shared].transportMode;
        self.searchResults = [[[SettingsStore shared] recentDestinationsForMode:mode] mutableCopy];
        [self.searchResultsTable reloadData];
        self.searchResultsTable.hidden = (self.searchResults.count == 0);
    }
}

- (void)triggerBusSearch:(NSString *)query {
    int currentSeq = _searchSeq; // catturato ORA — quando la ricerca PARTE
    NSString *escaped = [query stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    escaped = [escaped stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
    NSString *js = [NSString stringWithFormat:@"nativeSearchBusLineByTime('%@', %d, %d, %d, %d, %d)",
          escaped, currentSeq, _searchHour, _searchMin, _selectedDay, _selectedMonth];
    [self.webView evaluateJavaScript:js completionHandler:nil];
}

#pragma mark - Nominatim Search (ObC lato — NSURLSession, NO WKWebView)

- (double)distanceFromLat:(double)lat1 lng1:(double)lng1 toLat:(double)lat2 lng2:(double)lng2 {
    double R = 6371000;
    double dLat = (lat2 - lat1) * M_PI / 180.0;
    double dLng = (lng2 - lng1) * M_PI / 180.0;
    double a = sin(dLat/2) * sin(dLat/2) +
               cos(lat1 * M_PI / 180.0) * cos(lat2 * M_PI / 180.0) *
               sin(dLng/2) * sin(dLng/2);
    return R * 2 * atan2(sqrt(a), sqrt(1-a));
}

- (NSString *)formatDistance:(double)meters {
    if (meters < 1000) return [NSString stringWithFormat:@"%.0f m", meters];
    return [NSString stringWithFormat:@"%.1f km", meters / 1000.0];
}

- (void)performNominatimSearch:(NSString *)query {
    query = [query stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    if (query.length < 3) return;
    
    // Estrai civico esatto (ultimo token se contiene numero)
    _lastSearchCivico = nil;
    NSArray *tokens = [query componentsSeparatedByString:@" "];
    NSString *lastToken = [tokens lastObject];
    // Se l'ultimo token inizia con una cifra, è un civico (es. "60", "60c", "60d")
    if (lastToken.length > 0 && [[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[lastToken characterAtIndex:0]]) {
        _lastSearchCivico = lastToken;
    }
    
    int seq = ++_osmSearchSeq;
    // Debounce 400ms con dispatch_after (più affidabile di NSTimer)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (seq != _osmSearchSeq) return; // scartato da una ricerca più recente
        [self executeNominatimSearch:query seq:seq];
    });
}

- (void)executeNominatimSearch:(NSString *)query seq:(int)seq {
    double userLat = _lastLocation ? _lastLocation.coordinate.latitude : 44.49;
    double userLng = _lastLocation ? _lastLocation.coordinate.longitude : 11.34;
    
    // Token significativi della query (parole > 3 lettere, escludendo "via", "viale", ecc.)
    NSArray *queryTokens = [[query lowercaseString] componentsSeparatedByString:@" "];
    NSMutableSet *keyTokens = [NSMutableSet set];
    NSSet *stopWords = [NSSet setWithObjects:@"via", @"viale", @"piazza", @"corso", @"largo", @"strada", @"vicolo", nil];
    for (NSString *token in queryTokens) {
        NSString *t = [token stringByTrimmingCharactersInSet:[NSCharacterSet decimalDigitCharacterSet]];
        if (t.length > 3 && ![stopWords containsObject:t]) {
            [keyTokens addObject:t];
        }
    }
    
    // 🔥 DOPPIA query in parallelo: locale (GPS) + globale — merge completo
    NSMutableArray *results = [NSMutableArray array];
    NSMutableSet *seen = [NSMutableSet set];
    dispatch_group_t group = dispatch_group_create();
    __block BOOL localDone = NO, globalDone = NO;
    
    // 1) Query locale (con GPS)
    dispatch_group_enter(group);
    NSMutableArray *localItems = [NSMutableArray arrayWithArray:@[
        [NSURLQueryItem queryItemWithName:@"q" value:query],
        [NSURLQueryItem queryItemWithName:@"limit" value:@"50"]
    ]];
    if (_lastLocation) {
        [localItems addObject:[NSURLQueryItem queryItemWithName:@"lat" value:[NSString stringWithFormat:@"%.6f", _lastLocation.coordinate.latitude]]];
        [localItems addObject:[NSURLQueryItem queryItemWithName:@"lon" value:[NSString stringWithFormat:@"%.6f", _lastLocation.coordinate.longitude]]];
    }
    NSURLComponents *compLocal = [NSURLComponents componentsWithString:@"https://photon.komoot.io/api/"];
    compLocal.queryItems = localItems;
    NSMutableURLRequest *reqLocal = [NSMutableURLRequest requestWithURL:compLocal.URL];
    [reqLocal setValue:@"aNavigator/3.40" forHTTPHeaderField:@"User-Agent"];
    [reqLocal setTimeoutInterval:10];
    [[[NSURLSession sharedSession] dataTaskWithRequest:reqLocal completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
        if (seq == _osmSearchSeq && data && !err) {
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            NSArray *features = json[@"features"];
            if ([features isKindOfClass:[NSArray class]]) {
                @synchronized(results) {
                    for (NSDictionary *f in features) {
                        NSDictionary *r = [self parsePhotonFeature:f userLat:userLat userLng:userLng keyTokens:keyTokens];
                        if (!r) continue;
                        NSString *dk = [NSString stringWithFormat:@"%@|%@", r[@"displayName"], r[@"city"] ?: @""];
                        if ([seen containsObject:dk]) continue;
                        [seen addObject:dk];
                        [results addObject:r];
                    }
                }
            }
        }
        localDone = YES;
        dispatch_group_leave(group);
    }] resume];
    
    // 2) Query globale (senza GPS)
    dispatch_group_enter(group);
    NSURLComponents *compGlobal = [NSURLComponents componentsWithString:@"https://photon.komoot.io/api/"];
    compGlobal.queryItems = @[
        [NSURLQueryItem queryItemWithName:@"q" value:query],
        [NSURLQueryItem queryItemWithName:@"limit" value:@"50"]
    ];
    NSMutableURLRequest *reqGlobal = [NSMutableURLRequest requestWithURL:compGlobal.URL];
    [reqGlobal setValue:@"aNavigator/3.40" forHTTPHeaderField:@"User-Agent"];
    [reqGlobal setTimeoutInterval:10];
    [[[NSURLSession sharedSession] dataTaskWithRequest:reqGlobal completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
        if (seq == _osmSearchSeq && data && !err) {
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            NSArray *features = json[@"features"];
            if ([features isKindOfClass:[NSArray class]]) {
                @synchronized(results) {
                    for (NSDictionary *f in features) {
                        NSDictionary *r = [self parsePhotonFeature:f userLat:userLat userLng:userLng keyTokens:keyTokens];
                        if (!r) continue;
                        NSString *dk = [NSString stringWithFormat:@"%@|%@", r[@"displayName"], r[@"city"] ?: @""];
                        if ([seen containsObject:dk]) continue;
                        [seen addObject:dk];
                        [results addObject:r];
                    }
                }
            }
        }
        globalDone = YES;
        dispatch_group_leave(group);
    }] resume];
    
    // Quando ENTRAMBE le query sono completate, mostra risultati
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        if (seq != _osmSearchSeq) return;
        // Ordina per distanza
        [results sortUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b) {
            return [a[@"dist"] compare:b[@"dist"]];
        }];
        [self showNominatimResults:results seq:seq];
    });
}

- (NSDictionary *)parsePhotonFeature:(NSDictionary *)f userLat:(double)userLat userLng:(double)userLng keyTokens:(NSSet *)keyTokens {
    NSDictionary *p = f[@"properties"];
    if (![p isKindOfClass:[NSDictionary class]]) return nil;
    
    NSString *street = p[@"street"] ?: p[@"name"] ?: @"";
    NSString *hn = p[@"housenumber"] ?: @"";
    NSString *pType = p[@"type"] ?: @"";
    
    // 🔥 Salta risultati non-indirizzo (city/place/county/district/locality/state)
    // Questi non hanno via/civico → inutili per la navigazione e mostrano città vuota
    NSSet *nonAddressTypes = [NSSet setWithObjects:@"city", @"county", @"district", @"locality", @"place", @"state", nil];
    if ([nonAddressTypes containsObject:pType] && !p[@"street"] && hn.length == 0) {
        return nil;
    }
    
    // Filtro pertinenza: ogni token chiave deve apparire nel nome via (case-insensitive)
    if (keyTokens.count > 0) {
        NSString *lowerStreet = [street lowercaseString];
        for (NSString *token in keyTokens) {
            if (![lowerStreet containsString:token]) return nil;
        }
    }
    
    // Filtro precisione civico: se la query ha un numero, match esatto
    if (_lastSearchCivico) {
        if (![hn isEqualToString:_lastSearchCivico]) return nil;
    }
    
    NSString *name = street;
    if (hn.length > 0) name = [NSString stringWithFormat:@"%@ %@", name, hn];
    NSString *city = p[@"city"] ?: p[@"town"] ?: p[@"locality"] ?: @"";
    // 🔥 Fallback: se nessuna città, usa il nome o lo state
    if (city.length == 0) {
        city = p[@"state"] ?: p[@"name"] ?: @"";
    }
    NSString *state = p[@"state"] ?: @"";  // regione
    NSString *country = p[@"country"] ?: @"";
    // 🔥 Fix: empty string da JSON NON viene catturato da ?: — verifica esplicita
    if (country.length == 0) country = @"Italia";
    
    NSArray *coords = f[@"geometry"][@"coordinates"];
    double lat = 0, lon = 0;
    if ([coords isKindOfClass:[NSArray class]] && coords.count >= 2) {
        lon = [coords[0] doubleValue];
        lat = [coords[1] doubleValue];
    }
    double dist = [self distanceFromLat:userLat lng1:userLng toLat:lat lng2:lon];
    
    NSString *subtitle;
    NSMutableArray *parts = [NSMutableArray array];
    if (city.length > 0) [parts addObject:city];
    if (state.length > 0 && ![state isEqualToString:city]) [parts addObject:state];
    if (country.length > 0 && ![country isEqualToString:city]) [parts addObject:country];
    NSString *location = [parts componentsJoinedByString:@", "];
    if (location.length > 0) {
        subtitle = [NSString stringWithFormat:@"%@ — %@", location, [self formatDistance:dist]];
    } else {
        subtitle = [self formatDistance:dist];
    }
    
    return @{
        @"displayName": name,
        @"displaySubtitle": subtitle,
        @"name": name,
        @"lat": @(lat),
        @"lon": @(lon),
        @"full": p[@"name"] ?: @"",
        @"dist": @(dist),
        @"city": city
    };
}

- (NSDictionary *)parseNominatimItem:(NSDictionary *)item userLat:(double)userLat userLng:(double)userLng {
    NSDictionary *a = item[@"address"];
    if (![a isKindOfClass:[NSDictionary class]]) a = @{};
    
    NSString *strada = a[@"road"] ?: a[@"pedestrian"] ?: a[@"footway"] ?: a[@"street"] ?: a[@"suburb"] ?: @"";
    NSString *civico = a[@"house_number"] ?: @"";
    NSString *nome = strada;
    if (civico.length > 0) nome = [NSString stringWithFormat:@"%@ %@", nome, civico];
    if (nome.length == 0) {
        nome = item[@"name"] ?: @"?";
        if ([nome isEqualToString:@"?"]) {
            NSString *dn = item[@"display_name"];
            if (dn) nome = [[dn componentsSeparatedByString:@","] firstObject] ?: @"?";
        }
    }
    NSString *citta = a[@"city"] ?: a[@"town"] ?: a[@"village"] ?: a[@"municipality"] ?: a[@"county"] ?: @"";
    
    double lat = [item[@"lat"] doubleValue];
    double lon = [item[@"lon"] doubleValue];
    double dist = [self distanceFromLat:userLat lng1:userLng toLat:lat lng2:lon];
    
    // Formato Waze: "Via Aurelio Saffi 60, Bologna — 2.0 km"
    NSString *displayName = nome;
    NSString *displaySubtitle;
    if (citta.length > 0) {
        displaySubtitle = [NSString stringWithFormat:@"%@ — %@", citta, [self formatDistance:dist]];
    } else {
        displaySubtitle = [self formatDistance:dist];
    }
    
    return @{
        @"displayName": displayName,
        @"displaySubtitle": displaySubtitle,
        @"name": nome,
        @"lat": @(lat),
        @"lon": @(lon),
        @"full": item[@"display_name"] ?: @"",
        @"dist": @(dist),
        @"city": citta
    };
}

- (void)showNominatimResults:(NSArray *)results seq:(int)seq {
    if (seq != _osmSearchSeq) return;
    dispatch_async(dispatch_get_main_queue(), ^{
        self.searchResults = [results mutableCopy];
        CGFloat w = self.searchOverlay.bounds.size.width;
        if (w <= 0) return;
        CGFloat tableY = _isBusSearchMode ? CGRectGetMaxY(_searchContainer.frame) + 8 : CGRectGetMaxY(self.searchBar.frame) + 8;
        CGFloat maxH = self.searchOverlay.bounds.size.height - tableY - 20;
        CGFloat tableH = MIN((CGFloat)self.searchResults.count * 52, maxH);
        self.searchResultsTable.frame = CGRectMake(0, tableY, w, MAX(tableH, 1));
        self.searchResultsTable.hidden = (self.searchResults.count == 0);
        [self.searchResultsTable reloadData];
    });
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    if (textField == _searchField) {
        [_searchField resignFirstResponder];
    }
    return YES;
}

#pragma mark - Search

- (void)openSearch {
    // Nascondi la finestra dell'autobus durante la ricerca — INCONDIZIONALE
    self.busForceHidden = YES;
    if (self.busVC) {
        _busWasVisible = !self.busVC.view.hidden;
        self.busVC.view.hidden = YES;
        self.busVC.view.alpha = 0;
        [self.busVC.closeXButton setHidden:YES];
    }
    self.searchActive_Ivar = YES;
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    
    // Salva stato pulsanti prima di nasconderli
    _modalitaWasVisible = !self.modalitaButton.hidden;
    _searchWasVisible = !self.searchButton.hidden;
    _trackingWasVisible = !self.trackingButton.hidden;
    _settingsWasVisible = !self.settingsButton.hidden;
    _compassWasVisible = !self.compassButton.hidden;
    _logWasVisible = !self.logPanel.hidden;
    
    // Full-screen gray search page
    self.searchOverlay.hidden = NO;
    [self.view bringSubviewToFront:self.searchOverlay];
    self.searchOverlay.backgroundColor = [UIColor colorWithWhite:0.16 alpha:0.95];
    self.searchBlur.backgroundColor = [UIColor colorWithWhite:0.16 alpha:0.95];
    self.searchBlur.layer.cornerRadius = 0;
    self.searchBlur.layer.maskedCorners = 0;
    self.searchBlur.layer.borderWidth = 0;
    self.searchOverlay.frame = CGRectMake(0, 0, w, h);
    self.searchBlur.frame = self.searchOverlay.bounds;
    
    // Custom bar per bus mode / Search bar per car mode
    CGFloat safeTop = self.view.safeAreaInsets.top;
    BOOL isBus = ([SettingsStore shared].transportMode == TransportModeBus);
    
    if (isBus) {
        // Bus mode: aggiorna data e ora attuali
        NSDate *now = [NSDate date];
        NSCalendar *cal = [NSCalendar currentCalendar];
        NSDateComponents *comp = [cal components:(NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitHour | NSCalendarUnitMinute) fromDate:now];
        _selectedDay = (int)[comp day];
        _selectedMonth = (int)[comp month];
        _searchHour = (int)[comp hour];
        _searchMin = (int)[comp minute];
        
        // Bus mode: custom container con search + picker giorni + picker orario
        self.searchBar.hidden = YES;
        _isBusSearchMode = YES;
        
        CGFloat contW = w - 24;
        _searchContainer.frame = CGRectMake(12, safeTop + 8, contW, 44);
        _searchContainer.hidden = NO;
        
        // Layout: 🔍 (26pt) | textField (riempie) | picker allineati a DESTRA
        // day(50) + month(68) + hour(44) + ":"(16) + min(44) = 222pt + gap 6pt = 228pt
        CGFloat timeRightEdge = contW - 6;
        CGFloat minX = timeRightEdge - 44;
        CGFloat colonX = minX - 16;
        CGFloat hourX = colonX - 44;
        CGFloat monthX = hourX - 2 - 68;
        CGFloat dayX = monthX - 2 - 50;
        
        _searchField.frame = CGRectMake(40, 0, MAX(dayX - 43, 50), 44);
        _dayPicker.frame = CGRectMake(dayX, -48, 50, 140);
        _monthPicker.frame = CGRectMake(monthX, -48, 68, 140);
        _hourPicker.frame = CGRectMake(hourX, -48, 44, 140);
        _colonLabel.frame = CGRectMake(colonX, 0, 16, 44);
        _minPicker.frame = CGRectMake(minX, -48, 44, 140);
        
        // Annulla button — subito sopra la barra, bordo inferiore attaccato al bordo superiore della barra
        _annullaButton.frame = CGRectMake(w - 80, safeTop + 8 - 24, 68, 24);
        _annullaButton.hidden = NO;
        [self.searchOverlay bringSubviewToFront:_annullaButton];
        
        [_dayPicker selectRow:(31 * 5 + (_selectedDay - 1)) inComponent:0 animated:NO];
        [_monthPicker selectRow:(12 * 5 + (_selectedMonth - 1)) inComponent:0 animated:NO];
        [_hourPicker selectRow:(24 * 5 + _searchHour) inComponent:0 animated:NO];
        [_minPicker selectRow:(60 * 5 + _searchMin) inComponent:0 animated:NO];
        
        self.searchBar.searchTextField.placeholder = @"";
    } else {
        // Car mode: search bar normale
        self.searchBar.hidden = NO;
        _searchContainer.hidden = YES;
        _isBusSearchMode = NO;
        _annullaButton.hidden = YES;
        self.searchBar.frame = CGRectMake(12, safeTop + 8, w - 24, 44);
        self.searchBar.showsCancelButton = YES;
        self.searchBar.searchTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"Cerca indirizzo o luogo..." attributes:@{NSForegroundColorAttributeName: [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0]}];
    }
    
    // Nascondi TUTTI i pulsanti mappa
    self.modalitaButton.hidden = YES;
    self.mapButton.hidden = YES;
    self.searchButton.hidden = YES;
    [self hideGoButton];
    self.settingsButton.hidden = YES;
    self.trackingButton.hidden = YES;
    self.compassButton.hidden = YES;
    self.logButton.hidden = YES;
    self.logPanel.hidden = YES;
    [UIView animateWithDuration:0.25 animations:^{
        self.searchOverlay.alpha = 1;
    } completion:^(BOOL finished) {
        if (_isBusSearchMode) {
            [_searchField becomeFirstResponder];
            // Mostra recents subito all'apertura
            [self showRecentsForCurrentMode];
        } else {
            [self.searchBar becomeFirstResponder];
            // Mostra recents auto subito all'apertura
            [self showRecentsForCurrentMode];
        }
        // Mostra destinazioni recenti solo se campo ANCORA vuoto (l'utente potrebbe aver già digitato durante l'animazione)
        if (_isBusSearchMode && _searchField.text.length >= 1) {
            // Utente ha già iniziato a digitare — non mostrare recents
            self.searchResultsTable.hidden = YES;
            self.searchResultsTable.frame = CGRectZero;
            self.searchResults = [NSMutableArray array];
            [self.searchResultsTable reloadData];
        } else if (!_isBusSearchMode && self.searchBar.text.length >= 1) {
            self.searchResultsTable.hidden = YES;
            self.searchResultsTable.frame = CGRectZero;
            self.searchResults = [NSMutableArray array];
            [self.searchResultsTable reloadData];
        } else {
            CGFloat safeTop = self.view.safeAreaInsets.top;
            CGFloat tableY;
            if (_isBusSearchMode) {
                tableY = CGRectGetMaxY(_searchContainer.frame) + 8;
            } else {
                tableY = safeTop + 8 + 44 + 8;
            }
            CGFloat tableH = h - tableY - 20;
            self.searchResultsTable.frame = CGRectMake(0, tableY, w, tableH);
            TransportMode mode = [SettingsStore shared].transportMode;
            self.searchResults = [[[SettingsStore shared] recentDestinationsForMode:mode] mutableCopy];
            [self.searchResultsTable reloadData];
            self.searchResultsTable.hidden = (self.searchResults.count == 0);
            if (self.searchResults.count > 0) {
                [self appLog:@"📋 Mostrati %lu recenti", (unsigned long)self.searchResults.count];
            }
        }
    }];
}

- (void)showRecentsForCurrentMode {
    if (self.searchActive_Ivar) {
        CGFloat w = self.searchOverlay.bounds.size.width;
        CGFloat h = self.searchOverlay.bounds.size.height;
        CGFloat safeTop = self.view.safeAreaInsets.top;
        CGFloat tableY;
        if (_isBusSearchMode) {
            tableY = CGRectGetMaxY(_searchContainer.frame) + 8;
        } else {
            tableY = safeTop + 8 + 44 + 8;
        }
        CGFloat tableH = h - tableY - 20;
        self.searchResultsTable.frame = CGRectMake(0, tableY, w, tableH);
        TransportMode mode = [SettingsStore shared].transportMode;
        self.searchResults = [[[SettingsStore shared] recentDestinationsForMode:mode] mutableCopy];
        [self.searchResultsTable reloadData];
        self.searchResultsTable.hidden = (self.searchResults.count == 0);
    }
}

- (void)closeSearch {
    self.searchActive_Ivar = NO;
    self.busForceHidden = NO;
    
    [_searchField resignFirstResponder];
    [self.searchBar resignFirstResponder];
    [UIView animateWithDuration:0.2 animations:^{
        self.searchOverlay.alpha = 0;
    } completion:^(BOOL finished) {
        self.searchOverlay.hidden = YES;
        self.searchOverlay.alpha = 1;
        
        // Ripristina TUTTI i pulsanti allo stato salvato
        self.modalitaButton.hidden = !self->_modalitaWasVisible;
        self.mapButton.hidden = YES; // X gestito da BusViewController
        self.searchButton.hidden = !self->_searchWasVisible;
        self.settingsButton.hidden = !self->_settingsWasVisible;
        self.trackingButton.hidden = !self->_trackingWasVisible;
        self.compassButton.hidden = !self->_compassWasVisible;
        self.logButton.hidden = NO;  // log toggle sempre visibile dopo ricerca
        self.logPanel.hidden = !self->_logWasVisible;  // ripristina stato log
        
        self.searchResultsTable.hidden = YES;
        self.searchResults = [NSMutableArray array];
        [self.searchResultsTable reloadData];
        self.searchBar.text = @"";
        _searchField.text = @"";
        _searchContainer.hidden = YES;
        self.searchBar.hidden = NO;
        if (self->_busWasVisible && self.busVC) {
            self.busVC.view.hidden = NO;
            self.busVC.view.alpha = 1.0;
            [self.busVC showBus];  // ripristina layout + X completi
            self->_busWasVisible = NO;
        }
    }];
}

- (void)dismissSearch {
    if (self.searchActive_Ivar) {
        [self closeSearch];
    }
}

// Tap sul search overlay (fuori dalla barra) chiude la tastiera
- (void)setupSearchTapToDismiss {
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboard)];
    tap.cancelsTouchesInView = NO;
    [self.searchOverlay addGestureRecognizer:tap];
}

- (void)dismissKeyboard {
    if (self.searchActive_Ivar) {
        [self.searchBar resignFirstResponder];
        [_searchField resignFirstResponder];
    }
}

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
    [self closeSearch];
}

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    [searchBar resignFirstResponder];
}

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
    // UISearchBar = sempre ricerca auto (Nominatim)
    if (searchText.length >= 1) {
        _searchSeq++;
        // Nascondi SUBITO — frame zero
        self.searchResultsTable.frame = CGRectZero;
        self.searchResultsTable.hidden = YES;
        [self performNominatimSearch:searchText];
    } else {
        // Campo vuoto: mostra recents solo in car mode
        if (_isBusSearchMode) {
            self.searchResults = [NSMutableArray array];
            [self.searchResultsTable reloadData];
            self.searchResultsTable.hidden = YES;
            return;
        }
        // Mostra destinazioni recenti a tutta pagina
        CGFloat safeTop = self.view.safeAreaInsets.top;
        CGFloat w = self.searchOverlay.bounds.size.width;
        CGFloat h = self.searchOverlay.bounds.size.height;
        CGFloat tableY = safeTop + 8 + 44 + 8;
        CGFloat tableH = h - tableY - 20;
        self.searchResultsTable.frame = CGRectMake(0, tableY, w, tableH);
        TransportMode mode = [SettingsStore shared].transportMode;
        self.searchResults = [[[SettingsStore shared] recentDestinationsForMode:mode] mutableCopy];
        [self.searchResultsTable reloadData];
        self.searchResultsTable.hidden = (self.searchResults.count == 0);
    }
}

#pragma mark - TableView

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSDictionary *r = self.searchResults[indexPath.row];
    if (r[@"departure"]) return 60; // bus result
    return 52; // normal result (2 righe)
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.searchResults.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SCell" forIndexPath:indexPath];
    NSDictionary *r = self.searchResults[indexPath.row];
    
    // Rimuovi TUTTI i subview dal contentView — pulizia completa per evitare testo fantasma
    for (UIView *sv in [cell.contentView.subviews copy]) {
        [sv removeFromSuperview];
    }
    
    NSString *departure = r[@"departure"];
    
    if (departure && [departure length] > 0) {
        // --- BUS RESULT WITH TIME: orario grande a sx + dettagli a dx con spazio ---
        CGFloat cellW = tableView.bounds.size.width;
        
        // Colonna orologio
        UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(12, 0, 65, 58)];
        timeLabel.text = departure;
        timeLabel.font = [UIFont monospacedDigitSystemFontOfSize:22 weight:UIFontWeightBold];
        timeLabel.textColor = [UIColor whiteColor];
        timeLabel.textAlignment = NSTextAlignmentCenter;
        timeLabel.tag = 9001;
        [cell.contentView addSubview:timeLabel];
        
        // Direction label (Andata/Ritorno)
        NSString *dirLabel = r[@"dirLabel"] ?: @"";
        UIColor *dirColor = [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0];
        if ([dirLabel isEqualToString:@"Ritorno"]) {
            dirColor = [UIColor colorWithRed:1.0 green:0.27 blue:0.27 alpha:1.0];
        }
        UILabel *dirLB = [[UILabel alloc] initWithFrame:CGRectMake(12, 44, 65, 14)];
        dirLB.text = dirLabel;
        dirLB.font = [UIFont systemFontOfSize:11 weight:UIFontWeightSemibold];
        dirLB.textColor = dirColor;
        dirLB.textAlignment = NSTextAlignmentCenter;
        dirLB.tag = 9002;
        [cell.contentView addSubview:dirLB];
        
        // Info a destra con gap di 12pt dalla fine orologio (65+12=77, inizio a 90)
        NSString *displayName = r[@"displayName"] ?: @"";
        NSString *subtitle = r[@"displaySubtitle"] ?: @"";
        
        NSString *cleanName = displayName;
        if ([cleanName hasPrefix:departure]) {
            cleanName = [[cleanName substringFromIndex:departure.length] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        }
        
        CGFloat textLeft = 90;
        CGFloat textW = cellW - textLeft - 12;
        
        UILabel *lineLabel = [[UILabel alloc] initWithFrame:CGRectMake(textLeft, 4, textW, 22)];
        lineLabel.text = cleanName;
        lineLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
        lineLabel.textColor = [UIColor whiteColor];
        lineLabel.tag = 9003;
        [cell.contentView addSubview:lineLabel];
        
        UILabel *infoLabel = [[UILabel alloc] initWithFrame:CGRectMake(textLeft, 30, textW, 28)];
        infoLabel.text = subtitle;
        infoLabel.font = [UIFont systemFontOfSize:11];
        infoLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.55];
        infoLabel.numberOfLines = 2;
        infoLabel.tag = 9004;
        [cell.contentView addSubview:infoLabel];
    } else {
        // --- NORMAL RESULT (recenti / OSM) — formato Waze: via a sx, km a dx, città sotto ---
        CGFloat cellW = tableView.bounds.size.width;
        if (cellW <= 0) cellW = 320;
        
        NSString *name = r[@"displayName"] ?: (r[@"name"] ?: @"");
        NSString *sub = r[@"displaySubtitle"] ?: (r[@"full"] ?: @"");
        // Estrai distanza dal subtitle (formato "Bologna — 2.1 km")
        NSString *city = @"";
        NSString *distStr = @"";
        NSRange dashRange = [sub rangeOfString:@" — "];
        if (dashRange.location != NSNotFound) {
            city = [sub substringToIndex:dashRange.location];
            distStr = [sub substringFromIndex:dashRange.location + 3];
        } else {
            distStr = sub;
        }
        
        // Distanza a destra
        UILabel *distLabel = [[UILabel alloc] initWithFrame:CGRectMake(cellW - 90, 0, 80, 44)];
        distLabel.text = distStr;
        distLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
        distLabel.textColor = [UIColor whiteColor];
        distLabel.textAlignment = NSTextAlignmentRight;
        distLabel.tag = 9103;
        [cell.contentView addSubview:distLabel];
        
        // Nome via a sinistra
        UILabel *nameLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 4, cellW - 110, 22)];
        nameLabel.text = name;
        nameLabel.font = [UIFont systemFontOfSize:15];
        nameLabel.textColor = [UIColor whiteColor];
        nameLabel.tag = 9101;
        [cell.contentView addSubview:nameLabel];
        
        // Città sotto il nome
        UILabel *cityLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 26, cellW - 110, 16)];
        cityLabel.text = city;
        cityLabel.font = [UIFont systemFontOfSize:11];
        cityLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.55];
        cityLabel.tag = 9102;
        [cell.contentView addSubview:cityLabel];
    }
    
    cell.backgroundColor = [UIColor clearColor];
    return cell;
}

// Swipe to delete — solo per destinazioni recenti (quando searchText è vuoto)
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    // Solo se la search bar è vuota → mostriamo i recenti
    return (self.searchBar.text.length == 0 && _searchField.text.length == 0);
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSDictionary *item = self.searchResults[indexPath.row];
        // Rimuovi dai recenti
        [[SettingsStore shared] removeRecentDestination:item];
        // Rimuovi dalla lista
        [self.searchResults removeObjectAtIndex:indexPath.row];
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
        // Nascondi tabella se vuota
        self.searchResultsTable.hidden = (self.searchResults.count == 0);
    }
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSDictionary *result = self.searchResults[indexPath.row];
    double lat = [result[@"lat"] doubleValue];
    double lon = [result[@"lon"] doubleValue];
    NSString *name = result[@"name"] ?: result[@"displayName"];
    if (!name) name = @"Destinazione";
    
    [self appLog:@"📍 Tappato risultato: %@ (lat=%.4f, lon=%.4f)", name, lat, lon];
    if (lat == 0 && lon == 0) {
        [self appLog:@"❌ Coordinate invalide (0, 0) — salto ricalcolo"];
        return;
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self closeSearch];
        
        if ([SettingsStore shared].transportMode == TransportModeBus) {
            // Bus mode: select bus line — passa ANCHE l'indice pattern/trip esatto
            NSString *lineNum = result[@"lineNumber"] ?: name;
            NSInteger direction = [result[@"direction"] integerValue];
            NSInteger patIdx = [result[@"_patIdx"] integerValue];
            NSInteger tripIdx = [result[@"_tripIdx"] integerValue];
            NSString *js = [NSString stringWithFormat:@"selectBusLine('%@', %ld, %ld, %ld)", lineNum, (long)direction, (long)patIdx, (long)tripIdx];
            [self.webView evaluateJavaScript:js completionHandler:^(id res, NSError *err) {
                if (err) {
                    [self appLog:@"❌ selectBusLine error: %@", err.localizedDescription];
                } else {
                    [self appLog:@"✅ selectBusLine: linea %@ dir %ld", lineNum, (long)direction];
                }
                // Store destination — VAI mostra quando JS chiama routeReady
                self->_pendingDestDict = @{@"lat": @(lat), @"lon": @(lon), @"name": name,
                                           @"lineNumber": lineNum, @"direction": @(direction),
                                           @"isBus": @YES,
                                           @"displayName": result[@"displayName"] ?: @"",
                                           @"displaySubtitle": result[@"displaySubtitle"] ?: @"",
                                           @"departure": result[@"departure"] ?: @"",
                                           @"dirLabel": result[@"dirLabel"] ?: @"",
                                           @"stopCount": result[@"stopCount"] ?: @(0),
                                           @"_patIdx": result[@"_patIdx"] ?: @(0),
                                           @"_tripIdx": result[@"_tripIdx"] ?: @(0)};
                [self appLog:@"   _pendingDestDict (bus) salvato: %@", self->_pendingDestDict];
            }];
        } else {
            // Car mode: calculate OSRM route
            [self setRoutingProfileInJS];
            // Cattura displayName/Subtitle PRIMA del blocco (result nel blocco è output JS!)
            NSString *capturedDisplayName = result[@"displayName"] ?: name;
            NSString *capturedDisplaySubtitle = result[@"displaySubtitle"] ?: @"";
            NSString *js = [NSString stringWithFormat:@"calculateRoute(%f, %f, %f, %f, '%@')",
                            _lastLocation ? _lastLocation.coordinate.latitude : 44.49,
                            _lastLocation ? _lastLocation.coordinate.longitude : 11.34,
                            lat, lon,
                            [name stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]];
            [self appLog:@"🧑‍💻 evaluateJS: calculateRoute(%f, %f, %f, %f, '%@')",
             _lastLocation ? _lastLocation.coordinate.latitude : 44.49,
             _lastLocation ? _lastLocation.coordinate.longitude : 11.34,
             lat, lon, name];
            [self.webView evaluateJavaScript:js completionHandler:^(id jsResult, NSError *error) {
                if (error) {
                    [self appLog:@"❌ calculateRoute error: %@ (procedo comunque)", error.localizedDescription];
                } else {
                    [self appLog:@"✅ calculateRoute completata (JS sta disegnando percorso)"];
                }
                // Store destination — VAI mostra quando JS chiama routeReady
                self->_pendingDestDict = @{@"lat": @(lat), @"lon": @(lon), @"name": name,
                                           @"displayName": capturedDisplayName,
                                           @"displaySubtitle": capturedDisplaySubtitle};
                [self appLog:@"   _pendingDestDict salvato: %@", self->_pendingDestDict];
            }];
        }
    });
}

#pragma mark - Settings

- (void)openSettings {
    // Chiudi ricerca se attiva
    if (self.searchActive_Ivar) {
        [self closeSearch];
    }
    // Nascondi search overlay e finestra autobus
    self.searchOverlay.hidden = YES;
    self.busForceHidden = YES;
    if (self.busVC && !self.busVC.view.hidden) {
        self.busVC.view.hidden = YES;
        [self.busVC.closeXButton setHidden:YES];
        _busWasVisible = YES;
    }
    SettingsViewController *svc = [[SettingsViewController alloc] init];
    svc.mapVC = self;
    [self addChildViewController:svc];
    svc.view.frame = self.view.bounds;
    svc.view.alpha = 0;
    [self.view addSubview:svc.view];
    [svc didMoveToParentViewController:self];
    [UIView animateWithDuration:0.3 animations:^{
        svc.view.alpha = 1;
    }];
}

- (void)updateTopRightButtonForOrientation:(BOOL)isLandscape {
    if (isLandscape) {
        // Paesaggio: pulsante "Mappa" allungato (toggle bus)
        [self.mapButton setImage:[UIImage systemImageNamed:@"map.fill"] forState:UIControlStateNormal];
        [self.mapButton setTitle:@"  Mappa" forState:UIControlStateNormal];
        self.mapButton.layer.cornerRadius = 20; // pill shape
    } else {
        // Portrait: X rotondo
        [self.mapButton setImage:[UIImage systemImageNamed:@"xmark.circle.fill"] forState:UIControlStateNormal];
        [self.mapButton setTitle:@"" forState:UIControlStateNormal];
        self.mapButton.layer.cornerRadius = 20; // round
        self.mapButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
        self.mapButton.tintColor = [UIColor whiteColor];
    }
}

- (void)topRightButtonTapped {
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    BOOL isLandscape = h < w;
    
    if (isLandscape) {
        // Landscape: toggle bus view
        [self toggleBusView];
    } else {
        // Portrait: chiudi bus se visibile, altrimenti chiudi ricerca
        if (self.busVC && !self.busVC.view.hidden) {
            [self.busVC hideBus];
        } else if (self.searchActive_Ivar) {
            [self closeSearch];
        }
    }
}

- (void)modalitaButtonTapped {
    if (self.busVC.view.hidden) {
        [self.busVC showBus];
    } else {
        [self.busVC hideBus];
    }
}

- (void)compassButtonTapped {
    // Resetta orientamento a Nord
    if (self.userTrackingWithHeading) {
        self.userTrackingWithHeading = NO;
    }
    // Call JS to reset bearing to 0
    [self.webView evaluateJavaScript:@"setMapBearing(0)" completionHandler:nil];
}

#pragma mark - Bus View

- (void)showBusView:(BOOL)show fullScreen:(BOOL)full {
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    
    if (show) {
        if (full) {
            // Landscape full screen
            self.modalitaButton.hidden = YES;
            self.mapButton.hidden = YES;
            self.searchButton.hidden = YES;
            self.settingsButton.hidden = YES;
            self.trackingButton.hidden = YES;
            self.compassButton.hidden = YES;
            self.logButton.hidden = YES;
        } else {
            // Portrait: X nel BusVC, qui gestiamo solo i pulsanti mappa
            self.modalitaButton.hidden = YES;
            self.mapButton.hidden = YES;
            self.searchButton.hidden = NO;
            self.settingsButton.hidden = NO;
            self.trackingButton.hidden = NO;
            self.compassButton.hidden = YES;
            self.logButton.hidden = NO;
        }
    } else {
        // Bus hidden
        self.modalitaButton.hidden = NO;
        self.mapButton.hidden = YES;
        self.searchButton.hidden = NO;
        self.settingsButton.hidden = NO;
        self.trackingButton.hidden = NO;
        self.compassButton.hidden = YES;
        self.logButton.hidden = NO;
    }
    // Ogni volta che la visibilità cambia, ripristina posizioni salvate
    [self loadSavedLayout];
}

#pragma mark - Camera Slider (real-time push to JS)

- (void)pushCameraSettingsToJS {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    double alt = [ud objectForKey:@"autista_cam_alt"] ? [ud floatForKey:@"autista_cam_alt"] : 500;
    double pit = [ud objectForKey:@"autista_cam_pit"] ? [ud floatForKey:@"autista_cam_pit"] : 60;
    double off = [ud objectForKey:@"autista_cam_off"] ? [ud floatForKey:@"autista_cam_off"] : 0;
    double voff = [ud objectForKey:@"autista_cam_voff"] ? [ud floatForKey:@"autista_cam_voff"] : 0;
    double zoom = MAX(10.0, MIN(19.0, 18.0 - log2(MAX(alt, 10.0) / 100.0)));
    
    NSString *js = [NSString stringWithFormat:
        @"_camZoom=%f;_camPitch=%f;_camOffset=%f;_camVertOffset=%f;",
        zoom, pit, off, voff];
    [self.webView evaluateJavaScript:js completionHandler:nil];
}

- (void)showSliderValue:(NSString *)text {
    if (!self.floatingSliderLabel) {
        UILabel *lbl = [[UILabel alloc] init];
        lbl.font = [UIFont boldSystemFontOfSize:28];
        lbl.textColor = [UIColor whiteColor];
        lbl.textAlignment = NSTextAlignmentCenter;
        lbl.backgroundColor = [UIColor colorWithWhite:0 alpha:0.55];
        lbl.layer.cornerRadius = 12;
        lbl.layer.masksToBounds = YES;
        lbl.translatesAutoresizingMaskIntoConstraints = NO;
        [self.view addSubview:lbl];
        [NSLayoutConstraint activateConstraints:@[
            [lbl.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
            [lbl.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:-40],
            [lbl.widthAnchor constraintGreaterThanOrEqualToConstant:100],
            [lbl.heightAnchor constraintEqualToConstant:50]
        ]];
        self.floatingSliderLabel = lbl;
    }
    self.floatingSliderLabel.text = text;
    self.floatingSliderLabel.alpha = 0;
    self.floatingSliderLabel.hidden = NO;
    [UIView animateWithDuration:0.15 animations:^{
        self.floatingSliderLabel.alpha = 1.0;
    }];
}

- (void)hideSliderValue {
    if (!self.floatingSliderLabel) return;
    [UIView animateWithDuration:0.2 animations:^{
        self.floatingSliderLabel.alpha = 0;
    } completion:^(BOOL finished) {
        self.floatingSliderLabel.hidden = YES;
    }];
}

- (void)applyCameraSettings {
    // Durante navigazione/simulazione: pusha valori al JS rAF engine in tempo reale
    if (self.isNavigating || _isSimulating) {
        if (!_lastLocation) return;
        [self pushCameraSettingsToJS];
        return;
    }
    if (!_lastLocation) {
        CLLocationCoordinate2D target = CLLocationCoordinate2DMake(44.49, 11.34);
        double zoom = MAX(10.0, MIN(19.0, 18.0 - log2(MAX(self.cameraAltitude, 10.0) / 100.0)));
        NSString *js = [NSString stringWithFormat:
            @"animateCamera(%f, %f, %f, %f, %f);",
            target.latitude, target.longitude, zoom,
            0.0,
            self.cameraPitch];
        [self.webView evaluateJavaScript:js completionHandler:nil];
        return;
    }
    CLLocationDirection heading;
    if (self.isNavigating) {
        // Se in navigazione e in movimento, usa la rotta GPS. Se fermo, mantieni bearing attuale.
        if (_currentSpeed >= 3.0) {
            heading = (_lastLocation.course >= 0) ? _lastLocation.course : _cameraHeading;
        } else {
            heading = _cameraHeading; // mantiene bearing attuale, non oscilla da fermo
        }
    } else if (self.mapOrientationLocked) {
        heading = 0;
    } else {
        heading = MAX(0, _lastLocation.course);
    }
    double headingRad = heading * M_PI / 180.0;
    // Offset: cameraOffset = laterale (destra/sinistra), cameraVerticalOffset = su/giù (longitudinale)
    CLLocationCoordinate2D centerCoord = _lastLocation.coordinate;
    double latPerMeter = 1.0 / 111320.0;
    double lonPerMeter = 1.0 / (111320.0 * cos(centerCoord.latitude * M_PI / 180.0));
    
    // Laterale (perpendicolare alla direzione)
    double latOffset = self.cameraOffset * cos(headingRad + M_PI_2) * latPerMeter;
    double lonOffset = self.cameraOffset * sin(headingRad + M_PI_2) * lonPerMeter;
    
    // Longitudinale (parallelo alla direzione = su/giù)
    double latVert = self.cameraVerticalOffset * cos(headingRad) * latPerMeter;
    double lonVert = self.cameraVerticalOffset * sin(headingRad) * lonPerMeter;
    
    // Combinato
    CLLocationCoordinate2D target = CLLocationCoordinate2DMake(
        centerCoord.latitude + latOffset + latVert,
        centerCoord.longitude + lonOffset + lonVert);

    // Altitudine dinamica in base alla velocità (solo in navigazione)
    CLLocationDistance effectiveAltitude = self.cameraAltitude;
    if (self.isNavigating) {
        double speedKmh = _currentSpeed * 3.6;
        double factor = 1.0 + MAX(0, (speedKmh - 40.0) / 160.0) * 2.0;
        factor = MIN(factor, 3.0);
        effectiveAltitude = self.cameraAltitude * factor;
    }

    // BUGFIX: da fermo in navigazione NON aggiornare _cameraHeading — evitava rotazione di 30° ogni 2s!
    if (self.isNavigating && _currentSpeed < 3.0) {
        _cameraHeading = heading; // mantieni esattamente, senza aggiungere offset
    } else {
        _cameraHeading = heading; // mantieni esattamente
    }
    
    // JS calls for camera - zoom based on altitude, max zoom 21.5 per ~10m reali
    double zoom = MAX(10.0, MIN(21.5, 18.0 - log2(MAX(effectiveAltitude, 10.0) / 100.0)));
    // Use single animateCamera to avoid conflicting animations
    NSString *js = [NSString stringWithFormat:
        @"animateCamera(%f, %f, %f, %f, %f);",
        target.latitude, target.longitude, zoom,
        _cameraHeading,
        self.cameraPitch];
    [self.webView evaluateJavaScript:js completionHandler:nil];
}

- (void)applyMenuOpacity {
    CGFloat a = 0.15 + self.menuOpacity * 0.55;
    self.mapButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:a];
    self.modalitaButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:a];
    self.searchButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:a];
    self.trackingButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:a];
    self.settingsButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:a];
    self.compassButton.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:a];
    [self.busVC applyGlassOpacity:a];
}

#pragma mark - Preview Route

- (void)previewRouteTo:(NSDictionary *)destinationDict {
    // destinationDict = {lat, lon, name}
    double destLat = [destinationDict[@"lat"] doubleValue];
    double destLon = [destinationDict[@"lon"] doubleValue];
    NSString *destName = destinationDict[@"name"] ?: @"Destinazione";
    
    double fromLat = _lastLocation ? _lastLocation.coordinate.latitude : 44.49;
    double fromLon = _lastLocation ? _lastLocation.coordinate.longitude : 11.34;
    
    NSString *escapedName = [destName stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
    // Imposta profilo routing
    [self setRoutingProfileInJS];
    NSString *js = [NSString stringWithFormat:
        @"calculateRoute(%f, %f, %f, %f, '%@')",
        fromLat, fromLon, destLat, destLon, escapedName];
    
    [self.webView evaluateJavaScript:js completionHandler:^(id result, NSError *error) {
        if (error) {
            [self showAlert:@"Errore navigazione" message:error.localizedDescription];
            return;
        }
        if (!result) {
            [self showAlert:@"Errore navigazione" message:@"Nessun percorso trovato"];
            return;
        }
        // result is a JSON string with route info
        NSString *routeJSON = nil;
        if ([result isKindOfClass:[NSString class]]) {
            routeJSON = (NSString *)result;
        } else if ([result isKindOfClass:[NSDictionary class]]) {
            NSData *jsonData = [NSJSONSerialization dataWithJSONObject:result options:0 error:nil];
            if (jsonData) routeJSON = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self->_pendingDestDict = destinationDict;
            [self showGoButton];
        });
    }];
}

- (void)showGoButton {
    if (self.isNavigating || _isSimulating) {
        [self appLog:@"⏭️ showGoButton saltato: navigazione/simulazione attiva"];
        return;
    }
    if (self->_goButton) return;
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    
    // Bus mode: mostra UN SOLO pulsante per la direzione SELEZIONATA, centrato
    if ([SettingsStore shared].transportMode == TransportModeBus && self->_pendingDestDict) {
        NSInteger selectedDir = [self->_pendingDestDict[@"direction"] integerValue];
        
        [self.webView evaluateJavaScript:@"getBusRouteInfo()" completionHandler:^(id infoStr, NSError *e) {
            NSString *andataRoute = @"";
            NSString *ritornoRoute = @"";
            if ([infoStr isKindOfClass:[NSString class]]) {
                NSData *d = [(NSString *)infoStr dataUsingEncoding:NSUTF8StringEncoding];
                NSDictionary *info = [NSJSONSerialization JSONObjectWithData:d options:0 error:nil];
                if ([info isKindOfClass:[NSDictionary class]]) {
                    andataRoute = info[@"andata"] ?: @"";
                    ritornoRoute = info[@"ritorno"] ?: @"";
                }
            }
            
            dispatch_async(dispatch_get_main_queue(), ^{
                CGFloat btnW = 200;
                CGFloat btnH = 55;
                CGFloat btnY = h - 160;
                CGFloat startX = (w - btnW) / 2; // centrato
                
                BOOL isRitorno = (selectedDir == 1);
                
                // UN solo pulsante — direzione SELEZIONATA
                UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
                btn1.frame = CGRectMake(startX, btnY, btnW, btnH);
                btn1.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
                btn1.layer.cornerRadius = 14;
                btn1.clipsToBounds = YES;
                [btn1 addTarget:self action:@selector(goButtonTapped) forControlEvents:UIControlEventTouchUpInside];
                
                NSString *btnLabel = isRitorno ? @"RITORNO" : @"ANDATA";
                UIColor *btnColor = isRitorno ? [UIColor colorWithRed:0.80 green:0.15 blue:0.15 alpha:0.95] : [UIColor colorWithRed:0.13 green:0.70 blue:0.25 alpha:0.95];
                btn1.backgroundColor = btnColor;
                
                UILabel *label1 = [[UILabel alloc] init];
                label1.frame = CGRectMake(4, 6, btnW - 8, 22);
                label1.text = btnLabel;
                label1.font = [UIFont boldSystemFontOfSize:18];
                label1.textColor = [UIColor whiteColor];
                label1.textAlignment = NSTextAlignmentCenter;
                [btn1 addSubview:label1];
                
                UILabel *route1 = [[UILabel alloc] init];
                route1.frame = CGRectMake(4, 28, btnW - 8, 20);
                route1.text = isRitorno ? ritornoRoute : andataRoute;
                route1.font = [UIFont systemFontOfSize:10];
                route1.textColor = [UIColor colorWithWhite:0.90 alpha:1.0];
                route1.textAlignment = NSTextAlignmentCenter;
                route1.numberOfLines = 2;
                route1.adjustsFontSizeToFitWidth = YES;
                route1.minimumScaleFactor = 0.6;
                [btn1 addSubview:route1];
                
                [self.view addSubview:btn1];
                self->_goButton = btn1;
                self->_retButton = nil; // nessun secondo pulsante
                
                // Bottone SIMULAZIONE BUS (sotto il pulsante direzione)
                CGFloat simBtnY = btnY + btnH + 10;
                UIButton *busSim = [UIButton buttonWithType:UIButtonTypeSystem];
                busSim.frame = CGRectMake(w/2 - 70, simBtnY, 140, 36);
                busSim.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
                busSim.backgroundColor = [UIColor colorWithRed:0.2 green:0.6 blue:0.2 alpha:0.95];
                busSim.layer.cornerRadius = 18;
                busSim.tintColor = [UIColor whiteColor];
                busSim.titleLabel.font = [UIFont boldSystemFontOfSize:14];
                [busSim setTitle:@"▶ Sim Bus" forState:UIControlStateNormal];
                [busSim addTarget:self action:@selector(busSimButtonTapped) forControlEvents:UIControlEventTouchUpInside];
                [self.view addSubview:busSim];
                self->_busSimButton = busSim;
                
                [self appLog:@"✅ Mostrato pulsante %@ (centrato, senza opposto) + Sim Bus", btnLabel];
            });
        }];
        return;
    }
    
    // Auto mode: pulsante VAI standard
    UIButton *go = [UIButton buttonWithType:UIButtonTypeSystem];
    go.frame = CGRectMake(w/2 - 70, h - 150, 140, 50);
    go.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
    go.backgroundColor = [UIColor colorWithRed:0.0 green:0.45 blue:0.9 alpha:0.9];
    go.layer.cornerRadius = 25;
    go.tintColor = [UIColor whiteColor];
    go.titleLabel.font = [UIFont boldSystemFontOfSize:20];
    [go setTitle:@"VAI" forState:UIControlStateNormal];
    [go addTarget:self action:@selector(goButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:go];
    self->_goButton = go;
    
    // Pulsante Simulazione
    UIButton *sim = [UIButton buttonWithType:UIButtonTypeSystem];
    sim.frame = CGRectMake(w/2 - 70, h - 90, 140, 42);
    sim.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
    sim.backgroundColor = [UIColor colorWithRed:0.2 green:0.6 blue:0.2 alpha:0.95];
    sim.layer.cornerRadius = 21;
    sim.tintColor = [UIColor whiteColor];
    sim.titleLabel.font = [UIFont boldSystemFontOfSize:16];
    [sim setTitle:@"▶ Simulazione" forState:UIControlStateNormal];
    [sim addTarget:self action:@selector(simulationButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:sim];
    self->_simButton = sim;
}

- (void)hideGoButton {
    [self->_goButton removeFromSuperview];
    self->_goButton = nil;
    [self->_simButton removeFromSuperview];
    self->_simButton = nil;
    [self->_retButton removeFromSuperview];
    self->_retButton = nil;
    [self->_busSimButton removeFromSuperview];
    self->_busSimButton = nil;

    // Ferma simulazione se attiva
    if (_isSimulating) {
        _isSimulating = NO;
        [_simTimer invalidate]; _simTimer = nil;
        _simCoords = nil;
        _simIndex = 0;
    }
    // Ferma bus simulazione se attiva
    if (_isBusSimulating) {
        _isBusSimulating = NO;
        [_busSimTimer invalidate]; _busSimTimer = nil;
        _busSimCoords = nil;
        _busSimIndex = 0;
    }
}

- (void)goButtonTapped {
    [self appLog:@"🟢 VAI premuto"];
    [self hideGoButton];
    if (self->_pendingDestDict) {
        [self appLog:@"   pendingDestDict ok: %@", self->_pendingDestDict];
        // Salva nei recenti
        TransportMode saveMode = [self->_pendingDestDict[@"isBus"] boolValue] ? TransportModeBus : TransportModeAuto;
        [[SettingsStore shared] addRecentDestination:self->_pendingDestDict forMode:saveMode];
        
        if ([self->_pendingDestDict[@"isBus"] boolValue]) {
            // Bus mode: start bus navigation
            [self.webView evaluateJavaScript:@"startBusNavigation();" completionHandler:^(id res, NSError *err) {
                if (err) [self appLog:@"❌ startBusNavigation JS error: %@", err.localizedDescription];
                else [self appLog:@"✅ startBusNavigation JS ok"];
            }];
        } else {
            // Car mode: standard navigation
            [self startNavigationTo:self->_pendingDestDict];
        }
        
        self->_pendingDestDict = nil;
        [self appLog:@"   pendingDestDict azzerato"];
    } else {
        [self appLog:@"❌ VAI: _pendingDestDict è NIL"];
    }
}

- (void)ritornoButtonTapped {
    [self appLog:@"🔴 RITORNO premuto"];
    [self hideGoButton];
    if (self->_pendingDestDict) {
        [self appLog:@"   pendingDestDict ok: %@", self->_pendingDestDict];
        [[SettingsStore shared] addRecentDestination:self->_pendingDestDict forMode:TransportModeBus];
        // Chiama startBusNavigation('ritorno') per navigare il percorso di ritorno
        [self.webView evaluateJavaScript:@"startBusNavigation('ritorno');" completionHandler:^(id res, NSError *err) {
            if (err) [self appLog:@"❌ startBusNavigation(ritorno) JS error: %@", err.localizedDescription];
            else [self appLog:@"✅ startBusNavigation(ritorno) JS ok"];
        }];
        self->_pendingDestDict = nil;
    } else {
        [self appLog:@"❌ RITORNO: _pendingDestDict è NIL"];
    }
}

#pragma mark - Simulazione

- (void)simulationButtonTapped {
    [self appLog:@"🟢 Simulazione premuta"];
    [self hideGoButton];
    if (!self->_pendingDestDict) {
        [self appLog:@"❌ Simulazione: _pendingDestDict è NIL"];
        return;
    }
    
    NSDictionary *dest = [self->_pendingDestDict copy];
    TransportMode saveMode = [dest[@"isBus"] boolValue] ? TransportModeBus : TransportModeAuto;
    [[SettingsStore shared] addRecentDestination:dest forMode:saveMode];
    self->_pendingDestDict = nil;
    
    if ([dest[@"isBus"] boolValue]) {
        // Bus mode: usa startBusNavigation() che calcola OSRM attraverso le fermate
        // e aggiorna la mappa col percorso corretto
        [self appLog:@"🚌 Simulazione BUS — chiamo startBusNavigation() JS"];
        _simCoords = nil; // azzera coordinate vecchie (auto) per evitare mismatch
        [self.webView evaluateJavaScript:@"startBusNavigation();" completionHandler:^(id res, NSError *err) {
            if (err) [self appLog:@"❌ startBusNavigation JS error: %@", err.localizedDescription];
            else [self appLog:@"✅ startBusNavigation JS ok — attendo simCoords"];
        }];
        // _simCoords arriverà via simCoords message handler → fireSimulation
        _simPending = YES;
        return;
    }
    
    // _simCoords è già popolato da simCoords message handler (quando OSRM ha risposto)
    if (!_simCoords || _simCoords.count < 2) {
        [self appLog:@"⚠️ _simCoords vuoto — calcolo route OSRM..."];
        double fromLat = _lastLocation ? _lastLocation.coordinate.latitude : 44.49;
        double fromLon = _lastLocation ? _lastLocation.coordinate.longitude : 11.34;
        double toLat = [dest[@"lat"] doubleValue];
        double toLon = [dest[@"lon"] doubleValue];
        NSString *name = dest[@"name"] ?: @"";
        NSString *escaped = [name stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
        [self setRoutingProfileInJS];
        NSString *calcJs = [NSString stringWithFormat:
            @"calculateRoute(%f, %f, %f, %f, '%@')",
            fromLat, fromLon, toLat, toLon, escaped];
        [self.webView evaluateJavaScript:calcJs completionHandler:^(id r, NSError *e) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self appLog:@"⚠️ OSRM completion — _simCoords count=%lu", (unsigned long)(_simCoords ? [_simCoords count] : 0)];
                if (_simCoords && _simCoords.count >= 2) {
                    [self startNavigationTo:dest];
                    [self fireSimulation];
                } else {
                    [self appLog:@"❌ Simulazione: impossibile ottenere coordinate"];
                }
            });
        }];
        return;
    }
    
    [self appLog:@"✅ _simCoords già pronto (%lu coordinate) — avvio subito", (unsigned long)[_simCoords count]];
    [self startNavigationTo:dest];
    [self fireSimulation];
}

- (void)fireSimulation {
    if (!_simCoords || _simCoords.count < 2) {
        [self appLog:@"❌ fireSimulation: _simCoords insufficienti (count=%lu)", (unsigned long)[_simCoords count]];
        return;
    }
    _simIndex = 0;
    _posUpdateCounter = 0;
    _isSimulating = YES;
    [self appLog:@"🔥 fireSimulation: %lu coord, stepPerTick=%.3f m", (unsigned long)[_simCoords count], _simStepPerTick];
    NSDictionary *first = _simCoords[0];
    double firstLat = [first[@"lat"] doubleValue];
    double firstLon = [first[@"lon"] doubleValue];
    [self appLog:@"   prima posizione: %.6f, %.6f", firstLat, firstLon];
    [self setSimulatedLocation:firstLat lon:firstLon course:0];
    // FORZA pushPosition + startNavEngine SUBITO (senza aspettare 12 tick)
    [self pushPositionToJS];
    [self.webView evaluateJavaScript:@"if(!_navEngineRunning)startNavEngine();" completionHandler:nil];
    _simTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:@selector(simTick) userInfo:nil repeats:YES];
    [self appLog:@"✅ Simulazione avviata: %lu coord, %.3f m/tick, 60fps", (unsigned long)[_simCoords count], _simStepPerTick];
}

- (void)setSimulatedLocation:(double)lat lon:(double)lon course:(double)course {
    // Aggiorna _lastLocation con posizione simulata
    CLLocation *loc = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(lat, lon)
                                                    altitude:0
                                          horizontalAccuracy:5
                                            verticalAccuracy:5
                                                      course:course
                                                       speed:4.17
                                                   timestamp:[NSDate date]];
    _prevLocation = _lastLocation;
    _lastLocation = loc;
    _currentSpeed = 4.17;
    _currentCourse = course;
    
    // NavPush: invia pushPosition ogni ~50ms (più frequente per startup veloce)
    // Il JS rAF engine bufferizza e interpola a 60fps
    static int _camCounter = 0;
    _camCounter++;
    if (_camCounter >= 3) {
        _camCounter = 0;
        [self pushPositionToJS];
    }
}

- (void)simTick {
    if (!_isSimulating || !_simCoords || _simIndex >= (NSInteger)[_simCoords count] - 1) {
        if (_isSimulating) {
            [self appLog:@"✅ Simulazione completata"];
            _isSimulating = NO;
            [_simTimer invalidate]; _simTimer = nil;
            _simCoords = nil;
            [self.webView evaluateJavaScript:@"stopNavEngine();" completionHandler:nil];
            // 🔥 Arrivo: messaggio + stop navigazione automatico
            dispatch_async(dispatch_get_main_queue(), ^{
                [self announceArrival];
            });
        }
        return;
    }
    
    double remain = _simStepPerTick; // metri per tick
    
    // PARTENZA dalla posizione CORRENTE (accumulata dai tick precedenti)
    double curLat, curLon;
    if (_lastLocation) {
        curLat = _lastLocation.coordinate.latitude;
        curLon = _lastLocation.coordinate.longitude;
    } else {
        NSDictionary *first = _simCoords.firstObject;
        curLat = [first[@"lat"] doubleValue];
        curLon = [first[@"lon"] doubleValue];
    }
    
    while (remain > 0 && _simIndex < (NSInteger)[_simCoords count] - 1) {
        NSDictionary *nextDict = _simCoords[_simIndex + 1];
        double nextLat = [nextDict[@"lat"] doubleValue];
        double nextLon = [nextDict[@"lon"] doubleValue];
        
        // Distanza da cur a next
        double dlat = (nextLat - curLat) * M_PI / 180.0;
        double dlon = (nextLon - curLon) * M_PI / 180.0;
        double a = sin(dlat/2) * sin(dlat/2) + cos(curLat * M_PI / 180.0) * cos(nextLat * M_PI / 180.0) * sin(dlon/2) * sin(dlon/2);
        double c = 2 * atan2(sqrt(a), sqrt(1-a));
        double distToNext = 6371000 * c; // metri
        
        if (distToNext <= remain + 0.001) {
            // RAGGIUNTO il prossimo punto! Avanza _simIndex
            remain -= distToNext;
            _simIndex++;
            curLat = nextLat;
            curLon = nextLon;
        } else {
            // Interpola lungo il segmento da posizione corrente
            double frac = remain / distToNext;
            curLat = curLat + (nextLat - curLat) * frac;
            curLon = curLon + (nextLon - curLon) * frac;
            remain = 0;
        }
    }
    
    // Calcola rotta (bearing) verso il prossimo punto
    double course = 0;
    if (_simIndex < (NSInteger)[_simCoords count] - 1) {
        NSDictionary *next = _simCoords[_simIndex + 1];
        double nLat = [next[@"lat"] doubleValue] * M_PI / 180.0;
        double nLon = [next[@"lon"] doubleValue] * M_PI / 180.0;
        double cLat = curLat * M_PI / 180.0;
        double cLon = curLon * M_PI / 180.0;
        double dLon = nLon - cLon;
        double y = sin(dLon) * cos(nLat);
        double x = cos(cLat) * sin(nLat) - sin(cLat) * cos(nLat) * cos(dLon);
        course = atan2(y, x) * 180.0 / M_PI;
        if (course < 0) course += 360;
    }
    
    [self appLog:@"  🧭 course=%.0f → setSimLoc(%.6f, %.6f)", course, curLat, curLon];
    [self setSimulatedLocation:curLat lon:curLon course:course];
    
    if (_simIndex >= (NSInteger)[_simCoords count] - 1) {
        [self appLog:@"✅ Simulazione completata"];
        _isSimulating = NO;
        [_simTimer invalidate]; _simTimer = nil;
        _simCoords = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            [self announceArrival];
        });
    }
}

#pragma mark - Bus Simulazione (parallela, non tocca simulazione auto)

- (void)busSimButtonTapped {
    [self appLog:@"🟢 Sim Bus premuta"];
    [self hideGoButton];
    
    // Ferma SUBITO smoothTimer (GPS reale) — prima di qualsiasi JS async
    [self stopSmoothTimer];
    self->_isInterpolating = NO;
    
    // Nascondi SUBITO layer user-location (ghost)
    [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('user-location-layer','visibility','none')}catch(e){};try{map.setLayoutProperty('user-location-ring','visibility','none')}catch(e){}" completionHandler:nil];
    
    // v3.504: Usa la SHAPE PFAEDLE (trip.shape, [[lat,lon],...]) invece del OSRM inesistente
    // _busOsrmCoords non viene mai popolato → la freccia andava dritta tra fermate
    
    // FORZA barra via visibile SUBITO, sincrono, prima di qualsiasi async
    // v3.543: removeAllAnimations NON basta → endNavigation ha già settato alpha=0 nel model layer
    [self.roadNameLabel.layer removeAllAnimations];
    [self.roadETAPill.layer removeAllAnimations];
    [self.roadTimePill.layer removeAllAnimations];
    [self.roadDistPill.layer removeAllAnimations];
    self.roadNameLabel.alpha = 1.0;
    self.roadNameLabel.hidden = NO;
    self.roadStreetLabel.text = @"";
    
    // v3.552: Mostra panel info bus con slide-down + reset distanza update
    _busLastUpdateDist = 0;
    [self showBusInfoPanel];
    
    [self.webView evaluateJavaScript:@"JSON.stringify((_busNavData && _busNavData.trip && _busNavData.trip.shape && _busNavData.trip.shape.length >= 4) ? _busNavData.trip.shape.map(function(p){return{lat:p[0],lon:p[1]}}) : [])" completionHandler:^(id res, NSError *err) {
        if (err || ![res isKindOfClass:[NSString class]]) {
            [self appLog:@"❌ Sim Bus: impossibile ottenere shape dal JS"];
            // ERRORE: shape non disponibile — RIPRISTINA layer user-location
            [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('user-location-layer','visibility','visible')}catch(e){};try{map.setLayoutProperty('user-location-ring','visibility','visible')}catch(e){}" completionHandler:nil];
            [self hideBusInfoPanel];
            return;
        }
        NSData *jsonData = [(NSString *)res dataUsingEncoding:NSUTF8StringEncoding];
        NSArray *coords = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
        if (![coords isKindOfClass:[NSArray class]] || coords.count < 4) {
            [self appLog:@"❌ Sim Bus: shape insufficienti (count=%lu) — nessun fallback disponibile", (unsigned long)[coords count]];
            // ERRORE: shape insufficienti — RIPRISTINA layer user-location
            [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('user-location-layer','visibility','visible')}catch(e){};try{map.setLayoutProperty('user-location-ring','visibility','visible')}catch(e){}" completionHandler:nil];
            [self hideBusInfoPanel];
            return;
        }
        
        [self appLog:@"✅ Sim Bus: %lu coordinate dalla shape (percorso esatto del bus)", (unsigned long)[coords count]];
        self->_busSimCoords = [coords copy];
        self->_busSimStepPerTick = 0.1389; // 0.14 m/tick a 60fps = ~30 km/h
        
        // Nascondi layer user-location MapLibre (ghost posizione GPS reale)
        [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('user-location-layer','visibility','none')}catch(e){};try{map.setLayoutProperty('user-location-ring','visibility','none')}catch(e){}" completionHandler:nil];
        
        // Imposta navigazione
        self.isNavigating = YES;
        self.userTracking = YES;
        self.userTrackingWithHeading = YES;
        [self updateTrackingButton];
        self.navBar.hidden = YES;
        self.stopNavButton.hidden = NO;
        if (self.roadNameLabel) {
            self.roadNameLabel.hidden = NO;
        }
        
        // v3.554: Fetch stops + avvia simulazione. Fallback se stops vuoti.
        [self.webView evaluateJavaScript:@"getBusTripStopsJson()" completionHandler:^(id stopsRes, NSError *stopsErr) {
            if (!stopsErr && [stopsRes isKindOfClass:[NSString class]]) {
                NSData *jd = [(NSString *)stopsRes dataUsingEncoding:NSUTF8StringEncoding];
                NSArray *stops = [NSJSONSerialization JSONObjectWithData:jd options:0 error:nil];
                if ([stops isKindOfClass:[NSArray class]] && stops.count > 0) {
                    self->_busTripStops = stops;
                    self->_busCurrentStopIdx = 0;
                    [self appLog:@"✅ Bus stops: %lu fermate", (unsigned long)stops.count];
                } else {
                    [self appLog:@"⚠️ Bus stops VUOTI — sim senza metriche"];
                }
            } else {
                [self appLog:@"⚠️ Bus stops fetch FALLITO — sim senza metriche"];
            }
            [self fireBusSimulation];
        }];
    }];
}

- (void)fireBusSimulation {
    if (!_busSimCoords || _busSimCoords.count < 2) {
        [self appLog:@"❌ fireBusSimulation: coordinate insufficienti"];
        return;
    }
    _busSimIndex = 0;
    _isBusSimulating = YES;
    [self appLog:@"🔥 fireBusSimulation: %lu coord, stepPerTick=%.3f m", (unsigned long)[_busSimCoords count], _busSimStepPerTick];
    NSDictionary *first = _busSimCoords[0];
    double firstLat = [first[@"lat"] doubleValue];
    double firstLon = [first[@"lon"] doubleValue];
    [self setBusSimulatedLocation:firstLat lon:firstLon course:0];
    [self pushBusPositionToJS];
    // Setta variabili camera (zoom/pitch/offset) — il rAF engine JS gestisce la camera smooth
    [self pushCameraSettingsToJS];
    [self.webView evaluateJavaScript:@"if(!_navEngineRunning)startNavEngine();" completionHandler:nil];
    _busSimTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:@selector(busSimTick) userInfo:nil repeats:YES];
    // Prima chiamata immediata: mostra subito il nome della via (FORZA barra visibile)
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.roadNameLabel.layer removeAllAnimations];
        self.roadNameLabel.alpha = 1.0;
        self.roadNameLabel.hidden = NO;
    });
    [self.webView evaluateJavaScript:[NSString stringWithFormat:@"getRoadName(%f,%f)", firstLat, firstLon] completionHandler:^(id res, NSError *err) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if ([res isKindOfClass:[NSString class]] && [(NSString *)res length] > 0) {
                self.roadStreetLabel.text = (NSString *)res;
            }
        });
    }];
    [self appLog:@"✅ Bus simulazione avviata: %lu coord, %.3f m/tick, 60fps", (unsigned long)[_busSimCoords count], _busSimStepPerTick];
}

- (void)setBusSimulatedLocation:(double)lat lon:(double)lon course:(double)course {
    _busSimLat = lat;
    _busSimLon = lon;
    _busSimCourse = course;
    _hasBusSimPos = YES;
    _currentCourse = course;
    _currentSpeed = 4.17;
    _cameraHeading = course;
    // Aggiorna _lastLocation per far funzionare navTick (getNavState → nome via)
    CLLocation *loc = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(lat, lon)
                                                    altitude:0
                                          horizontalAccuracy:5
                                            verticalAccuracy:5
                                                      course:course
                                                       speed:4.17
                                                   timestamp:[NSDate date]];
    _prevLocation = _lastLocation;
    _lastLocation = loc;
}

- (void)busSimTick {
    if (!_isBusSimulating || !_busSimCoords || _busSimIndex >= (NSInteger)[_busSimCoords count] - 1) {
        if (_isBusSimulating) {
            [self appLog:@"✅ Bus simulazione completata"];
            _isBusSimulating = NO;
            [_busSimTimer invalidate]; _busSimTimer = nil;
            _busSimCoords = nil;
            _hasBusSimPos = NO;
            // Ripristina layer user-location
            [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('user-location-layer','visibility','visible')}catch(e){};try{map.setLayoutProperty('user-location-ring','visibility','visible')}catch(e){}" completionHandler:nil];
            [self.webView evaluateJavaScript:@"stopNavEngine();" completionHandler:nil];
            // 🔥 Arrivo bus: messaggio + stop navigazione automatico
            dispatch_async(dispatch_get_main_queue(), ^{
                [self announceArrival];
            });
        }
        return;
    }
    
    double remain = _busSimStepPerTick;
    
    double curLat = _busSimLat;
    double curLon = _busSimLon;
    
    // Al primo tick, prendi dalla prima coordinata
    if (_busSimIndex == 0 && curLat == 0 && curLon == 0) {
        NSDictionary *first = _busSimCoords.firstObject;
        curLat = [first[@"lat"] doubleValue];
        curLon = [first[@"lon"] doubleValue];
    }
    
    while (remain > 0 && _busSimIndex < (NSInteger)[_busSimCoords count] - 1) {
        NSDictionary *nextDict = _busSimCoords[_busSimIndex + 1];
        double nextLat = [nextDict[@"lat"] doubleValue];
        double nextLon = [nextDict[@"lon"] doubleValue];
        
        double dlat = (nextLat - curLat) * M_PI / 180.0;
        double dlon = (nextLon - curLon) * M_PI / 180.0;
        double a = sin(dlat/2) * sin(dlat/2) + cos(curLat * M_PI / 180.0) * cos(nextLat * M_PI / 180.0) * sin(dlon/2) * sin(dlon/2);
        double c = 2 * atan2(sqrt(a), sqrt(1-a));
        double distToNext = 6371000 * c;
        
        if (distToNext <= remain + 0.001) {
            remain -= distToNext;
            _busSimIndex++;
            curLat = nextLat;
            curLon = nextLon;
        } else {
            double frac = remain / distToNext;
            curLat = curLat + (nextLat - curLat) * frac;
            curLon = curLon + (nextLon - curLon) * frac;
            remain = 0;
        }
    }
    
    double course = 0;
    if (_busSimIndex < (NSInteger)[_busSimCoords count] - 1) {
        NSDictionary *next = _busSimCoords[_busSimIndex + 1];
        double nLat = [next[@"lat"] doubleValue] * M_PI / 180.0;
        double nLon = [next[@"lon"] doubleValue] * M_PI / 180.0;
        double cLat = curLat * M_PI / 180.0;
        double cLon = curLon * M_PI / 180.0;
        double dLon = nLon - cLon;
        double y = sin(dLon) * cos(nLat);
        double x = cos(cLat) * sin(nLat) - sin(cLat) * cos(nLat) * cos(dLon);
        course = atan2(y, x) * 180.0 / M_PI;
        if (course < 0) course += 360;
    }
    
    [self setBusSimulatedLocation:curLat lon:curLon course:course];
    
    // Push posizione a ~20fps (1 ogni 3 tick), come auto sim
    static int _busPushCounter = 0;
    _busPushCounter++;
    if (_busPushCounter >= 3) {
        _busPushCounter = 0;
        [self pushBusPositionToJS];
    }
    
    // Ogni ~50ms (3 tick) legge nome via
    static int _busStreetCounter = 0;
    _busStreetCounter++;
    if (_busStreetCounter >= 3) {
        _busStreetCounter = 0;
        double sLat = _busSimLat, sLon = _busSimLon;
        // Forza barra visibile OGNI volta, anche se nome non trovato
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.roadNameLabel.layer removeAllAnimations];
            if (self.roadNameLabel.alpha < 0.5) self.roadNameLabel.alpha = 1.0;
            self.roadNameLabel.hidden = NO;
        });
        [self.webView evaluateJavaScript:[NSString stringWithFormat:@"getRoadName(%f,%f)", sLat, sLon] completionHandler:^(id res, NSError *err) {
            if ([res isKindOfClass:[NSString class]] && [(NSString *)res length] > 0) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.roadStreetLabel.text = (NSString *)res;
                });
            }
        }];
        // Aggiorna camera slider in tempo reale (come auto sim via navTick/pushCameraSettingsToJS)
        [self pushCameraSettingsToJS];
        // v3.544: Aggiorna panel info bus (prossima fermata, arrivo, anticipo, ritardo, distanza)
        [self updateBusInfoPanel];
    }
    
    if (_busSimIndex >= (NSInteger)[_busSimCoords count] - 1) {
        [self appLog:@"✅ Bus simulazione completata"];
        _isBusSimulating = NO;
        [_busSimTimer invalidate]; _busSimTimer = nil;
        _busSimCoords = nil;
        _hasBusSimPos = NO;
        // v3.544: Nascondi panel info bus a fine simulazione
        [self hideBusInfoPanel];
        _busTripStops = nil;
        // Ripristina layer user-location
        [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('user-location-layer','visibility','visible')}catch(e){};try{map.setLayoutProperty('user-location-ring','visibility','visible')}catch(e){}" completionHandler:nil];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self announceArrival];
        });
    }
}

- (void)pushBusPositionToJS {
    if (!_hasBusSimPos) return;
    [self pushPositionToJSWithLat:_busSimLat lon:_busSimLon course:_busSimCourse];
}

- (void)busApplyCameraSettings {
    if (!_hasBusSimPos) return;
    
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    double alt = [ud objectForKey:@"autista_cam_alt"] ? [ud floatForKey:@"autista_cam_alt"] : 500;
    double pit = [ud objectForKey:@"autista_cam_pit"] ? [ud floatForKey:@"autista_cam_pit"] : 60;
    double off = [ud objectForKey:@"autista_cam_off"] ? [ud floatForKey:@"autista_cam_off"] : 0;
    double voff = [ud objectForKey:@"autista_cam_voff"] ? [ud floatForKey:@"autista_cam_voff"] : 0;
    
    CLLocationDirection heading;
    if (_currentSpeed >= 3.0) {
        heading = (_busSimCourse >= 0) ? _busSimCourse : _cameraHeading;
    } else {
        heading = _cameraHeading;
    }
    double headingRad = heading * M_PI / 180.0;
    CLLocationCoordinate2D centerCoord = CLLocationCoordinate2DMake(_busSimLat, _busSimLon);
    double latPerMeter = 1.0 / 111320.0;
    double lonPerMeter = 1.0 / (111320.0 * cos(centerCoord.latitude * M_PI / 180.0));
    
    double latOffset = off * cos(headingRad + M_PI_2) * latPerMeter;
    double lonOffset = off * sin(headingRad + M_PI_2) * lonPerMeter;
    double latVert = voff * cos(headingRad) * latPerMeter;
    double lonVert = voff * sin(headingRad) * lonPerMeter;
    
    CLLocationCoordinate2D target = CLLocationCoordinate2DMake(
        centerCoord.latitude + latOffset + latVert,
        centerCoord.longitude + lonOffset + lonVert);
    
    double speedKmh = _currentSpeed * 3.6;
    double factor = 1.0 + MAX(0, (speedKmh - 40.0) / 160.0) * 2.0;
    factor = MIN(factor, 3.0);
    CLLocationDistance effectiveAltitude = alt * factor;
    
    _cameraHeading = heading;
    
    double zoom = MAX(10.0, MIN(21.5, 18.0 - log2(MAX(effectiveAltitude, 10.0) / 100.0)));
    NSString *js = [NSString stringWithFormat:
        @"animateCamera(%f, %f, %f, %f, %f);",
        target.latitude, target.longitude, zoom,
        _cameraHeading,
        pit];
    [self.webView evaluateJavaScript:js completionHandler:nil];
}

#pragma mark - Navigation

- (void)startNavigationTo:(NSDictionary *)destinationDict {
    [self appLog:@"🟢 startNavigationTo:"];
    self.isNavigating = YES;
    self.currentStepIndex = 0;
    self.distanceRemaining = 0;
    self.etaSeconds = 0;
    self.userTracking = YES;
    self.userTrackingWithHeading = YES;
    [self updateTrackingButton];
    // navBar nascosta — info nella pill via in basso
    self.navBar.hidden = YES;
    self.stopNavButton.hidden = NO;
    
    // Mostra pill via con fade-in
    // Resetta label via (potrebbe essere "Linea X" da bus sim)
    // Rimuovi animazioni pendenti (endNavigation potrebbe ancora star facendo fade-out)
    [self.roadNameLabel.layer removeAllAnimations];
    [self.roadETAPill.layer removeAllAnimations];
    [self.roadTimePill.layer removeAllAnimations];
    [self.roadDistPill.layer removeAllAnimations];
    self.roadStreetLabel.text = @"";
    if (self.roadNameLabel) {
        self.roadNameLabel.hidden = NO;
        self.roadNameLabel.alpha = 0;
        self.roadETAPill.hidden = NO;
        self.roadETAPill.alpha = 0;
        self.roadTimePill.hidden = NO;
        self.roadTimePill.alpha = 0;
        self.roadDistPill.hidden = NO;
        self.roadDistPill.alpha = 0;
        [UIView animateWithDuration:0.3 animations:^{
            self.roadNameLabel.alpha = 1;
            self.roadETAPill.alpha = 1;
            self.roadTimePill.alpha = 1;
            self.roadDistPill.alpha = 1;
        }];
    }
    
    // Smooth position timer 20 fps per navigazione fluida
    _isInterpolating = NO;
    _interpStartTime = 0;
    [self startSmoothTimer];
    
    // Avvicina camera SUBITO con le impostazioni salvate dall'utente
    if (_lastLocation) {
        [self appLog:@"   GPS: %f,%f course=%f", _lastLocation.coordinate.latitude, _lastLocation.coordinate.longitude, _lastLocation.course];
        // Converte altitude in zoom: più bassa = zoom più alto
        double alt = MAX(self.cameraAltitude, 10.0);
        double zoom = MAX(10.0, MIN(19.0, 18.0 - log2(alt / 100.0)));
        double pitch = self.cameraPitch > 0 ? self.cameraPitch : 60.0;
        double bearing = _lastLocation.course >= 0 ? _lastLocation.course : _cameraHeading;
        NSString *flyJs = [NSString stringWithFormat:
            @"animateCamera(%f, %f, %f, %f, %f);",
            _lastLocation.coordinate.latitude,
            _lastLocation.coordinate.longitude,
            zoom, bearing, pitch];
        [self.webView evaluateJavaScript:flyJs completionHandler:nil];
    } else {
        [self appLog:@"   ⚠️ GPS NON disponibile (no animateCamera)"];
    }
    
    [self appLog:@"   applyCameraSettings..."];
    [self applyCameraSettings];
    [self appLog:@"   startNavigation() JS..."];
    [self.webView evaluateJavaScript:@"startNavigation()" completionHandler:^(id res, NSError *err) {
        if (err) [self appLog:@"   ⚠️ startNavigation JS error: %@", err.localizedDescription];
        else [self appLog:@"   ✅ startNavigation() JS ok"];
    }];
    
    [_navTimer invalidate];
    _navTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(navTick) userInfo:nil repeats:YES];
    [self appLog:@"   navTimer partito"];
    [self updateNavUI];
    [self appLog:@"✅ Navigazione avviata completa"];
}

// v3.549: Panel info bus — animazioni show/hide (sotto safe area)
- (void)showBusInfoPanel {
    if (!_busInfoPanel || !_busInfoPanel.hidden) return;
    CGFloat safeTop = self.view.safeAreaInsets.top;
    CGFloat panelH = _busInfoPanel.frame.size.height;
    _busInfoPanel.frame = CGRectMake(0, safeTop - panelH, self.view.bounds.size.width, panelH);
    _busInfoPanel.hidden = NO;
    _busSpeedPill.hidden = NO;
    [UIView animateWithDuration:0.35 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
        _busInfoPanel.frame = CGRectMake(0, safeTop, self.view.bounds.size.width, panelH);
    } completion:nil];
}

- (void)hideBusInfoPanel {
    if (!_busInfoPanel || _busInfoPanel.hidden) return;
    CGFloat safeTop = self.view.safeAreaInsets.top;
    CGFloat panelH = _busInfoPanel.frame.size.height;
    _busSpeedPill.hidden = YES;
    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
        _busInfoPanel.frame = CGRectMake(0, safeTop - panelH, self.view.bounds.size.width, panelH);
    } completion:^(BOOL fin) {
        _busInfoPanel.hidden = YES;
    }];
}

// v3.555: Aggiorna metriche panel info bus — OGNI chiamata, nessun throttle
- (void)updateBusInfoPanel {
    if (!_busTripStops || _busTripStops.count == 0) return;
    if (!_hasBusSimPos) return;
    
    double curLat = _busSimLat, curLon = _busSimLon;
    
    // Trova la prossima fermata: prima con distanza point-to-point > 20m dalla corrente
    // (abbiamo superato la fermata se distanza < 20m)
    NSInteger nextIdx = _busCurrentStopIdx;
    for (NSInteger i = _busCurrentStopIdx; i < (NSInteger)_busTripStops.count; i++) {
        NSDictionary *s = _busTripStops[i];
        double sLat = [s[@"lat"] doubleValue], sLon = [s[@"lon"] doubleValue];
        double d = [self haversineMeters:curLat lon1:curLon lat2:sLat lon2:sLon];
        if (d > 20.0 || i == (NSInteger)_busTripStops.count - 1) {
            nextIdx = i;
            break;
        }
        // Altrimenti questa fermata è già stata superata, passa alla successiva
    }
    if (nextIdx >= (NSInteger)_busTripStops.count) nextIdx = _busTripStops.count - 1;
    _busCurrentStopIdx = nextIdx;
    
    NSDictionary *nextStop = _busTripStops[_busCurrentStopIdx];
    NSString *stopName = nextStop[@"name"] ?: @"—";
    double stopLat = [nextStop[@"lat"] doubleValue], stopLon = [nextStop[@"lon"] doubleValue];
    double distToStop;
    if (_busSimCoords && _busSimCoords.count > 1) {
        distToStop = [self routeDistanceAlongSimCoords:stopLat lon:stopLon];
    } else {
        distToStop = [self haversineMeters:curLat lon1:curLon lat2:stopLat lon2:stopLon];
    }
    // Arrotonda GIÙ a multipli di 10m
    distToStop = floor(distToStop / 10.0) * 10.0;
    if (distToStop < 10.0) distToStop = 0.0;
    NSString *arrivalStr = nextStop[@"arrival"] ?: @"";
    
    // Distanza ultima fermata
    NSDictionary *lastStop = _busTripStops.lastObject;
    double lastLat = [lastStop[@"lat"] doubleValue], lastLon = [lastStop[@"lon"] doubleValue];
    double distRemaining;
    if (_busSimCoords && _busSimCoords.count > 1) {
        distRemaining = [self routeDistanceAlongSimCoords:lastLat lon:lastLon];
    } else {
        distRemaining = [self haversineMeters:curLat lon1:curLon lat2:lastLat lon2:lastLon];
    }
    
    // Anticipo/ritardo: orario schedulato vs tempo attuale
    NSInteger anticipoSec = 0, ritardoSec = 0;
    if (arrivalStr.length >= 5) {
        NSArray *parts = [arrivalStr componentsSeparatedByString:@":"];
        if (parts.count >= 2) {
            int schedH = [parts[0] intValue], schedM = [parts[1] intValue];
            NSCalendar *cal = [NSCalendar currentCalendar];
            NSDateComponents *nowComp = [cal components:NSCalendarUnitHour | NSCalendarUnitMinute fromDate:[NSDate date]];
            int nowMins = (int)(nowComp.hour * 60 + nowComp.minute);
            int schedMins = schedH * 60 + schedM;
            int diffMin = schedMins - nowMins;
            if (diffMin > 0) anticipoSec = diffMin * 60;
            else if (diffMin < 0) ritardoSec = -diffMin * 60;
        }
    }
    
    NSString *arrivoStr;
    if (distToStop >= 1000) arrivoStr = [NSString stringWithFormat:@"%.1fkm", distToStop/1000.0];
    else if (distToStop >= 10) arrivoStr = [NSString stringWithFormat:@"%.0fm", distToStop];
    else arrivoStr = @"Arr.";
    
    NSString *anticipoStr = anticipoSec >= 60 ? [NSString stringWithFormat:@"%ldmin", (long)(anticipoSec/60)] : @"0min";
    NSString *ritardoStr = ritardoSec >= 60 ? [NSString stringWithFormat:@"%ldmin", (long)(ritardoSec/60)] : @"0min";
    
    NSString *distanzaStr;
    if (distRemaining >= 1000) distanzaStr = [NSString stringWithFormat:@"%.1fkm", distRemaining/1000.0];
    else distanzaStr = [NSString stringWithFormat:@"%.0fm", distRemaining];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        _busNextStopLabel.text = @"Prossima fermata";
        _busStopNameLabel.text = stopName;
        
        // Attributed strings: numero grande, unità 20% piccola
        CGFloat numSize = _busArrivalNumber.font.pointSize;
        CGFloat unitSize = numSize * 0.40;
        NSAttributedString *(^makeAttr)(NSString *, CGFloat, CGFloat) = ^(NSString *str, CGFloat bigSize, CGFloat smallSize) {
            NSMutableString *numPart = [NSMutableString string];
            NSMutableString *unitPart = [NSMutableString string];
            BOOL inUnit = NO;
            for (NSInteger i = 0; i < (NSInteger)str.length; i++) {
                unichar c = [str characterAtIndex:i];
                if (!inUnit && (c >= '0' && c <= '9' || c == '.' || c == ',')) {
                    [numPart appendFormat:@"%C", c];
                } else {
                    inUnit = YES;
                    [unitPart appendFormat:@"%C", c];
                }
            }
            if (unitPart.length == 0) {
                return [[NSAttributedString alloc] initWithString:str attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:bigSize weight:UIFontWeightBlack]}];
            }
            NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] init];
            [attr appendAttributedString:[[NSAttributedString alloc] initWithString:numPart attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:bigSize weight:UIFontWeightBlack]}]];
            [attr appendAttributedString:[[NSAttributedString alloc] initWithString:unitPart attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:smallSize weight:UIFontWeightBold]}]];
            return (NSAttributedString *)attr;
        };
        
        _busArrivalNumber.attributedText = makeAttr(arrivoStr, numSize, unitSize);
        _busAnticipoNumber.attributedText = makeAttr(anticipoStr, numSize, unitSize);
        _busRitardoNumber.attributedText = makeAttr(ritardoStr, numSize, unitSize);
        _busDistanzaNumber.attributedText = makeAttr(distanzaStr, numSize, unitSize);
        
        // Colori dinamici: rosso se anticipo/ritardo > 0
        _busAnticipoNumber.textColor = (anticipoSec > 0) ? [UIColor redColor] : [UIColor blackColor];
        _busRitardoNumber.textColor = (ritardoSec > 0) ? [UIColor redColor] : [UIColor blackColor];
    });
}

// v3.557: Cicla velocità: 30 → 50 → 70 → 90 → 120 → 30 km/h
- (void)busSpeedButtonTapped {
    _busSpeedLevel = (_busSpeedLevel + 1) % 5;
    double kmh;
    NSString *title;
    switch (_busSpeedLevel) {
        case 0: kmh = 30.0; title = @"30"; break;
        case 1: kmh = 50.0; title = @"50"; break;
        case 2: kmh = 70.0; title = @"70"; break;
        case 3: kmh = 90.0; title = @"90"; break;
        case 4: kmh = 120.0; title = @"120"; break;
        default: kmh = 30.0; title = @"30"; break;
    }
    _busSimStepPerTick = kmh / 3.6 / 60.0;
    _busSpeedPillLabel.text = title;
    [self appLog:@"⚡ Velocità sim bus: %.0f km/h", kmh];
}

- (double)haversineMeters:(double)lat1 lon1:(double)lon1 lat2:(double)lat2 lon2:(double)lon2 {
    double R = 6371000;
    double dLat = (lat2 - lat1) * M_PI / 180.0;
    double dLon = (lon2 - lon1) * M_PI / 180.0;
    double a = sin(dLat/2) * sin(dLat/2) +
               cos(lat1 * M_PI / 180.0) * cos(lat2 * M_PI / 180.0) *
               sin(dLon/2) * sin(dLon/2);
    double c = 2 * atan2(sqrt(a), sqrt(1-a));
    return R * c;
}

/// Distanza LUNGO il percorso (_busSimCoords) dalla posizione bus corrente a una coordinata target
- (double)routeDistanceAlongSimCoords:(double)targetLat lon:(double)targetLon {
    if (!_busSimCoords || _busSimCoords.count < 2) return INFINITY;
    NSInteger count = (NSInteger)_busSimCoords.count;
    
    // Trova l'indice del punto più vicino al target
    NSInteger targetIdx = -1;
    double minDist = INFINITY;
    for (NSInteger i = _busSimIndex; i < count; i++) {
        NSDictionary *pt = _busSimCoords[i];
        double d = [self haversineMeters:targetLat lon1:targetLon
                                   lat2:[pt[@"lat"] doubleValue] lon2:[pt[@"lon"] doubleValue]];
        if (d < minDist) { minDist = d; targetIdx = i; }
    }
    if (targetIdx < 0 || targetIdx <= _busSimIndex) return minDist; // fallback haversine
    
    // Somma distanza dal bus al prossimo punto, poi segmenti fino al target
    double total = 0;
    // Primo segmento: posizione bus → prossimo punto della rotta
    if (_busSimIndex + 1 < count) {
        NSDictionary *nextPt = _busSimCoords[_busSimIndex + 1];
        total += [self haversineMeters:_busSimLat lon1:_busSimLon
                                 lat2:[nextPt[@"lat"] doubleValue] lon2:[nextPt[@"lon"] doubleValue]];
    }
    // Segmenti intermedi
    for (NSInteger i = _busSimIndex + 1; i < targetIdx && i + 1 < count; i++) {
        NSDictionary *a = _busSimCoords[i];
        NSDictionary *b = _busSimCoords[i + 1];
        total += [self haversineMeters:[a[@"lat"] doubleValue] lon1:[a[@"lon"] doubleValue]
                                 lat2:[b[@"lat"] doubleValue] lon2:[b[@"lon"] doubleValue]];
    }
    return total;
}

- (void)endNavigation {
    // v3.544: Nascondi panel info bus
    [self hideBusInfoPanel];
    _busTripStops = nil;
    _busCurrentStopIdx = 0;
    self.isNavigating = NO;
    _isSimulating = NO;
    [_navTimer invalidate]; _navTimer = nil;
    _isBusSimulating = NO;
    [_busSimTimer invalidate]; _busSimTimer = nil;
    _busSimCoords = nil;
    _busSimIndex = 0;
    // Ripristina layer user-location
    [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('user-location-layer','visibility','visible')}catch(e){};try{map.setLayoutProperty('user-location-ring','visibility','visible')}catch(e){}" completionHandler:nil];
    
    // Clear JS route, destination, bus route, e ferma rAF engine
    [self.webView evaluateJavaScript:@"endNavigation(); clearRoute(); clearDestination(); clearBusRouteLayers(); stopNavEngine();" completionHandler:^(id res, NSError *err) {
        if (err) [self appLog:@"endNavigation JS error: %@", err.localizedDescription];
    }];
    
    self.navBar.hidden = YES;
    self.stopNavButton.hidden = YES;
    // Resetta SUBITO label via (prima dell'animazione)
    self.roadStreetLabel.text = @"";
    // Torna alla vista mappa lontana
    [self.webView evaluateJavaScript:@"map.flyTo({zoom:12, pitch:0, bearing:0, duration:2000});" completionHandler:nil];
    // Nascondi pill via
    if (self.roadNameLabel && !self.roadNameLabel.hidden) {
        [UIView animateWithDuration:0.25 animations:^{
            self.roadNameLabel.alpha = 0;
            self.roadETAPill.alpha = 0;
            self.roadTimePill.alpha = 0;
            self.roadDistPill.alpha = 0;
        } completion:^(BOOL fin) {
            self.roadNameLabel.hidden = YES;
            self.roadETAPill.hidden = YES;
            self.roadTimePill.hidden = YES;
            self.roadDistPill.hidden = YES;
            self.roadStreetLabel.text = @"";
            self.roadETALabel.text = @"";
            self.roadTimeLabel.text = @"";
            self.roadDistLabel.text = @"";
        }];
    } else {
        self.roadETAPill.hidden = YES;
        self.roadTimePill.hidden = YES;
        self.roadDistPill.hidden = YES;
        self.roadETALabel.text = @"";
        self.roadTimeLabel.text = @"";
        self.roadDistLabel.text = @"";
    }
    
    [self hideGoButton];
    self.userTracking = NO;
    self.userTrackingWithHeading = NO;
    [self updateTrackingButton];
    _isInterpolating = NO;
    [self stopSmoothTimer];
}

/// Timer 2-secondi — ottiene stato navigazione da JS e aggiorna UI
- (void)navTick {
    if (!self.isNavigating || !_lastLocation) return;
    
    [self.webView evaluateJavaScript:@"getNavState()" completionHandler:^(id result, NSError *error) {
        if (error || !result) return;
        
        NSDictionary *state = nil;
        if ([result isKindOfClass:[NSString class]]) {
            NSData *data = [(NSString *)result dataUsingEncoding:NSUTF8StringEncoding];
            state = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        } else if ([result isKindOfClass:[NSDictionary class]]) {
            state = (NSDictionary *)result;
        }
        
        if (![state isKindOfClass:[NSDictionary class]]) return;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            NSString *instruction = state[@"instruction"];
            if (instruction) self.instructionLabel.text = instruction;
            
            id distVal = state[@"distanceRemaining"];
            if (distVal) self.distanceRemaining = [distVal doubleValue];
            
            id etaVal = state[@"etaSeconds"];
            if (etaVal) self.etaSeconds = [etaVal doubleValue];
            
            id speedVal = state[@"speedKmh"];
            if (speedVal) {
                double kmh = [speedVal doubleValue];
                // Keep GPS speed if available
                if (_currentSpeed < 0.1) _currentSpeed = kmh / 3.6;
            }
            
            [self updateNavUI];
            
            // Check for arrival
            NSNumber *arrived = state[@"arrived"];
            if ([arrived boolValue]) {
                [self announceArrival];
            }
        });
    }];
    
    // Ricalcolo se fuori rotta — via JS snapToRoute
    [self.webView evaluateJavaScript:[NSString stringWithFormat:@"snapToRoute(%f, %f)",
        _lastLocation.coordinate.latitude, _lastLocation.coordinate.longitude]
        completionHandler:^(id result, NSError *error) {
            if (error || !result) return;
            // result is the snapped coordinate or distance from route
            // If distance > threshold, recalculate
            if ([result isKindOfClass:[NSNumber class]]) {
                double dist = [result doubleValue];
                if (dist > 5.0 && !self->_isRecalculating) {
                    [self recalculateRoute];
                }
            }
        }];
    
    [self applyCameraSettings];
    [self applyAllSettings];
}

- (void)updateNavUI {
    self.distanceLabel.text = self.distanceRemaining >= 1000
        ? [NSString stringWithFormat:@"%.1f km rimanenti", self.distanceRemaining/1000.0]
        : [NSString stringWithFormat:@"%.0f m rimanenti", self.distanceRemaining];
    NSInteger mins = (NSInteger)(self.etaSeconds/60);
    self.etaLabel.text = mins >= 60
        ? [NSString stringWithFormat:@"🕐 %ldh %ldm", (long)(mins/60), (long)(mins%60)]
        : [NSString stringWithFormat:@"🕐 %ld min", (long)MAX(1,mins)];
    double kmh = _currentSpeed * 3.6;
    self.speedLabel.text = kmh > 2 ? [NSString stringWithFormat:@"%.0f", kmh] : @"--";
}

- (void)speakCurrentInstruction {
    if (!self.isNavigating) return;
    SettingsStore *st = [SettingsStore shared];
    if (!st.voiceGuidance) return;
    NSString *txt = self.instructionLabel.text;
    if (!txt || txt.length == 0) return;
    // Non ripetere la stessa istruzione
    if ([txt isEqualToString:_lastSpokenInstruction]) return;
    _lastSpokenInstruction = txt;
    txt = [txt stringByReplacingOccurrencesOfString:@"<[^>]+>" withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0,txt.length)];
    AVSpeechUtterance *u = [AVSpeechUtterance speechUtteranceWithString:txt];
    NSString *lang = st.voiceLanguage ?: @"it-IT";
    u.voice = [AVSpeechSynthesisVoice voiceWithLanguage:lang];
    u.rate = 0.5; u.volume = st.voiceVolume;
    [self.speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
    [self.speechSynthesizer speakUtterance:u];
}

- (void)announceArrival {
    self.isNavigating = NO; [_navTimer invalidate]; _navTimer = nil;
    SettingsStore *st = [SettingsStore shared];
    [self.webView evaluateJavaScript:@"endNavigation()" completionHandler:nil];
    if (st.voiceGuidance) {
        AVSpeechUtterance *u = [AVSpeechUtterance speechUtteranceWithString:@"Sei arrivato a destinazione!"];
        u.voice = [AVSpeechSynthesisVoice voiceWithLanguage:(st.voiceLanguage ?: @"it-IT")];
        u.rate = 0.45; u.volume = st.voiceVolume;
        [self.speechSynthesizer speakUtterance:u];
    }
    [self showAlert:@"Arrivato! 🎉" message:@"Sei arrivato a destinazione."];
    [self endNavigation];
}

- (void)generateArrow3D {
    // Freccia PNG da v6.25, ridotta 24×31pt
    NSString *path = [[NSBundle mainBundle] pathForResource:@"arrow" ofType:@"png"];
    if (path) {
        self.arrow3D = [UIImage imageWithContentsOfFile:path];
    }
    // Fallback di sicurezza
    if (!self.arrow3D) {
        CGFloat w = 24, h = 31;
        CGSize sz = CGSizeMake(w, h);
        UIGraphicsImageRenderer *r = [[UIGraphicsImageRenderer alloc] initWithSize:sz];
        self.arrow3D = [r imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
            UIBezierPath *arrow = [UIBezierPath bezierPath];
            [arrow moveToPoint:CGPointMake(w/2, 0)];
            [arrow addLineToPoint:CGPointMake(w, h*0.38)];
            [arrow addLineToPoint:CGPointMake(w*0.62, h*0.38)];
            [arrow addLineToPoint:CGPointMake(w*0.62, h)];
            [arrow addLineToPoint:CGPointMake(w*0.38, h)];
            [arrow addLineToPoint:CGPointMake(w*0.38, h*0.38)];
            [arrow addLineToPoint:CGPointMake(0, h*0.38)];
            [arrow closePath];
            [[UIColor colorWithRed:0.0 green:0.35 blue:0.9 alpha:1.0] setFill];
            [arrow fill];
        }];
    }
}

#pragma mark - Buttons

- (void)trackingButtonTapped {
    if (!self.userTracking) {
        // Attiva tracking e centra SUBITO sulla posizione
        self.userTracking = YES;
        self.userTrackingWithHeading = NO;
        // Forza visibilità layer posizione + freccia + centra mappa
        NSString *js = @"forceUserLocationVisible();initNavEngine();try{map.setLayoutProperty('nav-arrow-layer','visibility','visible')}catch(e){};centerOnUser()";
        [self.webView evaluateJavaScript:js completionHandler:nil];
        if (_lastLocation) {
            [self pushPositionToJSWithLat:_lastLocation.coordinate.latitude lon:_lastLocation.coordinate.longitude course:_currentCourse];
        }
    } else if (!self.userTrackingWithHeading) {
        self.userTrackingWithHeading = YES;
    } else {
        self.userTracking = NO;
        self.userTrackingWithHeading = NO;
    }
    [self updateTrackingButton];
}

- (void)updateTrackingButton {
    NSString *icon;
    if (self.userTrackingWithHeading) {
        icon = @"location.north.line.fill";
    } else if (self.userTracking) {
        icon = @"location.fill";
    } else {
        icon = @"location";
    }
    [self.trackingButton setImage:[UIImage systemImageNamed:icon] forState:UIControlStateNormal];
}

#pragma mark - MapView Delegate (removed — all JS now)

#pragma mark - Location

/// Restituisce la coordinata più vicina sul percorso (JS delegated)
- (CLLocationCoordinate2D)closestPointOnRouteToCoordinate:(CLLocationCoordinate2D)coord {
    // JS snapToRoute returns snapped coordinate
    __block CLLocationCoordinate2D result = coord;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    NSString *js = [NSString stringWithFormat:@"snapToRoute(%f, %f)", coord.latitude, coord.longitude];
    [self.webView evaluateJavaScript:js completionHandler:^(id res, NSError *err) {
        if (!err && res && [res isKindOfClass:[NSString class]]) {
            NSData *data = [(NSString *)res dataUsingEncoding:NSUTF8StringEncoding];
            NSDictionary *pt = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            if ([pt isKindOfClass:[NSDictionary class]]) {
                result = CLLocationCoordinate2DMake([pt[@"lat"] doubleValue], [pt[@"lon"] doubleValue]);
            }
        }
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)));
    return result;
}

/// Restituisce la coordinata N metri più avanti sul percorso (JS delegated)
- (CLLocationCoordinate2D)coordinateOnRouteAheadOf:(CLLocationCoordinate2D)currentCoord meters:(CLLocationDistance)lookAhead {
    __block CLLocationCoordinate2D result = currentCoord;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    NSString *js = [NSString stringWithFormat:@"coordinateAheadOnRoute(%f, %f, %f)",
                    currentCoord.latitude, currentCoord.longitude, lookAhead];
    [self.webView evaluateJavaScript:js completionHandler:^(id res, NSError *err) {
        if (!err && res && [res isKindOfClass:[NSString class]]) {
            NSData *data = [(NSString *)res dataUsingEncoding:NSUTF8StringEncoding];
            NSDictionary *pt = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            if ([pt isKindOfClass:[NSDictionary class]]) {
                result = CLLocationCoordinate2DMake([pt[@"lat"] doubleValue], [pt[@"lon"] doubleValue]);
            }
        }
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)));
    return result;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
    // Durante la simulazione, NON sovrascrivere la posizione simulata con GPS reale!
    if (_isSimulating) {
        return;
    }
    _prevLocation = _lastLocation;
    _lastLocation = locations.lastObject;
    _currentSpeed = MAX(0, _lastLocation.speed);
    _currentCourse = _lastLocation.course;
    
    // Snap GPS alla strada via JS
    CLLocationCoordinate2D snapped = _lastLocation.coordinate;
    if (self.isNavigating) {
        __block CLLocationCoordinate2D jsSnapped = snapped;
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        NSString *js = [NSString stringWithFormat:@"snapToRoute(%f, %f)", _lastLocation.coordinate.latitude, _lastLocation.coordinate.longitude];
        [self.webView evaluateJavaScript:js completionHandler:^(id res, NSError *err) {
            if (!err && res && [res isKindOfClass:[NSString class]]) {
                NSData *data = [(NSString *)res dataUsingEncoding:NSUTF8StringEncoding];
                NSDictionary *pt = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
                if ([pt isKindOfClass:[NSDictionary class]]) {
                    jsSnapped = CLLocationCoordinate2DMake([pt[@"lat"] doubleValue], [pt[@"lon"] doubleValue]);
                }
            }
            dispatch_semaphore_signal(sem);
        }];
        dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)));
        snapped = jsSnapped;
    }
    
    if (!_hasSnapped) {
        // Primo fix: inizializza entrambi allo stesso punto
        _prevSnapped = snapped;
        _lastSnapped = snapped;
    } else {
        _prevSnapped = _lastSnapped;
        _lastSnapped = snapped;
    }
    _hasSnapped = YES;
    _interpStart = [[NSDate date] timeIntervalSince1970];
    
    // Posizione fluida: se nav reale, interpola tra punti GPS a 20 fps
    if (self.isNavigating && !_isSimulating) {
        _interpFrom = _isInterpolating ? _interpTo : snapped;
        _interpTo = snapped;
        _interpStartTime = [[NSDate date] timeIntervalSince1970];
        _isInterpolating = YES;
        // smoothTick si occupa di inviare posizione via pushPosition
    } else {
        // Non in navigazione: aggiorna subito via pushPosition
        [self pushPositionToJSWithLat:snapped.latitude lon:snapped.longitude course:_currentCourse];
    }
    
    // Camera: in navigazione forzata sempre, altrimenti solo in movimento
    if (self.userTracking && _lastLocation) {
        if (self.isNavigating || (self.userTrackingWithHeading && _lastLocation.course >= 0 && _currentSpeed > 2.0)) {
            [self applyCameraSettings];
        } else if (!self.userTrackingWithHeading) {
            NSString *js = [NSString stringWithFormat:@"centerOnUser()"];
            [self.webView evaluateJavaScript:js completionHandler:nil];
        }
    }
    [self fetchBusStopsIfNeeded];
}

#pragma mark - Smooth Position (20 fps)

- (void)startSmoothTimer {
    [_smoothTimer invalidate];
    _smoothTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:@selector(smoothTick) userInfo:nil repeats:YES];
}

- (void)stopSmoothTimer {
    [_smoothTimer invalidate];
    _smoothTimer = nil;
}

- (void)smoothTick {
    // Se simulazione attiva, simTick già gestisce posizione
    if (_isSimulating) return;
    if (!self.isNavigating) return;
    
    // Interpolazione GPS reale
    if (_isInterpolating) {
        NSTimeInterval elapsed = [[NSDate date] timeIntervalSince1970] - _interpStartTime;
        double t = MIN(elapsed / 0.5, 1.0);
        t = t * t * (3.0 - 2.0 * t);
        
        double lat = _interpFrom.latitude + (_interpTo.latitude - _interpFrom.latitude) * t;
        double lon = _interpFrom.longitude + (_interpTo.longitude - _interpFrom.longitude) * t;
        
        double course = _currentCourse;
        if (course < 0 && t > 0.01) {
            course = atan2(_interpTo.latitude - _interpFrom.latitude, _interpTo.longitude - _interpFrom.longitude) * 180.0 / M_PI;
            if (course < 0) course += 360;
        }
        
        static int _gpsFrame = 0;
        _gpsFrame++;
        if (_gpsFrame >= 12) {
            _gpsFrame = 0;
            [self pushPositionToJSWithLat:lat lon:lon course:course];
        }
        
        if (t >= 1.0) {
            _isInterpolating = NO;
        }
    }
    
    if (self.isNavigating && _lastLocation) {
        [self applyCameraSettings];
    }
}

#pragma mark - NavEngine Push (5fps → JS rAF)

- (void)pushPositionToJS {
    // Usa _hasSimPos durante simulazione, altrimenti _lastLocation
    double lat, lon, course;
    if (_hasSimPos) {
        lat = _simLat;
        lon = _simLon;
        course = _simCourse;
    } else if (_lastLocation) {
        lat = _lastLocation.coordinate.latitude;
        lon = _lastLocation.coordinate.longitude;
        course = _lastLocation.course >= 0 ? _lastLocation.course : _currentCourse;
    } else {
        return;
    }
    [self pushPositionToJSWithLat:lat lon:lon course:course];
}

- (void)pushPositionToJSWithLat:(double)lat lon:(double)lon course:(double)course {
    double ts = [[NSDate date] timeIntervalSince1970] * 1000.0;
    NSString *js = [NSString stringWithFormat:@"pushPosition(%f,%f,%f,%f)", lat, lon, course, ts];
    [self.webView evaluateJavaScript:js completionHandler:nil];
}

- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading {
    if (newHeading.headingAccuracy < 0) return;
    _currentHeading = newHeading.trueHeading;
    // Rotazione freccia + camera gestita dal JS rAF engine
    // Ruota bussola custom (native)
    [UIView animateWithDuration:0.25 animations:^{
        self.compassButton.transform = CGAffineTransformMakeRotation(-newHeading.trueHeading * M_PI / 180.0);
    }];
}

- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
    if (manager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse ||
        manager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways) {
        [self.locationManager startUpdatingLocation];
    }
}

- (void)settingsDidClose {
    self.busForceHidden = NO;
    if (_busWasVisible && self.busVC) {
        self.busVC.view.hidden = NO;
        _busWasVisible = NO;
    }
    [self applyAllSettings];
}

- (void)applyNavigationSettings:(NSDictionary *)req {
    SettingsStore *st = [SettingsStore shared];
    // Note: navigation settings are handled by JS OSRM routing
    // Alt routes, transport mode etc are configured in JS
}

- (void)mapSettingsChanged {
    SettingsStore *st = [SettingsStore shared];
    [self appLog:@"▶ SETTINGS: night=%d dark=%d type=%ld bright=%.2f", st.nightMode, st.darkTheme, (long)st.mapType, st.mapBrightness];
    [self applyAllSettings];
}

- (void)refreshBuildingsVisibilityIfNeeded {
    // Buildings handled in applyAllSettings with correct MapLibre layer names
    // Map type also handled in applyAllSettings — no duplicate call needed
}

- (void)applyAllSettings {
    SettingsStore *st = [SettingsStore shared];
    
    // === MAPPA ===
    // Dark mode: solo modalità notturna (non tema scuro UI)
    if (st.nightMode) {
        [self appLog:@"→ setMapType(1) darkMode"];
        [self.webView evaluateJavaScript:@"setMapType(1)" completionHandler:^(id r, NSError *err) {
            if (err) [self appLog:@"❌ setMapType(1) ERR: %@", err.localizedDescription];
            else [self appLog:@"✓ setMapType(1) fatto"];
        }];
    } else {
        long mt = (long)st.mapType;
        [self appLog:@"→ setMapType(%ld) giorno", mt];
        [self.webView evaluateJavaScript:[NSString stringWithFormat:@"setMapType(%ld)", mt] completionHandler:^(id r, NSError *err) {
            if (err) [self appLog:@"❌ setMapType(%ld) ERR: %@", mt, err.localizedDescription];
            else [self appLog:@"✓ setMapType(%ld) fatto", mt];
        }];
    }
    
    // Mappa luminosita - from SettingsStore
    [self appLog:@"→ setMapBrightness(%.2f)", st.mapBrightness];
    [self.webView evaluateJavaScript:[NSString stringWithFormat:@"setMapBrightness(%f)", st.mapBrightness] completionHandler:^(id r, NSError *err) {
        if (err) [self appLog:@"❌ setMapBrightness ERR: %@", err.localizedDescription];
        else [self appLog:@"✓ setMapBrightness fatto"];
    }];
    
    // POI Labels — toggle via MapLibre
    if ([st.poiLabels isEqualToString:@"Nessuna"]) {
        [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('poi_r20','visibility','none');}catch(e){} try{map.setLayoutProperty('poi_r7','visibility','none');}catch(e){} try{map.setLayoutProperty('poi_transit','visibility','none');}catch(e){}" completionHandler:nil];
    } else {
        [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('poi_r20','visibility','visible');}catch(e){} try{map.setLayoutProperty('poi_r7','visibility','visible');}catch(e){} try{map.setLayoutProperty('poi_transit','visibility','visible');}catch(e){}" completionHandler:nil];
    }
    
    [self refreshBuildingsVisibilityIfNeeded];
    
    // Orientamento mappa
    self.mapOrientationLocked = [st.mapOrientation isEqualToString:@"Nord in alto"];
    
    // === VISUALE ===
    BOOL dark = st.nightMode || st.darkTheme;
    self.view.backgroundColor = dark ? [UIColor blackColor] : [UIColor colorWithWhite:0.15 alpha:1.0];
    
    // Bussola nativa MapLibre JS (bussola custom nascosta)
    self.compassButton.hidden = YES;
    [self applyMenuOpacity];
    
    // Mostra edifici 3D (MapLibre — nascondi/mostra layer building)
    if (st.show3DBuildings) {
        [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('building','visibility','visible');}catch(e){} try{map.setLayoutProperty('building-3d','visibility','visible');}catch(e){}" completionHandler:nil];
    } else {
        [self.webView evaluateJavaScript:@"try{map.setLayoutProperty('building','visibility','none');}catch(e){} try{map.setLayoutProperty('building-3d','visibility','none');}catch(e){}" completionHandler:nil];
    }
    
    // Dimensione testo
    CGFloat fontSize;
    if ([st.textSize isEqualToString:@"Grande"]) fontSize = 20;
    else if ([st.textSize isEqualToString:@"Molto grande"]) fontSize = 24;
    else fontSize = 18;
    self.instructionLabel.font = [UIFont boldSystemFontOfSize:fontSize];
    self.distanceLabel.font = [UIFont systemFontOfSize:fontSize-4];
    self.etaLabel.font = [UIFont boldSystemFontOfSize:fontSize-1];
    
    self.speedLabel.hidden = !st.showSpeedometer;
    self.etaLabel.hidden = !st.showETA;
    
    // Scala (MapLibre nativa)
    if (st.showScale) {
        [self.webView evaluateJavaScript:@"try{toggleScale(true)}catch(e){}" completionHandler:nil];
    } else {
        [self.webView evaluateJavaScript:@"try{toggleScale(false)}catch(e){}" completionHandler:nil];
    }
    
    if (st.reducedAnimations) {
        [UIView setAnimationsEnabled:NO];
    } else {
        [UIView setAnimationsEnabled:YES];
    }
    
    // Traffico - non disponibile in MapLibre gratuito
    // showTraffic toggle esiste ma non ha effetto (nessun dato traffico gratuito)
    
    // === VOCE ===
    // voiceGuidance e voiceVolume applicati in speakCurrentInstruction/announceArrival
    
    // === OPACITÀ MENU ===
    [self applyMenuOpacity];
    
    // === FRECCIA ===
    [self applyArrowTransform];
    
    // === FERMATE AUTOBUS ===
    [self applyBusStopsVisibility];
    [self fetchBusStopsIfNeeded];
    
    // === CAMERA ===
    if (_lastLocation) {
        [self applyCameraSettings];
    }
}

- (void)showAlert:(NSString *)title message:(NSString *)msg {
    UIAlertController *ac = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
    [ac addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
    [self presentViewController:ac animated:YES completion:nil];
    // Auto-dismiss dopo 2 secondi
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [ac dismissViewControllerAnimated:YES completion:nil];
    });
}

#pragma mark - Bus Stops

- (void)applyBusStopsVisibility {
    BOOL show = [SettingsStore shared].showBusStops;
    // Bus stop visibility is managed by JS markers
    // Pass visibility state to JS
    NSString *js = show ? @"busStopsSetVisible(true)" : @"busStopsSetVisible(false)";
    [self.webView evaluateJavaScript:js completionHandler:nil];
}

- (NSString *)tileKeyForCoordinate:(CLLocationCoordinate2D)coord {
    double lat = round(coord.latitude * 20.0) / 20.0;   // ~5.5km tile
    double lon = round(coord.longitude * 20.0) / 20.0;
    return [NSString stringWithFormat:@"%.2f,%.2f", lat, lon];
}

- (void)fetchBusStopsIfNeeded {
    if (![SettingsStore shared].showBusStops) return;
    if (self.pendingBusFetch) return;
    
    // Approximate zoom-based check — skip if too zoomed out
    __block BOOL shouldFetch = NO;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    [self.webView evaluateJavaScript:@"getMapZoom()" completionHandler:^(id res, NSError *err) {
        if (!err && res && [res isKindOfClass:[NSNumber class]]) {
            double zoom = [res doubleValue];
            shouldFetch = (zoom >= 10.0); // zoom 10+ is city level
        }
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)));
    if (!shouldFetch) return;
    
    // Get map bounds from JS
    __block double south = 0, north = 0, west = 0, east = 0;
    sem = dispatch_semaphore_create(0);
    [self.webView evaluateJavaScript:@"getMapBounds()" completionHandler:^(id res, NSError *err) {
        if (!err && res && [res isKindOfClass:[NSString class]]) {
            NSData *data = [(NSString *)res dataUsingEncoding:NSUTF8StringEncoding];
            NSDictionary *bounds = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            if ([bounds isKindOfClass:[NSDictionary class]]) {
                south = [bounds[@"south"] doubleValue];
                north = [bounds[@"north"] doubleValue];
                west = [bounds[@"west"] doubleValue];
                east = [bounds[@"east"] doubleValue];
            }
        }
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)));
    
    if (south == 0 && north == 0) return; // couldn't get bounds
    
    // Only fetch if in Italy (~35°N to 48°N, 6°E to 19°E)
    double centerLat = (south + north) / 2.0;
    double centerLon = (west + east) / 2.0;
    if (centerLat < 35.0 || centerLat > 48.0 ||
        centerLon < 6.0 || centerLon > 19.0) return;
    
    CLLocationCoordinate2D center = CLLocationCoordinate2DMake(centerLat, centerLon);
    NSString *tile = [self tileKeyForCoordinate:center];
    if ([self.busStopsFetched containsObject:tile]) return;
    
    [self.busStopsFetched addObject:tile];
    self.pendingBusFetch = YES;
    
    // Overpass API query
    NSString *query = [NSString stringWithFormat:
        @"[out:json][timeout:8];"
        @"(node[\"highway\"=\"bus_stop\"](%.6f,%.6f,%.6f,%.6f);"
        @"node[\"public_transport\"=\"stop_position\"][\"bus\"=\"yes\"](%.6f,%.6f,%.6f,%.6f););"
        @"out center 200;",
        south, west, north, east,
        south, west, north, east];
    
    NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:
        [NSURL URLWithString:@"https://overpass-api.de/api/interpreter"]];
    req.HTTPMethod = @"POST";
    req.HTTPBody = [query dataUsingEncoding:NSUTF8StringEncoding];
    req.timeoutInterval = 10;
    
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.pendingBusFetch = NO;
        });
        if (err || !data) return;
        [self processBusStopsJSON:data];
    }] resume];
}

- (void)processBusStopsJSON:(NSData *)data {
    NSError *err;
    NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
    if (err || ![json isKindOfClass:[NSDictionary class]]) return;
    
    NSArray *elements = json[@"elements"];
    if (![elements isKindOfClass:[NSArray class]]) return;
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // Convert elements to JSON string and pass to JS
        NSError *jsonErr;
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:elements options:0 error:&jsonErr];
        if (!jsonData) return;
        NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
        
        // Escape for JS
        jsonStr = [jsonStr stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
        jsonStr = [jsonStr stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
        jsonStr = [jsonStr stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
        
        NSString *js = [NSString stringWithFormat:@"busStopsFromJSON('%@')", jsonStr];
        [self.webView evaluateJavaScript:js completionHandler:^(id res, NSError *err) {
            if (err) {
                [self appLog:@"busStopsFromJSON error: %@", err.localizedDescription];
            } else {
                NSInteger count = [res integerValue];
                if (count > 0) {
                    [self appLog:@"🚏 Aggiunte %ld fermate autobus", (long)count];
                }
            }
        }];
    });
}

#pragma mark - Bus Toggle

- (void)toggleBusView {
    if (self.busVC.view.hidden) {
        [self.busVC showBus];
    } else {
        [self.busVC hideBus];
    }
}

#pragma mark - WKScriptMessageHandler

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"speak"]) {
        NSString *text = message.body;
        if (![text isKindOfClass:[NSString class]]) return;
        SettingsStore *st = [SettingsStore shared];
        if (!st.voiceGuidance) return;
        AVSpeechUtterance *u = [AVSpeechUtterance speechUtteranceWithString:text];
        NSString *lang = st.voiceLanguage ?: @"it-IT";
        u.voice = [AVSpeechSynthesisVoice voiceWithLanguage:lang];
        u.rate = 0.5;
        u.volume = st.voiceVolume;
        [self.speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
        [self.speechSynthesizer speakUtterance:u];
    } else if ([message.name isEqualToString:@"navigationEnd"]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self announceArrival];
        });
    } else if ([message.name isEqualToString:@"navUpdate"]) {
        NSString *jsonStr = message.body;
        if (![jsonStr isKindOfClass:[NSString class]]) return;
        NSData *data = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
        NSDictionary *state = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        if (![state isKindOfClass:[NSDictionary class]]) return;
        dispatch_async(dispatch_get_main_queue(), ^{
            NSString *instruction = state[@"instruction"];
            if (instruction) self.instructionLabel.text = instruction;
            NSString *streetName = state[@"streetName"];
            // Non toccare roadNameLabel durante simulazione bus (gestita da busSimTick)
            if (streetName && streetName.length > 0 && !_isBusSimulating) {
                // Animazione nome via: scende dall'alto quando cambia (solo sul label, non sulla pill)
                BOOL shouldAnimate = !self.roadNameLabel.hidden && ![self.roadStreetLabel.text isEqualToString:streetName];
                self.roadNameLabel.hidden = NO;
                self.roadStreetLabel.text = streetName;
                if (shouldAnimate) {
                    CATransition *slide = [CATransition animation];
                    slide.duration = 0.35;
                    slide.type = kCATransitionMoveIn;
                    slide.subtype = kCATransitionFromTop;
                    slide.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
                    [self.roadStreetLabel.layer addAnimation:slide forKey:@"streetChange"];
                }
            } else if (self.isNavigating && !_isBusSimulating) {
                // Fallback: mostra instruction come nome via (utile per bus nav)
                NSString *fallback = instruction ?: @"";
                if (fallback.length > 0) {
                    self.roadNameLabel.hidden = NO;
                    self.roadStreetLabel.text = fallback;
                }
            }
            // ETA: ora arrivo + "arrivo" sotto
            id etaVal = state[@"eta"];
            if (etaVal) {
                self.etaSeconds = [etaVal doubleValue];
                NSDate *arrival = [NSDate dateWithTimeIntervalSinceNow:self.etaSeconds];
                NSDateFormatter *df = [[NSDateFormatter alloc] init];
                df.dateFormat = @"HH:mm";
                NSString *etaStr = [df stringFromDate:arrival];
                NSMutableAttributedString *etaAttr = [[NSMutableAttributedString alloc] init];
                [etaAttr appendAttributedString:[[NSAttributedString alloc] initWithString:etaStr attributes:@{
                    NSFontAttributeName: [UIFont boldSystemFontOfSize:28], NSForegroundColorAttributeName: [UIColor whiteColor]
                }]];
                [etaAttr appendAttributedString:[[NSAttributedString alloc] initWithString:@"\narrivo" attributes:@{
                    NSFontAttributeName: [UIFont systemFontOfSize:18], NSForegroundColorAttributeName: [UIColor colorWithWhite:0.80 alpha:1.0]
                }]];
                self.roadETALabel.attributedText = etaAttr;
            }
            // Distanza rimanente + "distanza" sotto
            id distVal = state[@"distance"];
            if (distVal) {
                self.distanceRemaining = [distVal doubleValue];
                NSString *distStr = self.distanceRemaining >= 1000
                    ? [NSString stringWithFormat:@"%.1f km", self.distanceRemaining / 1000.0]
                    : [NSString stringWithFormat:@"%.0f m", self.distanceRemaining];
                NSMutableAttributedString *distAttr = [[NSMutableAttributedString alloc] init];
                [distAttr appendAttributedString:[[NSAttributedString alloc] initWithString:distStr attributes:@{
                    NSFontAttributeName: [UIFont boldSystemFontOfSize:28], NSForegroundColorAttributeName: [UIColor whiteColor]
                }]];
                [distAttr appendAttributedString:[[NSAttributedString alloc] initWithString:@"\ndistanza" attributes:@{
                    NSFontAttributeName: [UIFont systemFontOfSize:18], NSForegroundColorAttributeName: [UIColor colorWithWhite:0.80 alpha:1.0]
                }]];
                self.roadDistLabel.attributedText = distAttr;
            }
            // Tempo percorrenza + "tempo" sotto
            NSInteger mins = (NSInteger)(self.etaSeconds / 60);
            NSString *timeStr = mins >= 60
                ? [NSString stringWithFormat:@"%ldh %ldm", (long)(mins / 60), (long)(mins % 60)]
                : [NSString stringWithFormat:@"%ld min", (long)MAX(1, mins)];
            NSMutableAttributedString *timeAttr = [[NSMutableAttributedString alloc] init];
            [timeAttr appendAttributedString:[[NSAttributedString alloc] initWithString:timeStr attributes:@{
                NSFontAttributeName: [UIFont boldSystemFontOfSize:28], NSForegroundColorAttributeName: [UIColor whiteColor]
            }]];
            [timeAttr appendAttributedString:[[NSAttributedString alloc] initWithString:@"\ntempo" attributes:@{
                NSFontAttributeName: [UIFont systemFontOfSize:18], NSForegroundColorAttributeName: [UIColor colorWithWhite:0.80 alpha:1.0]
            }]];
            self.roadTimeLabel.attributedText = timeAttr;
            id speedVal = state[@"speed"];
            // Only use JS speed as fallback if GPS hasn't provided one
            if (speedVal && _currentSpeed < 0.1) {
                double kmh = [speedVal doubleValue];
                _currentSpeed = kmh / 3.6;
            }
            [self updateNavUI];
            NSNumber *arrived = state[@"arrived"];
            if ([arrived boolValue]) {
                [self announceArrival];
            }
        });
    } else if ([message.name isEqualToString:@"error"]) {
        NSString *desc = message.body;
        if (![desc isKindOfClass:[NSString class]]) desc = @"Errore sconosciuto";
        [self appLog:@"⚠️ JS Error: %@", desc];
    } else if ([message.name isEqualToString:@"searchResults"]) {
        // Results from nativeSearchOSM (car mode: plain array) or nativeSearchBusLineByTime (bus mode: {seq, results})
        int msgSeq = -1;
        NSArray *results = nil;
        
        if ([message.body isKindOfClass:[NSDictionary class]]) {
            // Bus mode: {seq: N, results: [...]}
            NSDictionary *body = (NSDictionary *)message.body;
            NSNumber *seqNum = body[@"seq"];
            if (seqNum) msgSeq = [seqNum intValue];
            results = body[@"results"];
        } else if ([message.body isKindOfClass:[NSArray class]]) {
            // Car mode: plain array from nativeSearchOSM
            results = (NSArray *)message.body;
        } else if ([message.body isKindOfClass:[NSString class]]) {
            NSData *data = [(NSString *)message.body dataUsingEncoding:NSUTF8StringEncoding];
            id parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            if ([parsed isKindOfClass:[NSDictionary class]]) {
                NSDictionary *body = (NSDictionary *)parsed;
                NSNumber *seqNum = body[@"seq"];
                if (seqNum) msgSeq = [seqNum intValue];
                results = body[@"results"];
            } else if ([parsed isKindOfClass:[NSArray class]]) {
                results = (NSArray *)parsed;
            }
        }
        if (![results isKindOfClass:[NSArray class]]) return;
        
        // Bus mode stale check (car mode has msgSeq=-1, always passes)
        if (msgSeq >= 0 && msgSeq != _searchSeq) return;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.searchResults = [results mutableCopy];
            CGFloat w = self.searchOverlay.bounds.size.width;
            if (w <= 0) { [self appLog:@"⚠️ searchResults: searchOverlay width=0, skip"]; return; }
            CGFloat tableY = _isBusSearchMode ? CGRectGetMaxY(_searchContainer.frame) + 8 : CGRectGetMaxY(self.searchBar.frame) + 8;
            CGFloat maxH = self.searchOverlay.bounds.size.height - tableY - 20;
            CGFloat rowH = (_isBusSearchMode && self.searchResults.count > 0 && self.searchResults[0][@"departure"]) ? 60 : 44;
            CGFloat tableH = MIN((CGFloat)self.searchResults.count * rowH, maxH);
            self.searchResultsTable.frame = CGRectMake(0, tableY, w, MAX(tableH, 1));
            self.searchResultsTable.hidden = (self.searchResults.count == 0);
            [self.searchResultsTable reloadData];
        });
    } else if ([message.name isEqualToString:@"appLog"]) {
        NSString *msg = message.body;
        if ([msg isKindOfClass:[NSString class]]) {
            [self appLog:@"📱 JS: %@", msg];
        }
    } else if ([message.name isEqualToString:@"simCoords"]) {
        NSString *jsonStr = message.body;
        if (![jsonStr isKindOfClass:[NSString class]]) return;
        [self appLog:@"📩 simCoords ricevute (%lu chars) — salvo _simCoords", (unsigned long)[jsonStr length]];
        NSData *data = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
        NSArray *coords = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        if ([coords isKindOfClass:[NSArray class]] && coords.count >= 2) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self appLog:@"✅ simCoords: %lu coordinate salvate, stepPerTick=%.4f", (unsigned long)[coords count], 0.1389];
                    _simCoords = coords;
                    _simIndex = 0;
                    _simStepPerTick = 0.1389;
                // Se in attesa simulazione bus, avvia subito
                if (_simPending) {
                    _simPending = NO;
                    [self appLog:@"🚀 _simPending attivo — avvio simulazione bus"];
                    [self fireSimulation];
                }
            });
        }
    } else if ([message.name isEqualToString:@"routeReady"]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self appLog:@"✅ Percorso pronto, mostro VAI"];
            [self showGoButton];
            // Forza push posizione — usa sempre l'ultima nota, o default
            double lat = _lastLocation ? _lastLocation.coordinate.latitude : 44.49;
            double lon = _lastLocation ? _lastLocation.coordinate.longitude : 11.34;
            double course = _lastLocation.course >= 0 ? _lastLocation.course : (_currentCourse >= 0 ? _currentCourse : 0);
            [self pushPositionToJSWithLat:lat lon:lon course:course];
            [self appLog:@"📌 Forzata push posizione (%.4f, %.4f)", lat, lon];
            // Salva nei recenti
            if (self->_pendingDestDict) {
                TransportMode saveMode = [self->_pendingDestDict[@"isBus"] boolValue] ? TransportModeBus : TransportModeAuto;
                [[SettingsStore shared] addRecentDestination:self->_pendingDestDict forMode:saveMode];
                [self appLog:@"✅ Destinazione salvata nei recenti: %@", self->_pendingDestDict];
            }
        });
    } else if ([message.name isEqualToString:@"navigationStart"]) {
        // Bus navigation started — come auto mode: smoothTimer + navTimer per GPS reale
        dispatch_async(dispatch_get_main_queue(), ^{
            [self appLog:@"✅ Bus navigation started (GPS reale)"];
            self.isNavigating = YES;
            self.userTracking = YES;
            self.userTrackingWithHeading = YES;
            [self updateTrackingButton];
            self.navBar.hidden = YES;
            self.stopNavButton.hidden = NO;
            // Forza push posizione corrente SUBITO (usa sempre l'ultima nota)
            double lat = _lastLocation ? _lastLocation.coordinate.latitude : 44.49;
            double lon = _lastLocation ? _lastLocation.coordinate.longitude : 11.34;
            double course = _lastLocation.course >= 0 ? _lastLocation.course : (_currentCourse >= 0 ? _currentCourse : 0);
            [self pushPositionToJSWithLat:lat lon:lon course:course];
            _interpFrom = CLLocationCoordinate2DMake(lat, lon);
            _interpTo = CLLocationCoordinate2DMake(lat, lon);
            _interpStartTime = [[NSDate date] timeIntervalSince1970];
            _isInterpolating = YES;
            [self appLog:@"✅ navigationStart: push posizione (%.4f, %.4f) + interpolazione forzata", lat, lon];
            [self startSmoothTimer];
            _interpStartTime = 0;
            // Avvia navTimer per aggiornare pill via/ETA ogni 2s
            [_navTimer invalidate];
            _navTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(navTick) userInfo:nil repeats:YES];
            if (self.roadNameLabel) {
                // Rimuovi animazioni pendenti da endNavigation
                [self.roadNameLabel.layer removeAllAnimations];
                [self.roadETAPill.layer removeAllAnimations];
                [self.roadTimePill.layer removeAllAnimations];
                [self.roadDistPill.layer removeAllAnimations];
                self.roadNameLabel.hidden = NO;
                self.roadNameLabel.alpha = 0;
                self.roadETAPill.hidden = NO;
                self.roadETAPill.alpha = 0;
                self.roadTimePill.hidden = NO;
                self.roadTimePill.alpha = 0;
                self.roadDistPill.hidden = NO;
                self.roadDistPill.alpha = 0;
                [UIView animateWithDuration:0.3 animations:^{
                    self.roadNameLabel.alpha = 1;
                    self.roadETAPill.alpha = 1;
                    self.roadTimePill.alpha = 1;
                    self.roadDistPill.alpha = 1;
                }];
            }
        });
    } else if ([message.name isEqualToString:@"requestGtfs"]) {
        // Bus mode: load GTFS database on demand
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSString *gtfsPath = [[NSBundle mainBundle] pathForResource:@"gtfs_db" ofType:@"json"];
            if (!gtfsPath) return;
            NSError *err = nil;
            NSData *gtfsData = [NSData dataWithContentsOfFile:gtfsPath options:NSDataReadingMappedAlways error:&err];
            if (!gtfsData || err) return;
            NSString *b64 = [gtfsData base64EncodedStringWithOptions:0];
            NSString *js = [NSString stringWithFormat:@"loadGtfsDatabaseBase64('%@')", b64];
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.webView evaluateJavaScript:js completionHandler:^(id res, NSError *jsErr) {
                    if (jsErr) [self appLog:@"⚠️ GTFS load error: %@", jsErr.localizedDescription];
                    else [self appLog:@"✅ GTFS DB loaded on-demand (%lu bytes)", (unsigned long)gtfsData.length];
                }];
            });
        });
    }
}

#pragma mark - WKNavigationDelegate

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [self appLog:@"🌍 map.html caricato"];
    // Apply initial settings once map is loaded
    [self applyAllSettings];
}

- (BOOL)searchActive {
    return self.searchActive_Ivar;
}

#pragma mark - Layout Edit Mode

- (void)enterLayoutEditMode {
    _layoutEditMode = YES;
    
    // Crea pillola stile pulsante (come Modalità, Cerca, etc.)
    if (!self.layoutEditOverlay) {
        CGFloat pillW = 100;
        CGFloat pillH = 40;
        CGFloat radius = pillH / 2;
        CGFloat w = self.view.bounds.size.width;
        
        UIView *pill = [[UIView alloc] initWithFrame:CGRectMake(w - pillW - 12, 54, pillW, pillH)];
        pill.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45];
        pill.layer.cornerRadius = radius;
        pill.layer.borderWidth = 1.5;
        pill.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.25].CGColor;
        pill.layer.shadowColor = [UIColor blackColor].CGColor;
        pill.layer.shadowOpacity = 0.3;
        pill.layer.shadowRadius = 8;
        pill.layer.shadowOffset = CGSizeZero;
        pill.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
        pill.userInteractionEnabled = YES;
        
        // ✕ Annulla (grigio su cerchio bianco, piccolo)
        UIButton *xBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        xBtn.frame = CGRectMake(6, 4, 32, 32);
        xBtn.backgroundColor = [UIColor whiteColor];
        xBtn.layer.cornerRadius = 16;
        [xBtn setTitle:@"✕" forState:UIControlStateNormal];
        [xBtn setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
        xBtn.titleLabel.font = [UIFont boldSystemFontOfSize:16];
        [xBtn addTarget:self action:@selector(layoutCancelTapped) forControlEvents:UIControlEventTouchUpInside];
        [pill addSubview:xBtn];
        
        // ✓ Salva (verde)
        UIButton *saveBtn = [UIButton buttonWithType:UIButtonTypeSystem];
        saveBtn.frame = CGRectMake(pillW - 38, 4, 32, 32);
        saveBtn.tintColor = [UIColor systemGreenColor];
        [saveBtn setImage:[UIImage systemImageNamed:@"checkmark.circle.fill"] forState:UIControlStateNormal];
        [saveBtn addTarget:self action:@selector(layoutSaveTapped) forControlEvents:UIControlEventTouchUpInside];
        [pill addSubview:saveBtn];
        
        [self.view addSubview:pill];
        self.layoutEditOverlay = pill;
    }
    self.layoutEditOverlay.hidden = NO;
    [self.view bringSubviewToFront:self.layoutEditOverlay];
    
    // Raccogli tutti gli elementi draggabili (escluso busVC, navBar, e la pill edit)
    NSMutableArray *items = [NSMutableArray array];
    void(^addItem)(UIView*, NSString*, CGRect) = ^(UIView *v, NSString *id, CGRect df) {
        if (!v) return;
        [items addObject:@{@"view": v, @"id": id, @"default": [NSValue valueWithCGRect:df]}];
    };
    
    addItem(self.modalitaButton, @"modalita", CGRectMake(self.view.bounds.size.width - 112, 54, 100, 40));
    addItem(self.mapButton, @"map", CGRectMake(self.view.bounds.size.width - 48, 54, 40, 40));
    addItem(self.searchButton, @"search", CGRectMake(self.view.bounds.size.width - 112, self.view.bounds.size.height * 0.35 + 10, 100, 40));
    addItem(self.trackingButton, @"tracking", CGRectMake(self.view.bounds.size.width - 112, self.view.bounds.size.height - 120, 100, 40));
    addItem(self.settingsButton, @"settings", CGRectMake(self.view.bounds.size.width - 112, self.view.bounds.size.height - 60, 100, 40));
    addItem(self.compassButton, @"compass", CGRectMake(12, self.view.bounds.size.height - 110, 52, 52));
    addItem(self.logButton, @"log", CGRectMake(12, self.view.bounds.size.height - 170, 44, 44));
    addItem((UIView *)self.roadNameLabel, @"streetname", CGRectMake(12, self.view.bounds.size.height - 532, self.view.bounds.size.width - 24, 68));
    addItem(self.stopNavButton, @"stopnav", CGRectMake(self.view.bounds.size.width - 56, 54, 44, 44));
    // 3 pillole separate ETA/Tempo/Distanza
    addItem(self.roadETAPill, @"etapill", CGRectMake(self.view.bounds.size.width - 360, self.view.bounds.size.height - 220, 120, 55));
    addItem(self.roadTimePill, @"timepill", CGRectMake(self.view.bounds.size.width - 230, self.view.bounds.size.height - 220, 120, 55));
    addItem(self.roadDistPill, @"distpill", CGRectMake(self.view.bounds.size.width - 100, self.view.bounds.size.height - 220, 120, 55));
    addItem(_busSpeedPill, @"speedpill", CGRectMake(self.view.bounds.size.width - 100, self.view.bounds.size.height/2 - 22, 44, 44));
    // Mostra temporaneamente pillole per edit (se nascoste)
    if (self.roadNameLabel.hidden && !self.isNavigating) {
        self.roadNameLabel.hidden = NO;
        self.roadNameLabel.alpha = 0.5;
        self.roadStreetLabel.text = @"Via Nome Strada";
        self.roadNameLabel.tag = 9999;
        // Mostra anche le 3 pillole con placeholder
        NSMutableAttributedString *phETA = [[NSMutableAttributedString alloc] initWithString:@"14:30\narrivo"];
        [phETA setAttributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:28], NSForegroundColorAttributeName: [UIColor whiteColor]} range:NSMakeRange(0, 5)];
        [phETA setAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:18], NSForegroundColorAttributeName: [UIColor colorWithWhite:0.80 alpha:1.0]} range:NSMakeRange(5, 7)];
        self.roadETALabel.attributedText = phETA;
        NSMutableAttributedString *phTime = [[NSMutableAttributedString alloc] initWithString:@"9 min\ntempo"];
        [phTime setAttributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:28], NSForegroundColorAttributeName: [UIColor whiteColor]} range:NSMakeRange(0, 6)];
        [phTime setAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:18], NSForegroundColorAttributeName: [UIColor colorWithWhite:0.80 alpha:1.0]} range:NSMakeRange(6, 6)];
        self.roadTimeLabel.attributedText = phTime;
        NSMutableAttributedString *phDist = [[NSMutableAttributedString alloc] initWithString:@"3.2 km\ndistanza"];
        [phDist setAttributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:28], NSForegroundColorAttributeName: [UIColor whiteColor]} range:NSMakeRange(0, 6)];
        [phDist setAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:18], NSForegroundColorAttributeName: [UIColor colorWithWhite:0.80 alpha:1.0]} range:NSMakeRange(6, 9)];
        self.roadDistLabel.attributedText = phDist;
        self.roadETAPill.hidden = NO;
        self.roadETAPill.alpha = 0.5;
        self.roadTimePill.hidden = NO;
        self.roadTimePill.alpha = 0.5;
        self.roadDistPill.hidden = NO;
        self.roadDistPill.alpha = 0.5;
    }
    // Mostra speedpill per edit (se nascosta)
    if (_busSpeedPill.hidden) {
        _busSpeedPill.hidden = NO;
        _busSpeedPill.alpha = 0.5;
        _busSpeedPill.tag = 9999;
    }
    // Anche la pill edit è draggabile
    addItem(self.layoutEditOverlay, @"editpill", CGRectMake(self.view.bounds.size.width - 112, 54, 100, 40));
    self.layoutItems = items;
    
    // Aggiungi pan gesture a ogni elemento
    for (NSDictionary *item in items) {
        UIView *v = item[@"view"];
        v.autoresizingMask = UIViewAutoresizingNone;
        UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(layoutPan:)];
        [v addGestureRecognizer:pan];
        v.userInteractionEnabled = YES;
    }
}

- (void)exitLayoutEditMode:(BOOL)save {
    _layoutEditMode = NO;
    // Nascondi la pill edit (sparisce completamente)
    self.layoutEditOverlay.hidden = YES;
    
    if (save) [self saveLayoutPositions];
    else [self loadSavedLayout];
    
    // Rimuovi pan gesture
    for (NSDictionary *item in self.layoutItems) {
        UIView *v = item[@"view"];
        NSMutableArray *toRemove = [NSMutableArray array];
        for (UIGestureRecognizer *g in v.gestureRecognizers) {
            if ([g isKindOfClass:[UIPanGestureRecognizer class]])
                [toRemove addObject:g];
        }
        for (UIGestureRecognizer *g in toRemove)
            [v removeGestureRecognizer:g];
    }
    self.layoutItems = nil;
    // Se la pillola nome via era stata mostrata solo per edit, nascondila di nuovo
    if (self.roadNameLabel.tag == 9999) {
        self.roadNameLabel.hidden = YES;
        self.roadStreetLabel.text = @"";
        self.roadETALabel.text = @"";
        self.roadTimeLabel.text = @"";
        self.roadDistLabel.text = @"";
        self.roadNameLabel.tag = 0;
        // Nascondi anche le 3 pillole (erano placeholder per edit)
        self.roadETAPill.hidden = YES;
        self.roadTimePill.hidden = YES;
        self.roadDistPill.hidden = YES;
    }
    // Nascondi speedpill se era stata mostrata solo per edit
    if (_busSpeedPill.tag == 9999) {
        _busSpeedPill.hidden = YES;
        _busSpeedPill.tag = 0;
    }
}

- (void)layoutPan:(UIPanGestureRecognizer *)pan {
    UIView *v = pan.view;
    CGPoint t = [pan translationInView:v.superview];
    v.center = CGPointMake(v.center.x + t.x, v.center.y + t.y);
    [pan setTranslation:CGPointZero inView:v.superview];
}

- (void)layoutSaveTapped {
    [self exitLayoutEditMode:YES];
}

- (void)layoutCancelTapped {
    [self exitLayoutEditMode:NO];
}

- (void)saveLayoutPositions {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    for (NSDictionary *item in self.layoutItems) {
        UIView *v = item[@"view"];
        NSString *key = item[@"id"];
        [ud setFloat:v.frame.origin.x forKey:[NSString stringWithFormat:@"layout_%@_x", key]];
        [ud setFloat:v.frame.origin.y forKey:[NSString stringWithFormat:@"layout_%@_y", key]];
        [ud setFloat:v.frame.size.width forKey:[NSString stringWithFormat:@"layout_%@_w", key]];
        [ud setFloat:v.frame.size.height forKey:[NSString stringWithFormat:@"layout_%@_h", key]];
    }
    [ud synchronize];
}

- (void)loadSavedLayout {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    // Forza W e H default per streetname (pill sempre alla larghezza/ altezza giusta)
    // NON tocchiamo X e Y così la posizione salvata dall'utente rimane
    [ud removeObjectForKey:@"layout_streetname_w"];
    [ud removeObjectForKey:@"layout_streetname_h"];
    void(^apply)(UIView*, NSString*, CGRect) = ^(UIView *v, NSString *key, CGRect defFrame) {
        if (!v) return;
        float x = [ud objectForKey:[NSString stringWithFormat:@"layout_%@_x", key]] ? [ud floatForKey:[NSString stringWithFormat:@"layout_%@_x", key]] : defFrame.origin.x;
        float y = [ud objectForKey:[NSString stringWithFormat:@"layout_%@_y", key]] ? [ud floatForKey:[NSString stringWithFormat:@"layout_%@_y", key]] : defFrame.origin.y;
        float w = [ud objectForKey:[NSString stringWithFormat:@"layout_%@_w", key]] ? [ud floatForKey:[NSString stringWithFormat:@"layout_%@_w", key]] : defFrame.size.width;
        float h = [ud objectForKey:[NSString stringWithFormat:@"layout_%@_h", key]] ? [ud floatForKey:[NSString stringWithFormat:@"layout_%@_h", key]] : defFrame.size.height;
        v.frame = CGRectMake(x, y, w, h);
        v.autoresizingMask = UIViewAutoresizingNone;
    };
    
    CGFloat w = self.view.bounds.size.width;
    CGFloat h = self.view.bounds.size.height;
    apply(self.modalitaButton, @"modalita", CGRectMake(w - 112, 54, 100, 40));
    apply(self.mapButton, @"map", CGRectMake(w - 48, 54, 40, 40));
    apply(self.searchButton, @"search", CGRectMake(w - 112, h * 0.35 + 10, 100, 40));
    apply(self.trackingButton, @"tracking", CGRectMake(w - 112, h - 120, 100, 40));
    apply(self.settingsButton, @"settings", CGRectMake(w - 112, h - 60, 100, 40));
    apply(self.compassButton, @"compass", CGRectMake(12, h - 110, 52, 52));
    apply(self.logButton, @"log", CGRectMake(12, h - 170, 44, 44));
    // streetname: NUOVO pannello bianco
    CGFloat streetScreenW = [UIScreen mainScreen].bounds.size.width;
    CGFloat streetScreenH = [UIScreen mainScreen].bounds.size.height;
    CGFloat streetPanelH = 60;
    CGFloat streetPanelY = streetScreenH - streetPanelH;
    apply(self.roadNameLabel, @"streetname", CGRectMake(0, streetPanelY, streetScreenW, streetPanelH));
    apply(self.stopNavButton, @"stopnav", CGRectMake(w - 56, 54, 44, 44));
    // 3 pillole separate ETA/Tempo/Distanza
    apply(self.roadETAPill, @"etapill", CGRectMake(w - 360, h - 220, 120, 55));
    apply(self.roadTimePill, @"timepill", CGRectMake(w - 230, h - 220, 120, 55));
    apply(self.roadDistPill, @"distpill", CGRectMake(w - 100, h - 220, 120, 55));
    apply(_busSpeedPill, @"speedpill", CGRectMake(w - 100, h/2 - 22, 44, 44));
    // Anche la pill edit (per ricordare posizione, ma è nascosta in modalità normale)
    if (self.layoutEditOverlay) {
        apply(self.layoutEditOverlay, @"editpill", CGRectMake(w - 112, 54, 100, 40));
        self.layoutEditOverlay.hidden = YES; // sempre nascosta in modalità normale
    }
}

#pragma mark - Missing Methods

- (void)applyArrowTransform {
    // Arrow rotation gestita dal JS rAF engine
}

- (void)setRoutingProfileInJS {
    NSString *profile;
    switch ([SettingsStore shared].transportMode) {
        case TransportModeBus:
            profile = @"driving";
            break;
        case TransportModeTruck:
            profile = @"driving";
            break;
        default:
            profile = @"driving";
            break;
    }
    NSString *js = [NSString stringWithFormat:@"setRoutingProfile('%@')", profile];
    [self.webView evaluateJavaScript:js completionHandler:nil];
}

- (void)recalculateRoute {
    [self setRoutingProfileInJS];
    _isRecalculating = YES;
    
    CGFloat destLat = [_pendingDestDict[@"lat"] doubleValue];
    CGFloat destLon = [_pendingDestDict[@"lon"] doubleValue];
    NSString *name = _pendingDestDict[@"name"] ?: @"Destinazione";
    
    [self.webView evaluateJavaScript:@"recalculateRoute();"
        completionHandler:^(id result, NSError *error) {
        self->_isRecalculating = NO;
    }];
    // Timeout fallback: unlock after 5s even if JS fails
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self->_isRecalculating = NO;
    });
}

@end
