import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.time.format.*;
import java.util.*;

public class LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger {
  static final String CODE_VERSION = "V9.2.18-UAT";
  static final String BUILD_LABEL = "HYBRID-LIVE + FROZEN-COUNTERFACTUAL + STAT-WATCHING-CASUAL + PERSONAL-POST-COMMIT-REVEAL + HYBRID-PUBLICATION + PREAMBLE-DISPLAY + PLATE-REFERENCE-PACK";
  enum A { HIT, STAND, DOUBLE, SPLIT }
  enum State { NORMAL, COOLING, CAUTIOUS, BRAKE }
  static class Card {
    final String rank, suit;
    final int value;
    Card(String rank,String suit,int value){this.rank=rank;this.suit=suit;this.value=value;}
    public String toString(){return rank+(suit.isEmpty()?"":suit);}
  }
  static final Scanner sc=new Scanner(System.in);
  static final List<String> log=new ArrayList<>();
  static final double PLATFORM_OFFSET=2400.0; // historical default only
  static final double PLATFORM_START=2500.0;  // historical default only
  static final double PLATFORM_DEPLETED=2400.0; // historical default only
  static final double TABLE_MINIMUM=15.0;
  static double sessionPlatformStart=PLATFORM_START;
  static double sessionPlatformOffset=PLATFORM_DEPLETED;
  static double bank=PLATFORM_START, peak=PLATFORM_START, trough=PLATFORM_START, maxDD=0, exposure=0;
  static boolean reached130=false;
  static String sessionName="";
  static State state=State.NORMAL;
  static int w=0,l=0,p=0, wagerMatch=0,wagerAbove=0,wagerBelow=0, actionMatch=0,actionDiff=0;
  static int validationWarnings=0, shuffleEvents=0, cardsSinceShuffle=0, totalCardsObserved=0, amendmentsMade=0;
  static int textbookAlignedDecisions=0, frozenResearchOverrideDecisions=0, frozenPrimarySpecialDecisions=0;
  static final List<String> shuffleLog=new ArrayList<>();
  static final List<String> chronologyLog=new ArrayList<>();
  static final List<Card> currentPlayerPostDealCards=new ArrayList<>();
  static final List<Card> currentDealerCards=new ArrayList<>();
  static int chronologicalCardNumber=0;
  static boolean handWasSplit=false;
  static boolean splitAnyLiveHand=false;
  static int splitAFinalTotal=-1, splitBFinalTotal=-1;
  static double splitACommittedStake=0.0, splitBCommittedStake=0.0;
  static double handCommittedStake=0.0;
  static boolean personalMode=false;
  static boolean hybridMode=false;
  static int lowSinceShuffle=0, neutralSinceShuffle=0, highSinceShuffle=0;
  static final List<String> handSignatureLog=new ArrayList<>();
  static final Path CUMULATIVE_OUTPUT=Path.of("output.txt");
  static final Path SESSION_EVIDENCE_DIR=Path.of("session_evidence");
  static final Path BACKUPS_DIR=Path.of("backups");
  static Path evidenceFile(String name){return SESSION_EVIDENCE_DIR.resolve(name);}
  static void ensureEvidenceDirectory()throws IOException{Files.createDirectories(SESSION_EVIDENCE_DIR);}
  static void ensureBackupsDirectory()throws IOException{Files.createDirectories(BACKUPS_DIR);}
  static String currentInitialAction="NONE";
  static Card currentDealerUpcardForValidation=null;
  static int currentHandNumber=0;
  static String lastSavedSessionId=null;
  static Path lastSavedReport=null;
  static final int DEFAULT_SHUFFLE_REPLICATES=5000;
  static final long SHUFFLE_SEED_BASE=202609050001L;

  static class PreambleContext {
    String id, mode; Path file; int hands,cardsSinceShuffle,totalCardsObserved,shuffleEvents,low,neutral,high;
    Map<String,Integer> physicalCounts=new LinkedHashMap<>();
    TrendState statWatchingState=null;
  }
  static PreambleContext pendingPreamble=null;
  // Live advisory state for the optional post-commit Stat-Watching Casual reveal.
  // It is fed only by completed, visibly observed hands. It never sees the current wager/deal before the human commits.
  static TrendState liveStatWatchingState=new TrendState();

  static class HandSnapshot {
    double bank,peak,trough,maxDD,exposure; boolean reached130; State state;
    int w,l,p,wagerMatch,wagerAbove,wagerBelow,actionMatch,actionDiff;
    int validationWarnings,shuffleEvents,cardsSinceShuffle,totalCardsObserved;
    int splitAFinalTotal,splitBFinalTotal; boolean splitAnyLiveHand; double splitACommittedStake,splitBCommittedStake;
    int textbookAlignedDecisions,frozenResearchOverrideDecisions,frozenPrimarySpecialDecisions;
    int logSize,shuffleLogSize,chronologySize,handSignatureLogSize,chronologicalCardNumber; int lowSinceShuffle,neutralSinceShuffle,highSinceShuffle; Map<String,Integer> counts;
    HandSnapshot(){
      this.bank=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.bank;
      this.peak=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.peak;
      this.trough=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.trough;
      this.maxDD=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.maxDD;
      this.exposure=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.exposure;
      this.reached130=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.reached130;
      this.state=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.state;
      this.w=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.w;
      this.l=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.l;
      this.p=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.p;
      this.wagerMatch=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerMatch;
      this.wagerAbove=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerAbove;
      this.wagerBelow=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerBelow;
      this.actionMatch=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionMatch;
      this.actionDiff=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionDiff;
      this.validationWarnings=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.validationWarnings;
      this.shuffleEvents=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.shuffleEvents;
      this.cardsSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.cardsSinceShuffle;
      this.totalCardsObserved=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.totalCardsObserved;
      this.splitAFinalTotal=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitAFinalTotal;
      this.splitBFinalTotal=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitBFinalTotal;
      this.splitAnyLiveHand=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitAnyLiveHand;
      this.splitACommittedStake=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitACommittedStake;
      this.splitBCommittedStake=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitBCommittedStake;
      this.textbookAlignedDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.textbookAlignedDecisions;
      this.frozenResearchOverrideDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenResearchOverrideDecisions;
      this.frozenPrimarySpecialDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenPrimarySpecialDecisions;
      this.logSize=log.size(); this.shuffleLogSize=shuffleLog.size(); this.chronologySize=chronologyLog.size(); this.handSignatureLogSize=handSignatureLog.size(); this.lowSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.lowSinceShuffle; this.neutralSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.neutralSinceShuffle; this.highSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.highSinceShuffle; this.chronologicalCardNumber=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.chronologicalCardNumber; this.counts=new LinkedHashMap<>(observedPhysicalCounts);
    }
    void restore(){
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.bank=bank; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.peak=peak; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.trough=trough; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.maxDD=maxDD; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.exposure=exposure;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.reached130=reached130; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.state=state;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.w=w; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.l=l; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.p=p;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerMatch=wagerMatch; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerAbove=wagerAbove; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerBelow=wagerBelow; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionMatch=actionMatch; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionDiff=actionDiff;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.validationWarnings=validationWarnings; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.shuffleEvents=shuffleEvents; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.cardsSinceShuffle=cardsSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.totalCardsObserved=totalCardsObserved;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitAFinalTotal=splitAFinalTotal; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitBFinalTotal=splitBFinalTotal; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitAnyLiveHand=splitAnyLiveHand; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitACommittedStake=splitACommittedStake; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.splitBCommittedStake=splitBCommittedStake;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.textbookAlignedDecisions=textbookAlignedDecisions; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenResearchOverrideDecisions=frozenResearchOverrideDecisions; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenPrimarySpecialDecisions=frozenPrimarySpecialDecisions;
      while(log.size()>logSize) log.remove(log.size()-1); while(shuffleLog.size()>shuffleLogSize) shuffleLog.remove(shuffleLog.size()-1); while(chronologyLog.size()>chronologySize) chronologyLog.remove(chronologyLog.size()-1); while(handSignatureLog.size()>handSignatureLogSize) handSignatureLog.remove(handSignatureLog.size()-1); LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.lowSinceShuffle=lowSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.neutralSinceShuffle=neutralSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.highSinceShuffle=highSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.chronologicalCardNumber=chronologicalCardNumber;
      observedPhysicalCounts.clear(); observedPhysicalCounts.putAll(counts);
    }
  }

  public static void main(String[] z)throws Exception{
    System.out.println("============================================================");
    System.out.println("CODE VERSION: " + CODE_VERSION);
    System.out.println("BUILD: " + BUILD_LABEL);
    System.out.println("============================================================");
    System.out.println("DETERMINISTIC BLACKJACK - LIVE + POST-SESSION ANALYSIS");
    System.out.println("Self-contained workflow: L = live capture, S = local shuffle robustness, Q = exit.");
    System.out.println();
    String latestPublicationSid=latestLiveSessionId();
    // If the latest completed session still has an outstanding plate, remind the
    // operator to publish THAT session. Only advance to the next-session reminder
    // after the latest plate has explicitly been marked COMPLETE.
    if(latestPublicationSid!=null && !"COMPLETE".equals(plateStatusForSession(latestPublicationSid)))
      printPublicationWorkflowReminder(latestPublicationSid);
    else
      printUpcomingPublicationWorkflowReminder(latestPublicationSid);
    // Historical observed-reconstruction repair remains available to maintainers only.
    // It is intentionally removed from the end-user menu to avoid workflow confusion.
    if(z.length>0 && z[0].equalsIgnoreCase("--repair-observed")){
      runObservedReconstructionCorrection();
      return;
    }
    while(true){
      String operation=choice("\nMAIN MENU: [L] LIVE SESSION / [S] RUN SHUFFLE ROBUSTNESS / [Q] QUIT: ","LSQ");
      if(operation.equals("Q")){ System.out.println("Program closed."); return; }
      if(operation.equals("S")){ runAutomaticShuffleAnalysis(); continue; }
      if(hasPendingShuffleAnalysis()){
        String pending=latestPendingSessionId();
        int n=publicationSessionNumber(pending);
        System.out.println("\n*** LIVE SESSION BLOCKED - POST-SESSION ANALYSIS OUTSTANDING ***");
        System.out.println(sessionLabel(n,pending)+" has not completed S shuffle robustness analysis.");
        System.out.println("Run S and complete the analysis before another live session can start.");
        continue;
      }
      if(!confirmPreviousSessionPlateBeforeNewLive()) continue;
      if(choice("Record a PREAMBLE before the next formal session? [Y/N]: ","YN").equals("Y")){
        runPreamble();
      }
      runLiveSession();
    }
  }

  static String liveModeName(){return hybridMode?"HYBRID":(personalMode?"PERSONAL":"FROZEN");}
  static String modeReportLabel(){return hybridMode?"HYBRID - Frozen reference visible; operator choices observed":(personalMode?"PERSONAL - no Frozen wager/action prompts":"FROZEN ARCHITECTURE");}

  static void resetLiveSessionState(){
    log.clear();shuffleLog.clear();chronologyLog.clear();currentPlayerPostDealCards.clear();currentDealerCards.clear();handSignatureLog.clear();observedPhysicalCounts.clear();
    sessionPlatformStart=PLATFORM_START;sessionPlatformOffset=PLATFORM_DEPLETED;bank=PLATFORM_START;peak=PLATFORM_START;trough=PLATFORM_START;maxDD=0;exposure=0;reached130=false;state=State.NORMAL;w=l=p=wagerMatch=wagerAbove=wagerBelow=actionMatch=actionDiff=0;validationWarnings=shuffleEvents=cardsSinceShuffle=totalCardsObserved=amendmentsMade=0;textbookAlignedDecisions=frozenResearchOverrideDecisions=frozenPrimarySpecialDecisions=0;chronologicalCardNumber=0;handWasSplit=false;splitAnyLiveHand=false;splitAFinalTotal=splitBFinalTotal=-1;splitACommittedStake=splitBCommittedStake=0;handCommittedStake=0;personalMode=false;hybridMode=false;lowSinceShuffle=neutralSinceShuffle=highSinceShuffle=0;currentInitialAction="NONE";currentDealerUpcardForValidation=null;currentHandNumber=0;sessionName="";
    liveStatWatchingState=new TrendState();
    if(pendingPreamble!=null){
      cardsSinceShuffle=pendingPreamble.cardsSinceShuffle;
      lowSinceShuffle=pendingPreamble.low; neutralSinceShuffle=pendingPreamble.neutral; highSinceShuffle=pendingPreamble.high;
      observedPhysicalCounts.putAll(pendingPreamble.physicalCounts);
      if(pendingPreamble.statWatchingState!=null) liveStatWatchingState=copyTrendState(pendingPreamble.statWatchingState);
    }
  }

  static void runPreamble()throws Exception{
    pendingPreamble=null;
    resetLiveSessionState();
    say("\nDETERMINISTIC BLACKJACK - PREAMBLE CAPTURE");
    say("Separate pre-formal capture | excluded from project ledger/plate statistics | card/shuffle context preserved into next formal session");
    say("SOURCE: 247blackjack.com | PLAY-MONEY | configured 6-deck game");
    say("VALIDATION ON: suit required, wager checked, balance arithmetic checked where determinable.");
    say("HAND REVIEW ON: rejected/amended current hands roll back before re-entry.");
    personalMode=choice("Mode: [P] PERSONAL (no Frozen prompts) / [F] FROZEN architecture: ","PF").equals("P");
    while(sessionName.isBlank()){
      System.out.print("Preamble description (e.g. join table / lead-in before formal session): ");
      sessionName=sc.nextLine().trim().replace("|","/").replace("\r"," ").replace("\n"," ");
      if(sessionName.length()>80) sessionName=sessionName.substring(0,80).trim();
      if(sessionName.isBlank()) say("Please enter a session name or short description.");
    }
    say("PREAMBLE DESCRIPTION: "+sessionName);
    say(personalMode ? "PERSONAL MODE: play entirely by your own judgement. Frozen wager/action prompts are suppressed; optional Stat-Watching comparison is offered only AFTER a committed choice differs from Frozen." : "FROZEN MODE: existing Frozen wager/action prompts remain active.");

    say("STARTING BALANCE CHECK: remove/clear ALL wagers or chips from the table before reading the balance.");
    while(!choice("Are there currently NO wagers/chips committed on the table? [Y/N]: ","YN").equals("Y")){
      say("Clear/remove all wagers/chips first. Do not enter the opening balance while a wager is committed.");
    }
    double enteredPlatformStart=money("Platform balance with NO wager committed: £");
    while(enteredPlatformStart<100.0){
      say("Starting platform balance must be at least £100.00 to preserve the £100 research frame.");
      enteredPlatformStart=money("Platform balance with NO wager committed: £");
    }
    sessionPlatformStart=enteredPlatformStart;
    sessionPlatformOffset=sessionPlatformStart-100.0;
    bank=sessionPlatformStart;
    peak=sessionPlatformStart;
    trough=sessionPlatformStart;
    maxDD=0;
    say("PREAMBLE START BALANCE CONFIRMED: Platform £"+m(bank)+" | research-equivalent £100.00 | session depletion floor £"+m(sessionPlatformOffset));
    log.add("PREAMBLE START BALANCE | Platform £"+m(bank)+" | Research-equivalent £100.00 | Session depletion floor £"+m(sessionPlatformOffset));

    say("PREAMBLE EVIDENCE ONLY: cumulative output/ledger will NOT be updated.\n");
    for(int h=1;h<=999 && bank>sessionPlatformOffset;h++){
      currentHandNumber=h;
      while(true){
      HandSnapshot snapshot=new HandSnapshot();
      updateState(); double ref=wager(h);
      say("\n========== PREAMBLE HAND "+h+" ==========");
      String sh=choice("Visible shuffle immediately BEFORE this hand? [Y/N]: ","YN");
      if(sh.equals("Y")){
        shuffleEvents++;
        shuffleLog.add("Shuffle observed before hand "+h+" after "+cardsSinceShuffle+" recorded cards since previous start/shuffle.");
        say("SHUFFLE RECORDED before hand "+h+" | previous segment cards recorded: "+cardsSinceShuffle);
        cardsSinceShuffle=0; lowSinceShuffle=neutralSinceShuffle=highSinceShuffle=0;
      }
      double researchBank=researchBankroll();
      if(personalMode){
        say("Bankroll £"+m(bank)+" | PERSONAL MODE");
        showPreWagerRecentHistory();
      } else {
        say("Bankroll £"+m(bank)+" | Research-equivalent available £"+m(researchBank)+" | State "+state+" | FROZEN WAGER £"+m(ref));
      }
      double swLiveWager=personalMode ? liveStatWatchingWager(h,researchBank) : 0.0;
      String swLiveWagerMindset=personalMode ? liveStatWatchingWagerMindset(h,swLiveWager,researchBank) : "";
      double actual=personalMode ? validatedPersonalWager() : validatedWager(ref);
      exposure+=actual;
      if(!personalMode) compareWager(actual,ref);
      else maybeRevealStatWatchingWager(h,actual,ref,swLiveWager,swLiveWagerMindset);
      double before=bank;
      handWasSplit=false; splitAnyLiveHand=false; splitAFinalTotal=splitBFinalTotal=-1; splitACommittedStake=splitBCommittedStake=0; handCommittedStake=actual; currentInitialAction="NONE"; currentPlayerPostDealCards.clear(); currentDealerCards.clear();
      say("Initial visible deal entry: PLAYER first card -> DEALER upcard -> PLAYER second card.");
      Card p1=card("Player card 1");
      Card up=card("Dealer upcard"); currentDealerUpcardForValidation=up;
      Card p2=card("Player card 2");
      List<Card> player=new ArrayList<>(); player.add(p1); player.add(p2);
      say("Player "+ct(player)+" = "+tt(player)+" | Dealer "+rt(up));
      showObservedHandContext(p1,p2,up);
      if(player.size()==2 && total(player)==21){ currentInitialAction="NATURAL"; say("Player 21/natural: no further player decision entered here."); }
      else play("MAIN",player,up,actual,true,true);
      String dealer;
      while(true){
        dealer=dealerSequence("Dealer hidden/draw sequence IN ORDER after upcard (e.g. 6H,10D,5C; or ?): ");
        showDealerTotal(up);
        if(dealerSequenceCompleteForLiveSettlement(player,up)) break;
      }

      // Show the reconstructed physical deal order BEFORE result/bankroll entry so the
      // operator can compare our interpretation with the game screen while the hand is fresh.
      showChronologyPreview(h,p1,up,p2);
      if(choice("Does this reconstructed card order correctly represent the hand? [Y/N]: ","YN").equals("N")){
        amendmentsMade++;
        snapshot.restore();
        say("CARD-STREAM REVIEW: hand "+h+" rejected before result/bankroll entry; re-entering the hand.");
        log.add("  CARD-STREAM REVIEW | Hand "+h+" rejected by operator; replacement entry follows.");
        continue;
      }

      String r=validatedResult(player,up); if(r.equals("W"))w++; else if(r.equals("L"))l++; else p++;
      boolean natural=(player.size()==2 && total(player)==21);
      say("BANKROLL CHECK: enter the updated platform balance shown immediately after the on-screen Win/Lose/Push verdict.");
      bank=validatedEndingBankroll(before,actual,handCommittedStake,r,natural,handWasSplit);

      String amend=choice("Hand "+h+" captured. Amend any details before moving on? [Y/N]: ","YN");
      if(amend.equals("Y")){
        amendmentsMade++;
        snapshot.restore();
        say("AMENDMENT: hand "+h+" superseded before commit and will now be re-entered from the beginning.");
        say("Previous hands remain committed. Current-hand counters/cards/exposure were rolled back.");
        log.add("  AMENDMENT AUDIT | Hand "+h+" superseded before commit; replacement entry follows.");
        continue;
      }

      observeCompletedHandForLiveStatWatching(p1,p2,up,r);
      appendChronology(h,p1,up,p2);
      String hist="HAND_HISTORY | "+signature(p1,p2,up)+" | mode "+liveModeName()+" | action "+currentInitialAction+" | result "+r+" | bank "+m(before)+"->"+m(bank)+" | wager "+m(actual);
      handSignatureLog.add(hist);
      log.add("  "+hist);
      peak=Math.max(peak,bank); trough=Math.min(trough,bank); maxDD=Math.max(maxDD,peak-bank); if(researchBankroll()>=130) reached130=true;
      log.add(String.format(Locale.ROOT,"HAND %02d | %.2f->%.2f | ref %.2f actual %.2f | committed %.2f | %s | P %s | D-up %s | D-hidden/draw %s | %s",h,before,bank,ref,actual,handCommittedStake,state,ct(player),rt(up),dealer,r));
      say("Recorded | Peak £"+m(peak)+" | Trough £"+m(trough)+" | MaxDD £"+m(maxDD)+" | Exposure £"+m(exposure));
      if(bank<=sessionPlatformOffset){
        say("DEPLETION THRESHOLD REACHED: platform balance £"+m(bank)+" <= session floor £"+m(sessionPlatformOffset)+".");
        say("Research-equivalent bankroll: £"+m(bank-sessionPlatformOffset)+". Session stops here.");
      }
      break; // current hand committed
      }
      double remainingResearch=researchBankroll();
      if(remainingResearch<TABLE_MINIMUM){
        say("TABLE EXIT: research-equivalent bankroll £"+m(remainingResearch)+" is below £"+m(TABLE_MINIMUM)+" table minimum.");
        say("No further valid wager can be placed.");
        say("PREAMBLE ENDED AUTOMATICALLY after Hand "+h+".");
        break;
      }
      if(line("ENTER next PREAMBLE hand, or Q to end PREAMBLE: ").equalsIgnoreCase("Q")){
        if(choice("END PREAMBLE now? [Y/N]: ","YN").equals("Y")) break;
      }
    }
    savePreamble();
  }


  static void runLiveSession()throws Exception{
    resetLiveSessionState();
    say("\nDETERMINISTIC BLACKJACK - LIVE 30-HAND COMPANION");
    say("Manual external-session entry | actual platform balance captured before Hand 1 | dynamic £100 research frame | 30-hand reference");
    say("SOURCE: 247blackjack.com | PLAY-MONEY | configured 6-deck game");
    say("VALIDATION ON: suit required, wager checked, balance arithmetic checked where determinable.");
    say("HAND REVIEW ON: rejected/amended current hands roll back before re-entry.");
    String liveMode=choice("Mode: [P] PERSONAL / [F] FROZEN / [H] HYBRID (Frozen reference visible, your choices): ","PFH");
    personalMode=liveMode.equals("P"); hybridMode=liveMode.equals("H");
    String preambleDefaultName = pendingPreamble==null ? "" :
      "Preamble: "+pendingPreamble.hands+" hands / cards: "+pendingPreamble.totalCardsObserved+
      " / since latest observed shuffle/start: "+pendingPreamble.cardsSinceShuffle;
    while(sessionName.isBlank()){
      if(preambleDefaultName.isBlank()){
        System.out.print("Session name / description (e.g. Make 3x Bankroll): ");
      }else{
        System.out.print("Session name / description [Enter = "+preambleDefaultName+"]: ");
      }
      sessionName=sc.nextLine().trim().replace("|","/").replace("\r"," ").replace("\n"," ");
      if(sessionName.isBlank()&&!preambleDefaultName.isBlank()) sessionName=preambleDefaultName;
      if(sessionName.length()>80) sessionName=sessionName.substring(0,80).trim();
      if(sessionName.isBlank()) say("Please enter a session name or short description.");
    }
    say("SESSION NAME: "+sessionName);
    say(personalMode ? "PERSONAL MODE: play entirely by your own judgement. Frozen wager/action prompts are suppressed; optional Stat-Watching comparison is offered only AFTER a committed choice differs from Frozen." : hybridMode ? "HYBRID MODE: Frozen wager/action reference remains visible; your entered wager/actions are the observed journey. Full Frozen counterfactual is calculated by S when source-complete." : "FROZEN MODE: existing Frozen wager/action prompts remain active.");

    say("STARTING BALANCE CHECK: remove/clear ALL wagers or chips from the table before reading the balance.");
    while(!choice("Are there currently NO wagers/chips committed on the table? [Y/N]: ","YN").equals("Y")){
      say("Clear/remove all wagers/chips first. Do not enter the opening balance while a wager is committed.");
    }
    double enteredPlatformStart=money("Platform balance with NO wager committed: £");
    while(enteredPlatformStart<100.0){
      say("Starting platform balance must be at least £100.00 to preserve the £100 research frame.");
      enteredPlatformStart=money("Platform balance with NO wager committed: £");
    }
    sessionPlatformStart=enteredPlatformStart;
    sessionPlatformOffset=sessionPlatformStart-100.0;
    bank=sessionPlatformStart;
    peak=sessionPlatformStart;
    trough=sessionPlatformStart;
    maxDD=0;
    say("SESSION START BALANCE CONFIRMED: Platform £"+m(bank)+" | research-equivalent £100.00 | session depletion floor £"+m(sessionPlatformOffset));
    log.add("SESSION START BALANCE | Platform £"+m(bank)+" | Research-equivalent £100.00 | Session depletion floor £"+m(sessionPlatformOffset));

    say("Cumulative project file: "+CUMULATIVE_OUTPUT.toAbsolutePath()+"\n");
    for(int h=1;h<=30 && bank>sessionPlatformOffset;h++){
      currentHandNumber=h;
      while(true){
      HandSnapshot snapshot=new HandSnapshot();
      updateState(); double ref=wager(h);
      say("\n========== HAND "+h+" / 30 ==========");
      String sh=choice("Visible shuffle immediately BEFORE this hand? [Y/N]: ","YN");
      if(sh.equals("Y")){
        shuffleEvents++;
        shuffleLog.add("Shuffle observed before hand "+h+" after "+cardsSinceShuffle+" recorded cards since previous start/shuffle.");
        say("SHUFFLE RECORDED before hand "+h+" | previous segment cards recorded: "+cardsSinceShuffle);
        cardsSinceShuffle=0; lowSinceShuffle=neutralSinceShuffle=highSinceShuffle=0;
      }
      double researchBank=researchBankroll();
      if(personalMode){
        say("Bankroll £"+m(bank)+" | PERSONAL MODE");
        showPreWagerRecentHistory();
      } else {
        say("Bankroll £"+m(bank)+" | Research-equivalent available £"+m(researchBank)+" | State "+state+" | FROZEN WAGER £"+m(ref));
      }
      double swLiveWager=personalMode ? liveStatWatchingWager(h,researchBank) : 0.0;
      String swLiveWagerMindset=personalMode ? liveStatWatchingWagerMindset(h,swLiveWager,researchBank) : "";
      double actual=personalMode ? validatedPersonalWager() : validatedWager(ref);
      exposure+=actual;
      if(!personalMode) compareWager(actual,ref);
      else maybeRevealStatWatchingWager(h,actual,ref,swLiveWager,swLiveWagerMindset);
      double before=bank;
      handWasSplit=false; splitAnyLiveHand=false; splitAFinalTotal=splitBFinalTotal=-1; splitACommittedStake=splitBCommittedStake=0; handCommittedStake=actual; currentInitialAction="NONE"; currentPlayerPostDealCards.clear(); currentDealerCards.clear();
      say("Initial visible deal entry: PLAYER first card -> DEALER upcard -> PLAYER second card.");
      Card p1=card("Player card 1");
      Card up=card("Dealer upcard"); currentDealerUpcardForValidation=up;
      Card p2=card("Player card 2");
      List<Card> player=new ArrayList<>(); player.add(p1); player.add(p2);
      say("Player "+ct(player)+" = "+tt(player)+" | Dealer "+rt(up));
      showObservedHandContext(p1,p2,up);
      if(player.size()==2 && total(player)==21){ currentInitialAction="NATURAL"; say("Player 21/natural: no further player decision entered here."); }
      else play("MAIN",player,up,actual,true,true);
      String dealer;
      while(true){
        dealer=dealerSequence("Dealer hidden/draw sequence IN ORDER after upcard (e.g. 6H,10D,5C; or ?): ");
        showDealerTotal(up);
        if(dealerSequenceCompleteForLiveSettlement(player,up)) break;
      }

      // Show the reconstructed physical deal order BEFORE result/bankroll entry so the
      // operator can compare our interpretation with the game screen while the hand is fresh.
      showChronologyPreview(h,p1,up,p2);
      if(choice("Does this reconstructed card order correctly represent the hand? [Y/N]: ","YN").equals("N")){
        amendmentsMade++;
        snapshot.restore();
        say("CARD-STREAM REVIEW: hand "+h+" rejected before result/bankroll entry; re-entering the hand.");
        log.add("  CARD-STREAM REVIEW | Hand "+h+" rejected by operator; replacement entry follows.");
        continue;
      }

      String r=validatedResult(player,up); if(r.equals("W"))w++; else if(r.equals("L"))l++; else p++;
      boolean natural=(player.size()==2 && total(player)==21);
      say("BANKROLL CHECK: enter the updated platform balance shown immediately after the on-screen Win/Lose/Push verdict.");
      bank=validatedEndingBankroll(before,actual,handCommittedStake,r,natural,handWasSplit);

      String amend=choice("Hand "+h+" captured. Amend any details before moving on? [Y/N]: ","YN");
      if(amend.equals("Y")){
        amendmentsMade++;
        snapshot.restore();
        say("AMENDMENT: hand "+h+" superseded before commit and will now be re-entered from the beginning.");
        say("Previous hands remain committed. Current-hand counters/cards/exposure were rolled back.");
        log.add("  AMENDMENT AUDIT | Hand "+h+" superseded before commit; replacement entry follows.");
        continue;
      }

      observeCompletedHandForLiveStatWatching(p1,p2,up,r);
      appendChronology(h,p1,up,p2);
      String hist="HAND_HISTORY | "+signature(p1,p2,up)+" | mode "+liveModeName()+" | action "+currentInitialAction+" | result "+r+" | bank "+m(before)+"->"+m(bank)+" | wager "+m(actual);
      handSignatureLog.add(hist);
      log.add("  "+hist);
      peak=Math.max(peak,bank); trough=Math.min(trough,bank); maxDD=Math.max(maxDD,peak-bank); if(researchBankroll()>=130) reached130=true;
      log.add(String.format(Locale.ROOT,"HAND %02d | %.2f->%.2f | ref %.2f actual %.2f | committed %.2f | %s | P %s | D-up %s | D-hidden/draw %s | %s",h,before,bank,ref,actual,handCommittedStake,state,ct(player),rt(up),dealer,r));
      say("Recorded | Peak £"+m(peak)+" | Trough £"+m(trough)+" | MaxDD £"+m(maxDD)+" | Exposure £"+m(exposure));
      if(bank<=sessionPlatformOffset){
        say("DEPLETION THRESHOLD REACHED: platform balance £"+m(bank)+" <= session floor £"+m(sessionPlatformOffset)+".");
        say("Research-equivalent bankroll: £"+m(bank-sessionPlatformOffset)+". Session stops here.");
      }
      break; // current hand committed
      }
      double remainingResearch=researchBankroll();
      if(remainingResearch<TABLE_MINIMUM){
        say("TABLE EXIT: research-equivalent bankroll £"+m(remainingResearch)+" is below £"+m(TABLE_MINIMUM)+" table minimum.");
        say("No further valid wager can be placed.");
        say("LIVE SESSION ENDED AUTOMATICALLY after Hand "+h+".");
        break;
      }
      if(h<30 && line("ENTER next hand, or Q to finish: ").equalsIgnoreCase("Q")) break;
    }
    save();
  }

  static void updateState(){
    double rb=researchBankroll();
    if(state==State.BRAKE)return;
    if(state==State.COOLING){ if(rb>=120)state=State.CAUTIOUS; return; }
    if(state==State.CAUTIOUS){ if(rb>=140)state=State.NORMAL; return; }
    if(reached130 && rb<=100)state=State.COOLING;
  }
  static double wager(int h){
    double rb=researchBankroll();
    if(state!=State.NORMAL)return 15;
    if(h<=8 || rb<130)return 15;
    if(rb<150)return 20;
    if(rb<170)return 30;
    return 15;
  }
  static void compareWager(double a,double r){
    if(Math.abs(a-r)<.001){wagerMatch++;say("Wager: MATCH reference");}
    else if(a>r){wagerAbove++;say("Wager: £"+m(a-r)+" ABOVE reference");}
    else {wagerBelow++;say("Wager: £"+m(r-a)+" BELOW reference");}
  }

  static TrendState copyTrendState(TrendState src){
    TrendState d=new TrendState();
    for(TrendHand h:src.recent)d.recent.addLast(new TrendHand(h.outcome,copyCards(h.visibleCards),h.dealerUp,h.revealedHoleValue));
    for(Map.Entry<Integer,List<Integer>> e:src.revealedHoleByUp.entrySet())d.revealedHoleByUp.put(e.getKey(),new ArrayList<>(e.getValue()));
    d.lossStreak=src.lossStreak;d.winStreak=src.winStreak;d.lastPressHand=src.lastPressHand;
    return d;
  }

  static double liveStatWatchingWager(int hand,double available){
    int mult=statWatchingMultiplier(liveStatWatchingState,hand,available);
    double v=15.0*mult;
    while(mult>1&&available+0.001<v){mult--;v=15.0*mult;}
    if(mult>1) liveStatWatchingState.lastPressHand=hand; // policy state advances whether or not the human asks to reveal it
    return Math.min(v,Math.max(0.0,available));
  }

  static String liveStatWatchingWagerMindset(int hand,double swWager,double available){
    if(liveStatWatchingState.recent.size()<3 || hand<=4)
      return "Too little recent completed-hand evidence for a press; ordinary £15 posture.";
    int score=trendScore(liveStatWatchingState);
    String snap=trendSnapshotReason(liveStatWatchingState);
    if(swWager>15.001)
      return snap+"; visible pattern pressure is strong enough for a "+(int)Math.round(swWager/15.0)+"x press"+(available<75?", restrained by bankroll pressure":"")+".";
    if(hand-liveStatWatchingState.lastPressHand<3)
      return snap+"; recent press cooldown keeps the wager at the ordinary £15 level.";
    if(score<3) return snap+"; not enough visible pattern pressure to justify a press, so stay at £15.";
    return snap+"; any larger press is constrained by the currently available research bankroll.";
  }

  static void maybeRevealStatWatchingWager(int hand,double personalWager,double frozenRef,double swWager,String mindset){
    if(!personalMode || Math.abs(personalWager-frozenRef)<0.001)return;
    say("PERSONAL WAGER COMMITTED. It differs from the hidden Frozen reference.");
    boolean showW=choice("See Stat-Watching Casual's wager? [Y/N]: ","YN").equals("Y");
    if(showW) say("STAT-WATCHING CASUAL WAGER: £"+m(swWager));
    boolean showM=choice("See Stat-Watching Casual's mindset? [Y/N]: ","YN").equals("Y");
    if(showM) say("STAT-WATCHING CASUAL MINDSET: "+mindset);
    log.add("  POST-COMMIT COMPARISON | H"+hand+" | WAGER | personal £"+m(personalWager)+" | hidden frozen £"+m(frozenRef)+" | stat-watching £"+m(swWager)+" | wager viewed "+(showW?"Y":"N")+" | mindset viewed "+(showM?"Y":"N")+(showM?" | "+mindset:""));
  }

  static A liveStatWatchingAction(List<Card> c,int up,boolean first,boolean pair,boolean canExtra){
    A base=casualReplayAction(c,up,first,pair,canExtra);
    int[] mix=recentFiveVisibleCardMix(liveStatWatchingState);int n=mix[0],tens=mix[1],t=total(c);
    if(first&&n==5&&!soft(c)&&t>=12&&t<=16&&up>=4&&up<=6&&base==A.HIT&&tens==0)return A.STAND;
    if(first&&n==5&&(t==10||t==11)&&base==A.DOUBLE&&tens>=4)return A.HIT;
    if(first&&!soft(c)&&base==A.HIT){
      List<Integer> seen=liveStatWatchingState.revealedHoleByUp.get(up);
      if(t>=12&&t<=16&&seen!=null&&seen.size()>=2){
        double avg=seen.stream().mapToInt(Integer::intValue).average().orElse(10);
        if(avg<=5.0)return A.STAND;
      }
    }
    return base;
  }

  static String liveStatWatchingActionMindset(List<Card> c,int up,A sw){
    int t=total(c);int[]mix=recentFiveVisibleCardMix(liveStatWatchingState);int n=mix[0],tens=mix[1];
    A base=casualReplayAction(c,up,true,c.size()==2&&c.get(0).value==c.get(1).value,true);
    if(n==5&&!soft(c)&&t>=12&&t<=16&&up>=4&&up<=6&&base==A.HIT&&sw==A.STAND&&tens==0)
      return "Five-card memory: only "+tens+" recent 10-value card"+(tens==1?"":"s")+" stood out; on hard "+t+" vs dealer "+up+", that made him prefer standing and leaving the awkward draw to the dealer.";
    if(n==5&&(t==10||t==11)&&base==A.DOUBLE&&sw==A.HIT&&tens>=4)
      return "Five-card memory: "+tens+" of the five recently remembered visible cards were 10-value; on "+t+", that made him reluctant to double the exposure, so he took the ordinary Hit.";
    List<Integer>seen=liveStatWatchingState.revealedHoleByUp.get(up);
    if(!soft(c)&&t>=12&&t<=16&&sw==A.STAND&&seen!=null&&seen.size()>=2){
      double avg=seen.stream().mapToInt(Integer::intValue).average().orElse(10);
      if(avg<=5.0)return "Hole-card hunch: prior revealed hole cards behind dealer "+up+" averaged "+String.format(Locale.ROOT,"%.1f",avg)+"; stand on hard "+t+".";
    }
    return "Casual-style hand policy on "+ct(c)+" vs dealer "+up+" recommends "+sw+"; no special short-memory hunch changes this decision.";
  }

  static void maybeRevealStatWatchingAction(String label,List<Card> c,Card upCard,A personal,A frozen,A sw,String mindset){
    if(!personalMode || personal==frozen)return;
    say("PERSONAL ACTION COMMITTED. It differs from the hidden Frozen action.");
    boolean showA=choice("See Stat-Watching Casual's action? [Y/N]: ","YN").equals("Y");
    if(showA) say("STAT-WATCHING CASUAL ACTION: "+sw);
    boolean showM=choice("See Stat-Watching Casual's mindset? [Y/N]: ","YN").equals("Y");
    if(showM) say("STAT-WATCHING CASUAL MINDSET: "+mindset);
    log.add("  POST-COMMIT COMPARISON | H"+currentHandNumber+" | "+label+" ACTION | personal "+personal+" | hidden frozen "+frozen+" | stat-watching "+sw+" | action viewed "+(showA?"Y":"N")+" | mindset viewed "+(showM?"Y":"N")+(showM?" | "+mindset:""));
  }

  static void observeCompletedHandForLiveStatWatching(Card p1,Card p2,Card up,String result){
    List<Card> visible=new ArrayList<>();visible.add(p1);visible.add(p2);visible.addAll(currentPlayerPostDealCards);visible.add(up);visible.addAll(currentDealerCards);
    Integer hole=currentDealerCards.isEmpty()?null:currentDealerCards.get(0).value;
    int outcome=result.equals("W")?1:result.equals("L")?-1:0;
    liveStatWatchingState.observe(outcome,visible,up.value,hole);
  }

  static double researchBankroll(){
    return Math.max(0.0, bank-sessionPlatformOffset);
  }

  static double uncommittedResearchBankroll(){
    return Math.max(0.0, researchBankroll()-handCommittedStake);
  }

  static boolean canCommitAdditional(double amount){
    return uncommittedResearchBankroll()+0.001 >= amount;
  }

  static void play(String label,List<Card> c,Card upCard,double stake,boolean first,boolean allowSplit){
    int up=upCard.value;
    while(true){
      int currentTotal=total(c);
      if(currentTotal>21){say(label+" BUST at "+currentTotal);return;}
      // Validation boundary: once a live hand reaches exactly 21, it is decision-complete.
      // Do not offer HIT/DOUBLE/SPLIT or create a synthetic STAND decision event.
      if(currentTotal==21){say(label+" "+ct(c)+" = "+tt(c)+" | 21 - hand complete; no further player action entered.");return;}
      boolean pair=allowSplit && first && c.size()==2 && c.get(0).value==c.get(1).value;
      boolean canD=first && canCommitAdditional(stake);
      boolean canP=pair && canCommitAdditional(stake);

      A rec=null;
      if(!personalMode){
        rec=recommend(c,up,first,pair,canD,canP);
        String provenance=policyProvenance(c,up,first,pair,rec);
        say(label+" "+ct(c)+" = "+tt(c)+" | FROZEN POLICY: "+rec+" ["+provenance+"]");
        log.add("  POLICY SOURCE | "+label+" | "+ct(c)+" vs "+rt(upCard)+" | "+rec+" | "+provenance);
      } else {
        say(label+" "+ct(c)+" = "+tt(c)+" | PERSONAL DECISION");
      }

      if(first && !canD){
        say("DOUBLE unavailable: research-equivalent bankroll £"+m(researchBankroll())+
            " | already committed £"+m(handCommittedStake)+
            " | additional £"+m(stake)+" required.");
      }
      if(pair && !canP){
        say("SPLIT unavailable: research-equivalent bankroll £"+m(researchBankroll())+
            " | already committed £"+m(handCommittedStake)+
            " | additional £"+m(stake)+" required.");
      }

      String opts="HS"+(canD?"D":"")+(canP?"P":"");
      A hiddenFrozen=null, swLiveAction=null; String swLiveActionMindset="";
      if(personalMode){
        hiddenFrozen=recommend(c,up,first,pair,canD,canP);
        swLiveAction=liveStatWatchingAction(c,up,first,pair,(canD||canP));
        swLiveActionMindset=liveStatWatchingActionMindset(c,up,swLiveAction);
      }
      String actionPrompt=(currentHandNumber<=2)
          ? "Actual action [H=Hit, S=Stand, D=Double, P=Split] (available: "+opts+"): "
          : "Actual action ["+opts+"]: ";
      A act=parse(choice(actionPrompt,opts));
      if(label.equals("MAIN") && first && currentInitialAction.equals("NONE")) currentInitialAction=act.toString();
      if(!personalMode){
        if(act==rec)actionMatch++; else actionDiff++;
        log.add("  "+label+" | "+ct(c)+" | frozen "+rec+" | actual "+act);
      } else {
        log.add("  "+label+" | "+ct(c)+" | personal actual "+act);
        maybeRevealStatWatchingAction(label,c,upCard,act,hiddenFrozen,swLiveAction,swLiveActionMindset);
      }

      if(act==A.STAND)return;
      if(act==A.DOUBLE){
        // Defensive guard as well as removing D from the allowed options.
        if(!canCommitAdditional(stake)){
          validationWarnings++;
          say("DOUBLE REJECTED: insufficient research-equivalent bankroll above the session-specific protected floor.");
          continue;
        }
        exposure+=stake; handCommittedStake+=stake;
        if(label.equals("SPLIT A")) splitACommittedStake+=stake;
        else if(label.equals("SPLIT B")) splitBCommittedStake+=stake;
        c.add(card(label+" double card")); say(label+" after DOUBLE: "+ct(c)+" = "+tt(c)); return;
      }
      if(act==A.SPLIT){
        if(!canCommitAdditional(stake)){
          validationWarnings++;
          say("SPLIT REJECTED: insufficient research-equivalent bankroll above the session-specific protected floor.");
          continue;
        }
        handWasSplit=true; exposure+=stake; handCommittedStake+=stake; splitACommittedStake=stake; splitBCommittedStake=stake; Card x=c.get(0),y=c.get(1);
        List<Card>a=new ArrayList<>(List.of(x,card("Split A added card")));
        List<Card>b=new ArrayList<>(List.of(y,card("Split B added card")));
        say("Split A "+ct(a)+" = "+tt(a)); say("Split B "+ct(b)+" = "+tt(b));
        if(x.value==11&&y.value==11){
          splitAFinalTotal=total(a); splitBFinalTotal=total(b);
          splitAnyLiveHand=(splitAFinalTotal<=21 || splitBFinalTotal<=21);
          say("Split Aces: one additional card each; stop.");return;
        }
        play("SPLIT A",a,upCard,stake,true,false);
        play("SPLIT B",b,upCard,stake,true,false);
        splitAFinalTotal=total(a); splitBFinalTotal=total(b);
        splitAnyLiveHand=(splitAFinalTotal<=21 || splitBFinalTotal<=21);
        return;
      }
      c.add(card(label+" hit card")); first=false; allowSplit=false;
    }
  }

  static double validatedPersonalWager(){
    while(true){
      double a=money("Actual wager £: ");
      if(a<=0){validationWarnings++;say("VALIDATION FAILED: wager must be positive.");continue;}
      double available=researchBankroll();
      if(a>available+0.001){
        validationWarnings++;
        say("WAGER REJECTED: research-equivalent bankroll is only £"+m(available)+
            ". The session-specific platform floor £"+m(sessionPlatformOffset)+" is protected and cannot be wagered in the research session.");
        continue;
      }
      return a;
    }
  }

  static String rankOnly(Card q){ return q.rank; }
  static int bucket(Card q){ return (q.value>=2&&q.value<=6)?-1:(q.value>=10?1:0); }
  static String signature(Card p1,Card p2,Card up){
    String a=rankOnly(p1), b=rankOnly(p2);
    if(a.compareTo(b)>0){String t=a;a=b;b=t;}
    return a+","+b+" vs "+rankOnly(up);
  }
  static int previousOccurrences(String sig){
    if(!Files.exists(CUMULATIVE_OUTPUT)) return 0;
    try{
      int n=0; String needle="HAND_SIGNATURE | "+sig;
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)) if(x.trim().equals(needle)) n++;
      return n;
    }catch(Exception e){ validationWarnings++; return 0; }
  }
  static List<String> previousOutcomeLines(String sig){
    // HAND_HISTORY is intentionally present in more than one report section.
    // De-duplicate exact history records so one historical occurrence is shown once.
    LinkedHashSet<String> unique=new LinkedHashSet<>();
    if(!Files.exists(CUMULATIVE_OUTPUT)) return new ArrayList<>();
    try{
      String needle="HAND_HISTORY | "+sig+" |";
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(t.startsWith(needle)) unique.add(t);
      }
    }catch(Exception e){ validationWarnings++; }
    return new ArrayList<>(unique);
  }

  static void showPreWagerRecentHistory(){
    boolean recentHandsRequested=choice("Before wagering, show previous hands played on this table/session? [Y/N]: ","YN").equals("Y");
    if(recentHandsRequested){
      int recentCount=Integer.parseInt(choice("How many recent hands? [1/2/3/4/5]: ","12345"));
      log.add("  BEHAVIOURAL INFO REQUEST | recent table hands | REQUESTED BEFORE WAGER | last "+recentCount);
      handSignatureLog.add("  HISTORY_REQUESTED | RECENT_TABLE_HANDS | Y | BEFORE_WAGER | requested "+recentCount);
      showRecentTableHands(recentCount);
    } else {
      log.add("  BEHAVIOURAL INFO REQUEST | recent table hands | NOT REQUESTED BEFORE WAGER");
      handSignatureLog.add("  HISTORY_REQUESTED | RECENT_TABLE_HANDS | N | BEFORE_WAGER");
      say("RECENT TABLE-HAND HISTORY: not shown before wager.");
    }
  }

  static void showObservedHandContext(Card p1,Card p2,Card up){
    String sig=signature(p1,p2,up);
    int prev=previousOccurrences(sig);
    int cl=0,cn=0,ch=0;
    for(Card q:List.of(p1,p2,up)){int b=bucket(q);if(b<0)cl++;else if(b>0)ch++;else cn++;}
    int bl=lowSinceShuffle-cl, bn=neutralSinceShuffle-cn, bh=highSinceShuffle-ch;
    if(currentHandNumber<=1){
      // Hand 1 has no prior hands in the current live session. Keep the console clean:
      // no prior-history block and no pre-hand card-mix line.
      say("OBSERVED HAND (suits excluded): "+sig);
    } else {
      say("OBSERVED HAND (suits excluded): "+sig+" | appeared before in project output: "+prev+" time(s)");
      boolean sameHandRequested=false;
      if(personalMode){
        sameHandRequested=choice("View previous outcomes for this starting hand? [Y/N]: ","YN").equals("Y");
        log.add("  BEHAVIOURAL INFO REQUEST | matching starting-hand history | "+(sameHandRequested?"REQUESTED":"NOT REQUESTED")+" | signature "+sig);
        handSignatureLog.add("  HISTORY_REQUESTED | MATCHING_START_HAND | "+(sameHandRequested?"Y":"N")+" | signature "+sig);
        if(sameHandRequested && prev>0) showMatchingHandHistory(sig);
        else if(sameHandRequested) say("MATCHING-HAND HISTORY: no previous occurrence recorded.");
        else say("MATCHING-HAND HISTORY: not shown.");
      } else if(prev>0){
        // Frozen-mode history remains observational only; suppress empty history blocks.
        showMatchingHandHistory(sig);
      }
      say("CARD MIX before current visible hand since last recorded shuffle: Low "+Math.max(0,bl)+" | Neutral "+Math.max(0,bn)+" | High "+Math.max(0,bh));
    }
    say("CARD MIX current visible hand (P1 + P2 + dealer up): Low "+cl+" | Neutral "+cn+" | High "+ch);
    say("Low=2-6 | Neutral=7-9 | High=10/J/Q/K/A | observational only");
    handSignatureLog.add("HAND_SIGNATURE | "+sig);
    handSignatureLog.add("  PRIOR_OCCURRENCES | "+prev+" | BEFORE_LNH "+Math.max(0,bl)+"/"+Math.max(0,bn)+"/"+Math.max(0,bh)+" | CURRENT_VISIBLE_LNH "+cl+"/"+cn+"/"+ch);
  }

  static void showMatchingHandHistory(String sig){
    List<String> priorOutcomes=previousOutcomeLines(sig);
    say("CURRENT STARTING HAND | "+sig);
    if(priorOutcomes.isEmpty()){
      say("PREVIOUS MATCHES | none recorded");
      say("ALL PREVIOUS OCCURRENCES | TOTAL 0 | W 0 (0.0%) | L 0 (0.0%) | P 0 (0.0%)");
      handSignatureLog.add("  HISTORY_MATCHES_SHOWN | 0 | signature "+sig);
      return;
    }

    // Keep the console as restrained as the replay GUI: current state + at most
    // the five most recent matching historical occurrences. Aggregate statistics
    // below still use every eligible previous occurrence, not only the displayed three.
    int shown=Math.min(5,priorOutcomes.size());
    say("PREVIOUS MATCHES | showing most recent "+shown+" of "+priorOutcomes.size());
    for(int i=priorOutcomes.size()-shown;i<priorOutcomes.size();i++){
      String x=priorOutcomes.get(i);
      String detail=x.substring(("HAND_HISTORY | "+sig+" | ").length());
      say("  PREVIOUS "+(i-(priorOutcomes.size()-shown)+1)+" | "+detail);
    }

    int wHist=0,lHist=0,pHist=0;
    for(String x:priorOutcomes){
      if(x.contains(" | result W |")) wHist++;
      else if(x.contains(" | result L |")) lHist++;
      else if(x.contains(" | result P |")) pHist++;
    }
    int total=wHist+lHist+pHist;
    double wp=total==0?0.0:100.0*wHist/total;
    double lp=total==0?0.0:100.0*lHist/total;
    double pp=total==0?0.0:100.0*pHist/total;
    say(String.format(Locale.ROOT,
      "ALL PREVIOUS OCCURRENCES | TOTAL %d | W %d (%.1f%%) | L %d (%.1f%%) | P %d (%.1f%%)",
      total,wHist,wp,lHist,lp,pHist,pp));
    handSignatureLog.add("  HISTORY_MATCHES_SHOWN | "+shown+" | total eligible "+priorOutcomes.size()+" | signature "+sig);
  }

  static void showRecentTableHands(int requested){
    List<String> prior=new ArrayList<>();
    for(String x:handSignatureLog) if(x.startsWith("HAND_HISTORY | ")) prior.add(x);
    int n=Math.min(Math.min(requested,5),prior.size());
    if(n==0){
      say("RECENT TABLE HANDS | none yet in this session");
      handSignatureLog.add("  RECENT_TABLE_HANDS_SHOWN | 0");
      return;
    }
    say("RECENT TABLE HANDS | showing "+n+" most recent from current session");
    int display=1;
    for(int i=prior.size()-n;i<prior.size();i++){
      String x=prior.get(i);
      String detail=x.startsWith("HAND_HISTORY | ")?x.substring("HAND_HISTORY | ".length()):x;
      say("  PREVIOUS "+(display++)+" | "+detail);
    }
    handSignatureLog.add("  RECENT_TABLE_HANDS_SHOWN | "+n+" | requested "+requested);
  }

  static String policyProvenance(List<Card> c,int up,boolean first,boolean pair,A rec){
    if(first && pair && c.size()==2 && c.get(0).value==10 && c.get(1).value==10 && up==10 && rec==A.STAND){
      frozenResearchOverrideDecisions++;
      return "FROZEN RESEARCH OVERRIDE: prior SPLIT -> STAND; also TEXTBOOK";
    }
    if(first && total(c)==11 && !soft(c) && up==10 && rec==A.DOUBLE){
      frozenPrimarySpecialDecisions++;
      return "FROZEN PRIMARY: DOUBLE; TEXTBOOK-aligned";
    }
    textbookAlignedDecisions++;
    return "TEXTBOOK / FROZEN-ALIGNED";
  }

  static A recommend(List<Card>c,int up,boolean first,boolean pair,boolean canD,boolean canP){
    int t=total(c); if(t>=21)return A.STAND;
    if(pair){int q=c.get(0).value;
      if(q==10)return A.STAND;
      if(canP){
        if(q==11||q==8)return A.SPLIT;
        if(q==9&&in(up,2,3,4,5,6,8,9))return A.SPLIT;
        if(q==7&&between(up,2,7))return A.SPLIT;
        if(q==6&&between(up,2,6))return A.SPLIT;
        if(q==4&&(up==5||up==6))return A.SPLIT;
        if((q==3||q==2)&&between(up,2,7))return A.SPLIT;
      }
      // If a split is unaffordable (or not called for), continue into the ordinary
      // hard/soft policy for the same cards instead of inventing a forced HIT.
    }
    if(soft(c)){
      if(t>=19)return A.STAND;
      if(t==18){if(first&&canD&&between(up,3,6))return A.DOUBLE; if(up==2||up==7||up==8)return A.STAND; return A.HIT;}
      if(t==17){if(first&&canD&&between(up,3,6))return A.DOUBLE;return A.HIT;}
      if(t==15||t==16){if(first&&canD&&between(up,4,6))return A.DOUBLE;return A.HIT;}
      if(t==13||t==14){if(first&&canD&&(up==5||up==6))return A.DOUBLE;return A.HIT;}
      return A.HIT;
    }
    if(t>=17)return A.STAND; if(t>=13)return between(up,2,6)?A.STAND:A.HIT; if(t==12)return between(up,4,6)?A.STAND:A.HIT;
    if(t==11)return first&&canD&&between(up,2,10)?A.DOUBLE:A.HIT;
    if(t==10)return first&&canD&&between(up,2,9)?A.DOUBLE:A.HIT;
    if(t==9)return first&&canD&&between(up,3,6)?A.DOUBLE:A.HIT;
    return A.HIT;
  }

  static int total(List<Card>c){int t=0,a=0;for(Card q:c){t+=q.value;if(q.value==11)a++;}while(t>21&&a-->0)t-=10;return t;}
  static boolean soft(List<Card>c){int t=0,a=0;for(Card q:c){t+=q.value;if(q.value==11)a++;}while(t>21&&a>0){t-=10;a--;}return a>0;}
  static String tt(List<Card>c){return (soft(c)?"soft ":"")+total(c);}
  static String ct(List<Card>c){List<String>x=new ArrayList<>();for(Card q:c)x.add(q.toString());return String.join(",",x);}
  static String rt(Card q){return q.toString();}
  // Stable ASCII rendering for chronology/card-stream review.
  // Avoids console/font substitution of suit glyphs (e.g. ♠ appearing as ?).
  static String streamCard(Card q){
    String s=q.suit;
    String a=s.equals("♠")?"S":s.equals("♥")?"H":s.equals("♦")?"D":s.equals("♣")?"C":s.equals("?")?"U":s;
    return q.rank+a;
  }
  static boolean between(int x,int a,int b){return x>=a&&x<=b;} static boolean in(int x,int...v){for(int q:v)if(x==q)return true;return false;}
  static A parse(String s){return switch(s){case"H"->A.HIT;case"S"->A.STAND;case"D"->A.DOUBLE;default->A.SPLIT;};}
  static final Map<String,Integer> observedPhysicalCounts=new LinkedHashMap<>();

  static Card card(String p){
    while(true){
      String raw=line(p+": ").trim().toUpperCase(Locale.ROOT);
      Card q=parseCard(raw);
      if(q==null){ say("INVALID CARD. Use rank+suit such as 8H, KS, A♦, 10C, or 4U if suit genuinely unknown."); continue; }
      if(q.suit.isEmpty()){
        validationWarnings++;
        say("SUIT MISSING - entry rejected. Re-enter with S/H/D/C, ♠/♥/♦/♣, or U for explicitly unknown.");
        continue;
      }
      trackPhysicalCard(q);
      String lp=p.toLowerCase(Locale.ROOT);
      if(lp.contains("hit card") || lp.contains("double card") || lp.contains("split a added card") || lp.contains("split b added card")) currentPlayerPostDealCards.add(q);
      return q;
    }
  }
  static Card parseCard(String raw){
    String x=raw.trim().toUpperCase(Locale.ROOT).replace(" ",""), suit="";
    if(x.endsWith("♠")||x.endsWith("S")){suit="♠";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♥")||x.endsWith("H")){suit="♥";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♦")||x.endsWith("D")){suit="♦";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♣")||x.endsWith("C")){suit="♣";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("U")){suit="?";x=x.substring(0,x.length()-1);}
    int v;
    if(x.equals("A"))v=11; else if(x.matches("10|J|Q|K"))v=10;
    else {try{v=Integer.parseInt(x);}catch(Exception e){return null;}if(v<2||v>9)return null;}
    return new Card(x,suit,v);
  }
  static void trackPhysicalCard(Card q){
    if(q.suit.isEmpty()) return;
    totalCardsObserved++;
    cardsSinceShuffle++;
    if(!q.suit.equals("?")) observedPhysicalCounts.merge(q.rank+q.suit,1,Integer::sum);
  }
  static String dealerSequence(String prompt){
    currentDealerCards.clear();
    while(true){
      String raw=line(prompt).trim(); 
      if(raw.equals("?")){validationWarnings++; say("Dealer sequence explicitly recorded as UNKNOWN."); return "?";}
      String[] a=raw.split(","); List<String> out=new ArrayList<>(); boolean ok=true;
      for(String x:a){
        Card q=parseCard(x);
        if(q==null || q.suit.isEmpty()){ok=false;break;}
        trackPhysicalCard(q); currentDealerCards.add(q); out.add(q.toString());
      }
      if(ok&&!out.isEmpty())return String.join(",",out);
      validationWarnings++;
      say("VALIDATION FAILED. Every dealer card needs a suit (or U), e.g. 6H,10D,5C.");
    }
  }


  static void showDealerTotal(Card up){
    if(currentDealerCards.isEmpty()){
      say("Dealer total: UNKNOWN (dealer hidden/draw sequence not fully recorded)");
      return;
    }
    List<Card> full=new ArrayList<>();
    full.add(up);
    full.addAll(currentDealerCards);
    int t=total(full);
    String status=t>21 ? " BUST" : (t==21 ? " 21" : "");
    say("Dealer "+ct(full)+" = "+tt(full)+status);
  }

  static boolean dealerSequenceCompleteForLiveSettlement(List<Card> player, Card up){
    // Live-settlement guard: a dealer below 17 cannot settle while any player hand remains live.
    // For split rounds, track the completed child totals instead of bypassing validation.
    // If every split child busted, the dealer does not need to draw further.
    if(currentDealerCards.isEmpty()) return true;

    boolean playerStillLive;
    if(handWasSplit){
      playerStillLive=splitAnyLiveHand;
      if(!playerStillLive) return true;
    } else {
      playerStillLive=total(player)<=21;
      if(!playerStillLive) return true;
    }

    List<Card> dealer=new ArrayList<>();
    dealer.add(up); dealer.addAll(currentDealerCards);
    int dt=total(dealer);
    if(dt>=17) return true;

    validationWarnings++;
    say("*** DEALER CARD-SEQUENCE INCOMPLETE ***");
    if(handWasSplit){
      say("Dealer is currently "+dt+". At least one split hand remains live (Split A "+splitAFinalTotal+", Split B "+splitBFinalTotal+") so another dealer card is required before settlement.");
      log.add("  DEALER_SEQUENCE_GUARD | split round | Split A "+splitAFinalTotal+" | Split B "+splitBFinalTotal+" | dealer total "+dt+" below 17 | re-entry required");
    } else {
      say("Dealer is currently "+dt+". A live player hand requires another dealer card before settlement.");
      log.add("  DEALER_SEQUENCE_GUARD | dealer total "+dt+" below 17 before settlement | re-entry required");
    }
    say("Please re-check the game screen and enter the COMPLETE dealer hidden/draw sequence.");
    return false;
  }

  static String validatedResult(List<Card> player, Card up){
    while(true){
      String entered=choice("Result W/L/P: ","WLP");

      if(handWasSplit){
        if(currentDealerCards.isEmpty()){
          say("SPLIT CHILD CHECK: dealer sequence UNKNOWN; child outcomes cannot be derived.");
          if(choice("Confirm entered round result "+entered+" from the website? [Y/N]: ","YN").equals("Y")) return entered;
          validationWarnings++;
          continue;
        }
        int dt=dealerTotal(up);
        boolean dealerNatural=isDealerNatural(up);
        String a=splitChildOutcome(splitAFinalTotal,dt,dealerNatural);
        String b=splitChildOutcome(splitBFinalTotal,dt,dealerNatural);
        say("SPLIT CHILD CHECK | A "+splitAFinalTotal+" vs Dealer "+dt+" = "+resultWord(a)+" | B "+splitBFinalTotal+" vs Dealer "+dt+" = "+resultWord(b));
        log.add("  SPLIT_CHILD_RESULT | A total "+splitAFinalTotal+" stake "+m(splitACommittedStake)+" result "+a+" | B total "+splitBFinalTotal+" stake "+m(splitBCommittedStake)+" result "+b+" | dealer "+dt);
        if(a.equals(b)){
          if(entered.equals(a)){ say("RESULT VALIDATED ✓ both split children imply "+resultWord(a)); return entered; }
          validationWarnings++;
          say("*** RESULT VALIDATION WARNING *** both split children imply "+resultWord(a)+" ("+a+") but entered round result is "+resultWord(entered)+" ("+entered+").");
          String c=choice("[R] re-enter result / [A] accept round-level discrepancy: ","RA");
          if(c.equals("R")) continue;
          log.add("  SPLIT ROUND RESULT OVERRIDE | children both "+a+" | entered "+entered);
          return entered;
        }
        say("Mixed split outcomes: round-level W/L/P retained as entered; child outcomes will drive balance validation.");
        return entered;
      }

      if(currentDealerCards.isEmpty()){
        say("RESULT CHECK: dealer sequence is UNKNOWN, so card-based result validation cannot be completed.");
        if(choice("Confirm entered result "+entered+" from the website? [Y/N]: ","YN").equals("Y")) return entered;
        validationWarnings++;
        continue;
      }

      List<Card> dealer=new ArrayList<>();
      dealer.add(up); dealer.addAll(currentDealerCards);
      int pt=total(player), dt=total(dealer);
      boolean playerNatural=(player.size()==2 && pt==21);
      boolean dealerNatural=(dealer.size()==2 && dt==21);
      String expected;
      if(pt>21) expected="L";
      else if(dealerNatural) expected=playerNatural?"P":"L";
      else if(playerNatural) expected="W";
      else if(dt>21) expected="W";
      else if(pt>dt) expected="W";
      else if(pt<dt) expected="L";
      else expected="P";

      if(entered.equals(expected)){
        say("RESULT VALIDATED ✓ cards imply "+resultWord(expected)+" | Player "+pt+" vs Dealer "+dt);
        return entered;
      }

      validationWarnings++;
      say("*** RESULT VALIDATION WARNING ***");
      say("Player "+ct(player)+" = "+pt+" | Dealer "+ct(dealer)+" = "+dt);
      say("Cards imply "+resultWord(expected)+" ("+expected+") but entered result is "+resultWord(entered)+" ("+entered+").");
      String c=choice("[R] re-enter result / [A] accept discrepancy with explanation: ","RA");
      if(c.equals("R")) continue;
      String note=line("Reason/explanation (check the game screen first): ");
      log.add("  RESULT VALIDATION OVERRIDE | cards imply "+expected+" entered "+entered+" | "+note);
      return entered;
    }
  }

  static int dealerTotal(Card up){
    List<Card> dealer=new ArrayList<>(); dealer.add(up); dealer.addAll(currentDealerCards); return total(dealer);
  }

  static boolean isDealerNatural(Card up){
    return currentDealerCards.size()==1 && dealerTotal(up)==21;
  }

  static String splitChildOutcome(int childTotal,int dealerTotal,boolean dealerNatural){
    if(childTotal>21) return "L";
    if(dealerNatural) return "L";
    if(dealerTotal>21) return "W";
    if(childTotal>dealerTotal) return "W";
    if(childTotal<dealerTotal) return "L";
    return "P";
  }

  static double splitNet(String result,double stake){
    return result.equals("W") ? stake : result.equals("L") ? -stake : 0.0;
  }

  static String resultWord(String r){
    return r.equals("W")?"PLAYER WIN":r.equals("L")?"PLAYER LOSS":"PUSH";
  }

  static double validatedWager(double ref){
    while(true){
      double a=money("Actual wager £: ");
      if(a<=0){validationWarnings++;say("VALIDATION FAILED: wager must be positive.");continue;}
      double available=researchBankroll();
      if(a>available+0.001){
        validationWarnings++;
        say("WAGER REJECTED: research-equivalent bankroll is only £"+m(available)+
            ". The session-specific platform floor £"+m(sessionPlatformOffset)+" is protected and cannot be wagered in the research session.");
        continue;
      }
      if(Math.abs(a-ref)>.001){
        validationWarnings++;
        say("CHECK: frozen reference is £"+m(ref)+" but entered wager is £"+m(a)+".");
        String c=choice("Is £"+m(a)+" definitely the wager placed? [Y/N]: ","YN");
        if(c.equals("N"))continue;

        // Preserve the actual wager as evidence, but classify why it differed.
        String reason=choice("Reason for wager deviation: [A] accidental entry/website amount / [I] intentional deviation / [O] other: ","AIO");
        String reasonText=reason.equals("A") ? "ACCIDENTAL" : reason.equals("I") ? "INTENTIONAL" : "OTHER";
        String note="";
        if(reason.equals("O")) note=line("Brief reason/note: ");
        log.add("  WAGER DEVIATION | frozen_reference "+m(ref)+" | actual "+m(a)+" | difference "+m(a-ref)+" | reason "+reasonText+(note.isEmpty()?"":" | note "+note));
      }
      return a;
    }
  }

  static double validatedEndingBankroll(double before,double base,double committed,String result,boolean natural,boolean split){
    while(true){
      double entered=money("Bankroll AFTER hand £: ");
      if(split){
        if(currentDealerCards.isEmpty() || splitAFinalTotal<0 || splitBFinalTotal<0){
          say("Split balance check unavailable: child/dealer evidence is incomplete.");
          if(choice("Confirm website balance £"+m(entered)+" is correct? [Y/N]: ","YN").equals("Y")) return entered;
          validationWarnings++; continue;
        }
        int dt=dealerTotal(currentDealerUpcardForValidation);
        boolean dealerNatural=isDealerNatural(currentDealerUpcardForValidation);
        String ar=splitChildOutcome(splitAFinalTotal,dt,dealerNatural);
        String br=splitChildOutcome(splitBFinalTotal,dt,dealerNatural);
        double expected=before+splitNet(ar,splitACommittedStake)+splitNet(br,splitBCommittedStake);
        say("SPLIT BALANCE CHECK | A "+ar+" £"+m(splitACommittedStake)+" | B "+br+" £"+m(splitBCommittedStake)+" | expected £"+m(expected));
        if(Math.abs(entered-expected)<0.011){ say("BALANCE VALIDATED ✓ expected £"+m(expected)); return entered; }
        validationWarnings++;
        say("*** BALANCE VALIDATION WARNING *** Expected £"+m(expected)+" but entered £"+m(entered)+" | difference £"+m(entered-expected));
        String c=choice("[R] re-enter balance / [A] accept discrepancy with note: ","RA");
        if(c.equals("R")) continue;
        String note=line("Reason/note (may be brief): ");
        log.add("  SPLIT BALANCE OVERRIDE | expected "+m(expected)+" entered "+m(entered)+" | "+note);
        return entered;
      }
      double expected;
      if(result.equals("P")) expected=before;
      else if(result.equals("L")) expected=before-committed;
      else expected=before+(natural ? 1.5*base : committed);

      if(Math.abs(entered-expected)<0.011){
        say("BALANCE VALIDATED ✓ expected £"+m(expected));
        return entered;
      }

      validationWarnings++;
      say("*** BALANCE VALIDATION WARNING ***");
      say("Start £"+m(before)+" | committed stake £"+m(committed)+" | result "+result+(natural?" | natural blackjack":""));
      say("Expected £"+m(expected)+" | entered £"+m(entered)+" | difference £"+m(entered-expected));
      String c=choice("[R] re-enter balance / [A] accept discrepancy with note: ","RA");
      if(c.equals("R")) continue;
      String note=line("Reason/note (may be brief): ");
      log.add("  VALIDATION OVERRIDE | expected "+m(expected)+" entered "+m(entered)+" | "+note);
      return entered;
    }
  }


  static void showChronologyPreview(int hand,Card p1,Card up,Card p2){
    say("\n--- CARD STREAM REVIEW: HAND "+hand+" ---");
    say("Reconstructed physical deal order (not the order you typed hidden cards):");
    say("Suit key: S=Spades, H=Hearts, D=Diamonds, C=Clubs, U=Unknown. Suits are shown in ASCII so they cannot disappear in the console.");
    int n=chronologicalCardNumber;
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER card 1",streamCard(p1)));
    if(currentDealerCards.isEmpty()){
      say("[UNKNOWN] DEALER hole card / dealer sequence not fully recorded");
    } else {
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER hole card",streamCard(currentDealerCards.get(0))));
    }
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER card 2",streamCard(p2)));
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER upcard",streamCard(up)));
    for(Card q:currentPlayerPostDealCards)
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER post-deal card",streamCard(q)));
    for(int i=1;i<currentDealerCards.size();i++)
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER draw "+i,streamCard(currentDealerCards.get(i))));
    if(handWasSplit)
      say("SPLIT NOTE: post-deal split cards are shown in operator-entry order; verify this sub-order against the screen.");

    int cardsThisHand=n-chronologicalCardNumber;
    say("TOTAL CARDS RECORDED THIS HAND: "+cardsThisHand);
    say("--- END CARD STREAM REVIEW ---\n");
  }

  static void appendChronology(int hand,Card p1,Card up,Card p2){
    chronologyLog.add("HAND "+hand+" DEAL-ORDER RECONSTRUCTION");
    addChron(hand,"PLAYER card 1",p1);
    if(currentDealerCards.isEmpty()){
      chronologyLog.add("  [UNKNOWN] DEALER hole card / dealer sequence not fully recorded");
    } else {
      addChron(hand,"DEALER hole card",currentDealerCards.get(0));
    }
    addChron(hand,"PLAYER card 2",p2);
    addChron(hand,"DEALER upcard",up);
    for(Card q:currentPlayerPostDealCards) addChron(hand,"PLAYER post-deal card",q);
    for(int i=1;i<currentDealerCards.size();i++) addChron(hand,"DEALER draw "+i,currentDealerCards.get(i));
    chronologyLog.add("");
  }
  static void addChron(int hand,String role,Card q){
    chronologicalCardNumber++;
    chronologyLog.add(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",chronologicalCardNumber,hand,role,q));
  }

  static double money(String p){while(true)try{return Double.parseDouble(line(p).replace("£","").trim());}catch(Exception e){say("Enter numeric amount.");}}
  static String choice(String p,String allowed){while(true){String s=line(p).toUpperCase();if(s.length()==1&&allowed.contains(s))return s;say("Allowed: "+allowed);}}
  static String line(String p){System.out.print(p);return sc.nextLine().trim();}
  static void say(String s){System.out.println(s);log.add(s);}
  static String m(double x){return String.format(Locale.UK,"%.2f",x);}

  static class LedgerTotals { int sessions=0, hands=0; double cumulativePL=0.0; }

  static LedgerTotals readLedgerTotals(){
    LedgerTotals t=new LedgerTotals();
    if(!Files.exists(CUMULATIVE_OUTPUT)) return t;
    try{
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String q=x.trim();
        if(!q.startsWith("LEDGER_ENTRY |")) continue;
        t.sessions++;
        for(String part:q.split("\\|")){
          String z=part.trim();
          if(z.startsWith("hands ")) t.hands+=Integer.parseInt(z.substring(6).trim());
          else if(z.startsWith("session_pl ")) t.cumulativePL+=Double.parseDouble(z.substring(11).trim());
        }
      }
    }catch(Exception e){ validationWarnings++; }
    return t;
  }

  static int committedHands(){ return w+l+p; }

  // Explicitly distinguishes player decision events from hands played.
  static int totalDecisionEvents(){
    return textbookAlignedDecisions+frozenResearchOverrideDecisions+frozenPrimarySpecialDecisions;
  }

  static String ledgerEntry(String stamp){
    double researchFinal=bank-sessionPlatformOffset;
    double sessionPL=researchFinal-100.0;
    return "LEDGER_ENTRY | session "+stamp+" | mode "+liveModeName()+
      " | hands "+committedHands()+" | start 100.00 | final "+m(researchFinal)+" | session_pl "+m(sessionPL)+
      " | total_decision_events "+totalDecisionEvents()+
      " | wager_match "+wagerMatch+" | wager_above "+wagerAbove+" | wager_below "+wagerBelow+
      " | action_match "+actionMatch+" | action_different "+actionDiff;
  }

  static List<String> ledgerSummaryLines(LedgerTotals prior,double thisPL,int thisHands){
    int sessions=prior.sessions+1, hands=prior.hands+thisHands;
    double cumulative=prior.cumulativePL+thisPL;
    double cumulativePct=100.0*cumulative/(100.0*sessions);
    double runningProjectBalance=100.0+cumulative;
    List<String> x=new ArrayList<>();
    x.add("PROJECT LEDGER SUMMARY");
    x.add("Completed sessions: "+sessions+" | Total hands: "+hands);
    x.add("Cumulative project P/L: £"+m(cumulative));
    x.add("Cumulative P/L vs £100 per-session starting capital: "+m(cumulativePct)+"%");
    x.add("Project-equivalent running balance (initial £100 + cumulative P/L): £"+m(runningProjectBalance));
    x.add("Ledger basis: each live session independently starts at research-equivalent £100; cumulative P/L sums session profits/losses.");
    return x;
  }


  static void savePreamble()throws Exception{
    String stamp=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
    ensureEvidenceDirectory();
    Path f=evidenceFile("blackjack_preamble_"+stamp+".txt");
    List<String> r=new ArrayList<>();
    r.add("DETERMINISTIC BLACKJACK - PREAMBLE EVIDENCE REPORT");
    r.add("PREAMBLE_ID: "+stamp);
    r.add("LINKED_FORMAL_SESSION: PENDING - next formal live session");
    r.add("PREAMBLE DESCRIPTION: "+sessionName);
    r.add("MODE: "+modeReportLabel());
    r.add("PREAMBLE HANDS: "+committedHands());
    r.add("BOUNDARY: preamble is observational lead-in evidence only; it is excluded from formal-session W/L/P, exposure, bankroll score, project ledger and plate statistics.");
    r.add("PLATFORM START £"+m(sessionPlatformStart)+" | FINAL £"+m(bank)+" | PEAK £"+m(peak)+" | TROUGH £"+m(trough)+" | MAXDD £"+m(maxDD)+" | EXPOSURE £"+m(exposure));
    r.add("W/L/P "+w+"/"+l+"/"+p);
    r.add("Shuffle events explicitly observed during preamble: "+shuffleEvents);
    r.add("Shuffle log: "+(shuffleLog.isEmpty()?"none":String.join(" | ",shuffleLog)));
    r.add("Total cards recorded during preamble: "+totalCardsObserved);
    r.add("Cards since most recent observed shuffle/preamble capture start at preamble end: "+cardsSinceShuffle);
    r.add("Exact known rank+suit identities retained for continuing-shoe validation: "+observedPhysicalCounts.size());
    r.add(""); r.add("OBSERVED INITIAL-HAND INDEX + HISTORICAL OUTCOMES (SUITS EXCLUDED)"); r.addAll(handSignatureLog);
    r.add(""); r.add("CHRONOLOGICAL CARD DATASET (PREAMBLE)"); r.addAll(chronologyLog);
    r.add(""); r.add("AUDIT LOG"); r.addAll(log);
    Files.write(f,r,StandardCharsets.UTF_8);
    PreambleContext pc=new PreambleContext();
    pc.id=stamp; pc.file=f; pc.mode=liveModeName(); pc.hands=committedHands();
    pc.cardsSinceShuffle=cardsSinceShuffle; pc.totalCardsObserved=totalCardsObserved; pc.shuffleEvents=shuffleEvents;
    pc.low=lowSinceShuffle; pc.neutral=neutralSinceShuffle; pc.high=highSinceShuffle; pc.physicalCounts.putAll(observedPhysicalCounts);
    pc.statWatchingState=copyTrendState(liveStatWatchingState);
    pendingPreamble=pc;
    System.out.println("\nPreamble report written: "+f.toAbsolutePath());
    System.out.println("Preamble hands: "+pc.hands+" | cards: "+pc.totalCardsObserved+" | cards since latest observed shuffle/start: "+pc.cardsSinceShuffle);
    System.out.println("No ledger, shuffle-analysis status or plate status was created for the preamble.");
    System.out.println("The next formal session will retain the preamble shoe/card-validation context but start its own Hand 1 and £100 research frame.");
  }

  static void save()throws Exception{
    String stamp=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); ensureEvidenceDirectory(); Path f=evidenceFile("blackjack_live_session_"+stamp+".txt");
    List<String>r=new ArrayList<>(); r.add("DETERMINISTIC BLACKJACK - LIVE SESSION REPORT");
    r.add("SESSION NAME: "+sessionName);
    if(pendingPreamble!=null){
      r.add("PREAMBLE: YES");
      r.add("PREAMBLE_ID: "+pendingPreamble.id);
      r.add("PREAMBLE_SOURCE: "+pendingPreamble.file.toString());
      r.add("PREAMBLE_HANDS: "+pendingPreamble.hands);
      r.add("PREAMBLE_MODE: "+pendingPreamble.mode);
      r.add("PREAMBLE_CARDS_RECORDED: "+pendingPreamble.totalCardsObserved);
      r.add("PREAMBLE_SHUFFLES_OBSERVED: "+pendingPreamble.shuffleEvents);
      r.add("CARDS SINCE MOST RECENT OBSERVED SHUFFLE/PREAMBLE START AT FORMAL HAND 1: "+pendingPreamble.cardsSinceShuffle);
      r.add("BOUNDARY: preamble evidence is linked for chronology/visual context but excluded from this formal session's W/L/P, exposure, bankroll metrics, ledger and plate statistics.");
    } else r.add("PREAMBLE: NO");
    r.add("PLATFORM BALANCE BASIS: Start £"+m(sessionPlatformStart)+" | Session depletion floor £"+m(sessionPlatformOffset)+" = research-equivalent £0/depleted");
    r.add("Platform Final £"+m(bank)+" | Platform Peak £"+m(peak)+" | Platform Trough £"+m(trough)+" | MaxDD £"+m(maxDD)+" | Exposure £"+m(exposure));
    r.add("OFFSET RESEARCH-EQUIVALENT: Start £100.00 | Final £"+m(bank-sessionPlatformOffset)+" | Peak £"+m(peak-sessionPlatformOffset)+" | Trough £"+m(trough-sessionPlatformOffset));
    r.add("OFFSET RULE: research-equivalent bankroll = platform balance - session depletion floor £"+m(sessionPlatformOffset));
    r.add("AFFORDABILITY RULE: session-specific platform floor £"+m(sessionPlatformOffset)+" is protected; wager/double/split affordability uses research-equivalent bankroll only.");
    r.add("MODE: "+modeReportLabel()); r.add("W/L/P "+w+"/"+l+"/"+p); if(!personalMode){r.add("Wager match/above/below "+wagerMatch+"/"+wagerAbove+"/"+wagerBelow);r.add("Action match/different "+actionMatch+"/"+actionDiff);}
    if(hybridMode) r.add("HYBRID COUNTERFACTUAL RULE: observed bankroll/W/L/P are the operator's Hybrid journey; S separately replays the unchanged Frozen and Casual policies from the same formal starting cardstream when source permits.");
    r.add("SOURCE: 247blackjack.com");
    r.add("SOURCE TYPE: Online play-money blackjack, configured 6-deck game");
    r.add("CARD GENERATION: computational/RNG implementation; visible six-deck shuffle events recorded from interface");
    r.add("Shuffle events explicitly observed: "+shuffleEvents);
    r.add("Total cards recorded: "+totalCardsObserved);
    r.add("Cards since most recent observed shuffle/start: "+cardsSinceShuffle);
    r.add("Exact known rank+suit identities observed: "+observedPhysicalCounts.size());
    r.add("Validation warnings/overrides encountered: "+validationWarnings);
    r.add("Hands re-entered through amendment review: "+amendmentsMade);
    r.add("Decision provenance counts:");
    r.add("  TOTAL DECISION EVENTS: "+totalDecisionEvents()+" (decision events, not hands)");
    r.add("  TEXTBOOK / FROZEN-ALIGNED: "+textbookAlignedDecisions);
    r.add("  FROZEN RESEARCH OVERRIDE events: "+frozenResearchOverrideDecisions);
    r.add("  FROZEN PRIMARY HARD11-vs-10 events: "+frozenPrimarySpecialDecisions);
    r.add("Policy note: Pair20-vs-10 STAND is the current Frozen action; provenance records that it replaced the earlier research SPLIT rule, and it also agrees with textbook.");
    r.add("Policy note: HARD11-vs-10 primary remains DOUBLE; Cooling->HIT is a capital-protection alternate, not the primary action used here.");
    r.add("Shuffle log: "+(shuffleLog.isEmpty()?"none":String.join(" | ",shuffleLog)));
    r.add("Penetration is not inferred unless a visible shuffle boundary is recorded; card counts are observational.");
    r.add("Boundary: frozen wagers are historical reference wagers, not proven optimal stakes.");r.add("Controller note: exact mature BRAKE predicate is not invented; only explicit handover transitions are used.");
    r.add(""); r.add("PERSONAL-MODE HISTORY ACCESS: recent-table-hand history is hidden by default and, if requested, shown BEFORE the wager; matching-hand history is available only after the current starting cards are known and before the player action.");
    r.add("RECENT TABLE-HAND WINDOW: user-selectable 1-5 previously committed hands from the current session only; the request itself is retained as behavioural evidence and occurs before capital is committed.");
    r.add(""); r.add("OBSERVED INITIAL-HAND INDEX + HISTORICAL OUTCOMES (SUITS EXCLUDED)"); r.addAll(handSignatureLog); r.add(""); r.add("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)");
    r.add("Order rule observed on 247 animation: Player1 -> Dealer hole -> Player2 -> Dealer up -> subsequent player cards -> subsequent dealer draws.");
    r.add("Dealer hole cards are entered later when revealed but are retrospectively placed as the dealer's first dealt card, matching the observed 247 animation.");
    r.add("Split/post-deal cards are retained in operator-entry order; if platform split dealing order differs, that sub-order must be reviewed before deterministic replay.");
    r.addAll(chronologyLog);
    r.add("");r.add("AUDIT LOG");r.addAll(log);Files.write(f,r);
     LedgerTotals priorLedger=readLedgerTotals();
     double thisPL=(bank-sessionPlatformOffset)-100.0; int thisHands=committedHands();
     List<String> cumulative=new ArrayList<>();
     cumulative.add("\n================ SESSION "+stamp+" ================"); cumulative.addAll(r);
     cumulative.add(ledgerEntry(stamp)); cumulative.addAll(ledgerSummaryLines(priorLedger,thisPL,thisHands));
     List<String> observedEvidence;
     try{ observedEvidence=observedSourceEvidence(stamp,f); }catch(Exception ex){ observedEvidence=List.of("", "OBSERVED-SOURCE RECONSTRUCTION STATUS: NOT SOURCE-VERIFIED | "+ex.getMessage(), "Publication rule: Casual/comparison values remain N/A; do not infer them."); }
     cumulative.addAll(observedEvidence);
     Files.write(f,observedEvidence,StandardCharsets.UTF_8,StandardOpenOption.APPEND);
     cumulative.add("================ END SESSION "+stamp+" ================\n");
     cumulative.add("SHUFFLE_ANALYSIS_STATUS | session "+stamp+" | PENDING");
     cumulative.add("PLATE_STATUS | session "+stamp+" | PENDING");
     Files.write(CUMULATIVE_OUTPUT,cumulative,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
     if(pendingPreamble!=null){
       Files.write(pendingPreamble.file,List.of("LINKED_FORMAL_SESSION: "+stamp),StandardCharsets.UTF_8,StandardOpenOption.APPEND);
     }
     lastSavedSessionId=stamp; lastSavedReport=f;
     PreambleContext linkedPreamble=pendingPreamble;
     pendingPreamble=null;
     System.out.println("\nReport written: "+f.toAbsolutePath());
     if(linkedPreamble!=null) System.out.println("Linked preamble: "+linkedPreamble.file.toAbsolutePath()+" | "+linkedPreamble.hands+" hands");
     System.out.println("Cumulative project output updated: "+CUMULATIVE_OUTPUT.toAbsolutePath());
     System.out.println("Ledger updated: cumulative project P/L £"+m(priorLedger.cumulativePL+thisPL)+" | project-equivalent running balance £"+m(100.0+priorLedger.cumulativePL+thisPL));
  }

  // ==================== SELF-CONTAINED POST-SESSION SHUFFLE ENGINE ====================
  // Replay rules: 247 physical initial deal order P1 -> dealer hole -> P2 -> dealer up; clean peek; dealer stands on all 17s; blackjack 3:2; no insurance;
  // one split level; split aces receive one card; double receives one card.
  // Fixed Casual profile: £15 flat wager; conventional Hit/Stand; double hard 10/11
  // versus dealer 2-9 when affordable; split A-A and 8-8 when affordable.

  enum ShuffleMethod { A, B, C1, C2, C3, C4, D, E }
  static class ReplayDecision { final String key; final A action; ReplayDecision(String k,A a){key=k;action=a;} }
  static class ReplayResult {
    double bank=100,peak=100,trough=100,maxDD=0,exposure=0;
    int peakHand=0,troughHand=0,maxDDHand=0;
    int hands=0,w=0,l=0,p=0,cards=0; boolean reached200=false,sourceExhausted=false,reached130Replay=false;
    int textbookAligned=0,researchOverrides=0,primaryH11v10=0;
    State replayState=State.NORMAL; String reason="COMPLETED"; final List<ReplayDecision> decisions=new ArrayList<>();
  }
  static class PairDiff { int comparable=0,different=0; }
  static class TrendHand {
    final int outcome; // +1 win, 0 push, -1 loss
    final List<Card> visibleCards;
    final int dealerUp;
    final Integer revealedHoleValue;
    TrendHand(int outcome,List<Card> visibleCards,int dealerUp,Integer revealedHoleValue){this.outcome=outcome;this.visibleCards=visibleCards;this.dealerUp=dealerUp;this.revealedHoleValue=revealedHoleValue;}
  }
  static class TrendInfluence {
    final int hand; final String kind,detail;
    TrendInfluence(int hand,String kind,String detail){this.hand=hand;this.kind=kind;this.detail=detail;}
  }
  static class TrendState {
    final Deque<TrendHand> recent=new ArrayDeque<>();
    final Map<Integer,List<Integer>> revealedHoleByUp=new LinkedHashMap<>();
    final List<TrendInfluence> influences=new ArrayList<>();
    int lossStreak=0, winStreak=0, lastPressHand=-99;
    void observe(int outcome,List<Card> visible,int dealerUp,Integer revealedHole){
      List<Card> snap=copyCards(visible); recent.addLast(new TrendHand(outcome,snap,dealerUp,revealedHole)); while(recent.size()>5)recent.removeFirst();
      if(revealedHole!=null)revealedHoleByUp.computeIfAbsent(dealerUp,k->new ArrayList<>()).add(revealedHole);
      if(outcome<0){lossStreak++;winStreak=0;}else if(outcome>0){winStreak++;lossStreak=0;}else{lossStreak=winStreak=0;}
    }
  }
  static class StatWatchingResult extends ReplayResult {
    int chosenExitHand=0; double chosenExitBank=Double.NaN;
    final List<Double> path=new ArrayList<>(); final List<Integer> wagerMultipliers=new ArrayList<>(); final List<TrendInfluence> influences=new ArrayList<>();
    StatWatchingResult(){path.add(100.0);}
  }
  static class MethodAggregate {
    final ShuffleMethod method; final int reps; final List<Double> frozenFinal=new ArrayList<>(),casualFinal=new ArrayList<>();
    double frozenExposure=0,casualExposure=0,frozenDD=0,casualDD=0,pairedDelta=0; int frozenReach=0,casualReach=0,frozenComplete=0,casualComplete=0,bothComplete=0;
    long comparableActions=0,differentActions=0; MethodAggregate(ShuffleMethod m,int n){method=m;reps=n;}
  }
  interface DrawSource { void beforeHand(int hand)throws EOFException; Card draw()throws EOFException; void afterHand(); }
  static class ReplayShoe implements DrawSource {
    final ShuffleMethod method; final Random rng; final List<Card> original; List<Card> cards; int pos=0,segmentStart=0;
    ReplayShoe(ShuffleMethod m,List<Card> reservoir,long seed){method=m;rng=new Random(seed);original=copyCards(reservoir);cards=(m==ShuffleMethod.B)?fullSixDeck():copyCards(reservoir);if(m==ShuffleMethod.D)physicalShuffle(cards,rng);else Collections.shuffle(cards,rng);}
    public void beforeHand(int hand){
      if(method==ShuffleMethod.C1||method==ShuffleMethod.C2||method==ShuffleMethod.C3||method==ShuffleMethod.C4){
        double t=switch(method){case C1->.50;case C2->.65;case C3->.75;default->.80;};
        if(pos-segmentStart>=Math.ceil(original.size()*t)&&pos<cards.size()){Collections.shuffle(cards.subList(pos,cards.size()),rng);segmentStart=pos;}
      }
    }
    public Card draw()throws EOFException{if(pos>=cards.size())throw new EOFException("source exhausted");return cards.get(pos++);}
    public void afterHand(){if(method==ShuffleMethod.E){cards=copyCards(original);Collections.shuffle(cards,rng);pos=0;segmentStart=0;}}
  }
  static class ObservedStream {
    final List<List<Card>> segments=new ArrayList<>();
    final List<Integer> starts=new ArrayList<>();
    final Map<Integer,Integer> cardsPerHand=new LinkedHashMap<>();
    final Map<Integer,Double> settlementAdjustments=new LinkedHashMap<>();
  }
  static class LinkedPreambleEvidence {
    Path file; String id=""; int hands=0,cards=0,cardsAtFormalEntry=0,shuffles=0;
  }
  static class ExactObservedShoe implements DrawSource {
    final ObservedStream stream; int segment=-1,pos=0;
    ExactObservedShoe(ObservedStream s){stream=s;}
    public void beforeHand(int hand)throws EOFException{
      int wanted=-1; for(int i=0;i<stream.starts.size();i++)if(stream.starts.get(i)<=hand)wanted=i;
      if(wanted<0)throw new EOFException("no observed segment for hand "+hand);
      if(wanted!=segment){segment=wanted;pos=0;}
    }
    public Card draw()throws EOFException{
      if(segment<0||segment>=stream.segments.size()||pos>=stream.segments.get(segment).size())throw new EOFException("observed source exhausted");
      return stream.segments.get(segment).get(pos++);
    }
    public void afterHand(){}
  }

  static class HandCtx { double committed; HandCtx(double x){committed=x;} }
  static class PlayedHand { List<Card> cards; double stake; PlayedHand(List<Card> c,double s){cards=c;stake=s;} }

  static List<Card> copyCards(List<Card> src){List<Card>x=new ArrayList<>();for(Card c:src)x.add(new Card(c.rank,c.suit,c.value));return x;}
  static List<Card> fullSixDeck(){List<Card>x=new ArrayList<>();String[]su={"S","H","D","C"},ra={"A","2","3","4","5","6","7","8","9","10","J","Q","K"};for(int d=0;d<6;d++)for(String s:su)for(String r:ra){int v=r.equals("A")?11:(r.matches("10|J|Q|K")?10:Integer.parseInt(r));x.add(new Card(r,s,v));}return x;}
  static Card replayDraw(ReplayResult r,DrawSource s)throws EOFException{Card c=s.draw();r.cards++;return c;}

  // Declared physical-style mathematical proxy; not a proprietary casino-shuffler claim.
  static void physicalShuffle(List<Card> deck,Random rng){
    for(int cycle=0;cycle<3;cycle++){
      int cut=Math.max(1,Math.min(deck.size()-1,deck.size()/2+rng.nextInt(11)-5));List<Card>L=new ArrayList<>(deck.subList(0,cut)),R=new ArrayList<>(deck.subList(cut,deck.size())),mix=new ArrayList<>();int i=0,j=0;
      while(i<L.size()||j<R.size()){boolean left=i<L.size()&&(j>=R.size()||rng.nextBoolean());int packet=1+rng.nextInt(3);for(int k=0;k<packet;k++){if(left&&i<L.size())mix.add(L.get(i++));else if(!left&&j<R.size())mix.add(R.get(j++));else if(i<L.size())mix.add(L.get(i++));else if(j<R.size())mix.add(R.get(j++));}}
      deck.clear();deck.addAll(mix);List<Card>strip=new ArrayList<>();int at=0;while(at<deck.size()){int n=Math.min(deck.size()-at,2+rng.nextInt(5));strip.addAll(0,new ArrayList<>(deck.subList(at,at+n)));at+=n;}deck.clear();deck.addAll(strip);if(deck.size()>1)Collections.rotate(deck,-(1+rng.nextInt(deck.size()-1)));
    }
  }

  static A frozenReplayAction(List<Card>c,int up,boolean first,boolean pair,boolean canD){
    int t=total(c);if(t>=21)return A.STAND;if(pair){int q=c.get(0).value;if(q==10)return A.STAND;if(canD){if(q==11||q==8)return A.SPLIT;if(q==9&&in(up,2,3,4,5,6,8,9))return A.SPLIT;if(q==7&&between(up,2,7))return A.SPLIT;if(q==6&&between(up,2,6))return A.SPLIT;if(q==4&&(up==5||up==6))return A.SPLIT;if((q==3||q==2)&&between(up,2,7))return A.SPLIT;}}
    if(soft(c)){if(t>=19)return A.STAND;if(t==18){if(first&&canD&&between(up,3,6))return A.DOUBLE;if(up==2||up==7||up==8)return A.STAND;return A.HIT;}if(t==17){if(first&&canD&&between(up,3,6))return A.DOUBLE;return A.HIT;}if(t==15||t==16){if(first&&canD&&between(up,4,6))return A.DOUBLE;return A.HIT;}if(t==13||t==14){if(first&&canD&&(up==5||up==6))return A.DOUBLE;return A.HIT;}return A.HIT;}
    if(t>=17)return A.STAND;if(t>=13)return between(up,2,6)?A.STAND:A.HIT;if(t==12)return between(up,4,6)?A.STAND:A.HIT;if(t==11)return first&&canD&&between(up,2,10)?A.DOUBLE:A.HIT;if(t==10)return first&&canD&&between(up,2,9)?A.DOUBLE:A.HIT;if(t==9)return first&&canD&&between(up,3,6)?A.DOUBLE:A.HIT;return A.HIT;
  }
  static A casualReplayAction(List<Card>c,int up,boolean first,boolean pair,boolean canExtra){
    int t=total(c);if(t>=21)return A.STAND;if(first&&pair&&canExtra&&(c.get(0).value==11||c.get(0).value==8))return A.SPLIT;if(!soft(c)&&first&&canExtra&&(t==10||t==11)&&between(up,2,9))return A.DOUBLE;
    if(soft(c)){if(t>=19)return A.STAND;if(t==18&&(up==2||up==7||up==8))return A.STAND;return A.HIT;}if(t>=17)return A.STAND;if(t>=13)return between(up,2,6)?A.STAND:A.HIT;if(t==12)return between(up,4,6)?A.STAND:A.HIT;return A.HIT;
  }
  static void classifyReplayDecision(ReplayResult rr,List<Card> c,int up,boolean first,A actual,boolean frozen){
    int t=total(c);
    boolean hard11=!soft(c)&&t==11;
    if(first&&hard11&&up==10){rr.primaryH11v10++;return;}
    // Pair-20 vs 10 STAND is the documented research-override state, while also textbook-aligned.
    boolean pair20=first&&c.size()==2&&c.get(0).value==10&&c.get(1).value==10&&up==10;
    if(pair20&&actual==A.STAND&&frozen){rr.researchOverrides++;return;}
    A frozenAtState=frozenReplayAction(c,up,first,first&&c.size()==2&&c.get(0).value==c.get(1).value,true);
    if(actual==frozenAtState||!frozen)rr.textbookAligned++; else rr.researchOverrides++;
  }

  static String decisionKey(int hand,String branch,int ord,List<Card>c,int up){List<String>r=new ArrayList<>();for(Card q:c)r.add(q.rank);Collections.sort(r);return hand+"|"+branch+"|"+ord+"|"+String.join(",",r)+"|UP"+up;}

  static void playReplayBranch(ReplayResult rr,DrawSource shoe,List<PlayedHand> out,List<Card> cards,int up,double stake,boolean frozen,int hand,String branch,boolean allowSplit,HandCtx hc)throws EOFException{
    boolean first=true;int ord=0;
    while(true){
      if(total(cards)>21){out.add(new PlayedHand(cards,stake));return;}
      boolean pair=allowSplit&&first&&cards.size()==2&&cards.get(0).value==cards.get(1).value;boolean canExtra=rr.bank-hc.committed+0.001>=stake;
      A act=frozen?frozenReplayAction(cards,up,first,pair,canExtra):casualReplayAction(cards,up,first,pair,canExtra);
      classifyReplayDecision(rr,cards,up,first,act,frozen);
      rr.decisions.add(new ReplayDecision(decisionKey(hand,branch,ord++,cards,up),act));
      if(act==A.STAND){out.add(new PlayedHand(cards,stake));return;}
      if(act==A.DOUBLE&&canExtra){hc.committed+=stake;List<Card>d=new ArrayList<>(cards);d.add(replayDraw(rr,shoe));out.add(new PlayedHand(d,stake*2));return;}
      if(act==A.SPLIT&&pair&&canExtra){hc.committed+=stake;Card a=cards.get(0),b=cards.get(1);List<Card>x=new ArrayList<>(List.of(a,replayDraw(rr,shoe))),y=new ArrayList<>(List.of(b,replayDraw(rr,shoe)));if(a.value==11&&b.value==11){out.add(new PlayedHand(x,stake));out.add(new PlayedHand(y,stake));return;}playReplayBranch(rr,shoe,out,x,up,stake,frozen,hand,branch+"A",false,hc);playReplayBranch(rr,shoe,out,y,up,stake,frozen,hand,branch+"B",false,hc);return;}
      cards.add(replayDraw(rr,shoe));first=false;allowSplit=false;
    }
  }

  static void updateReplayState(ReplayResult r){if(r.replayState==State.BRAKE)return;if(r.replayState==State.COOLING){if(r.bank>=120)r.replayState=State.CAUTIOUS;return;}if(r.replayState==State.CAUTIOUS){if(r.bank>=140)r.replayState=State.NORMAL;return;}if(r.reached130Replay&&r.bank<=100)r.replayState=State.COOLING;}
  static double frozenReplayWager(ReplayResult r,int h){updateReplayState(r);if(r.replayState!=State.NORMAL)return 15;if(h<=8||r.bank<130)return 15;if(r.bank<150)return 20;if(r.bank<170)return 30;return 15;}

  static ReplayResult runReplay(List<Card> reservoir,ShuffleMethod method,long seed,boolean frozen){
    ReplayResult rr=new ReplayResult();ReplayShoe shoe=new ReplayShoe(method,reservoir,seed);
    try{
      for(int h=1;h<=30;h++){
        double base=frozen?frozenReplayWager(rr,h):15.0;if(rr.bank+0.001<15||rr.bank+0.001<base){rr.reason="BANKROLL";break;}shoe.beforeHand(h);
        Card p1=replayDraw(rr,shoe),hole=replayDraw(rr,shoe),p2=replayDraw(rr,shoe),up=replayDraw(rr,shoe);List<Card>player=new ArrayList<>(List.of(p1,p2)),dealer=new ArrayList<>(List.of(up,hole));double before=rr.bank;HandCtx hc=new HandCtx(base);List<PlayedHand>ph=new ArrayList<>();
        boolean pn=total(player)==21,dn=total(dealer)==21;double net=0;
        if(dn){net=pn?0:-base;ph.add(new PlayedHand(player,base));}
        else if(pn){net=1.5*base;ph.add(new PlayedHand(player,base));}
        else{
          playReplayBranch(rr,shoe,ph,player,up.value,base,frozen,h,"MAIN",true,hc);
          boolean anyLive=false;for(PlayedHand x:ph)if(total(x.cards)<=21)anyLive=true;
          if(anyLive)while(total(dealer)<17)dealer.add(replayDraw(rr,shoe));int dt=total(dealer);
          for(PlayedHand x:ph){int pt=total(x.cards);if(pt>21)net-=x.stake;else if(dt>21||pt>dt)net+=x.stake;else if(pt<dt)net-=x.stake;}
        }
        rr.exposure+=hc.committed;rr.bank+=net;rr.hands++;if(net>0)rr.w++;else if(net<0)rr.l++;else rr.p++;if(rr.bank>rr.peak){rr.peak=rr.bank;rr.peakHand=h;}if(rr.bank<rr.trough){rr.trough=rr.bank;rr.troughHand=h;}double dd=rr.peak-rr.bank;if(dd>rr.maxDD){rr.maxDD=dd;rr.maxDDHand=h;}if(rr.bank>=130)rr.reached130Replay=true;if(rr.bank>=200)rr.reached200=true;shoe.afterHand();
      }
      if(rr.hands==30)rr.reason="COMPLETED";
    }catch(EOFException e){rr.sourceExhausted=true;rr.reason="SOURCE_EXHAUSTED";}
    return rr;
  }

  static ReplayResult runObservedReplay(ObservedStream stream,boolean frozen){
    ReplayResult rr=new ReplayResult(); ExactObservedShoe shoe=new ExactObservedShoe(stream);
    try{
      for(int h=1;h<=30;h++){
        double base=frozen?frozenReplayWager(rr,h):15.0;
        if(rr.bank+0.001<15||rr.bank+0.001<base){rr.reason="BANKROLL";break;}
        shoe.beforeHand(h); int handStartPos=shoe.pos;
        Card p1=replayDraw(rr,shoe),hole=replayDraw(rr,shoe),p2=replayDraw(rr,shoe),up=replayDraw(rr,shoe);
        List<Card>player=new ArrayList<>(List.of(p1,p2)),dealer=new ArrayList<>(List.of(up,hole)); HandCtx hc=new HandCtx(base); List<PlayedHand>ph=new ArrayList<>();
        boolean pn=total(player)==21,dn=total(dealer)==21; double net=0;

        // Observed 247 timing is reconstructed as NO-PEEK: player decisions occur before
        // a dealer natural is established. This is intentionally separate from the A-E
        // synthetic engine, whose declared clean-peek architecture is unchanged.
        if(pn){
          ph.add(new PlayedHand(player,base));
          // 247 visibly completed the dealer hand after a player natural in Session 5.
          // Consume those cards so the next observed hand remains on the captured stream.
          while(total(dealer)<17)dealer.add(replayDraw(rr,shoe));
          net=dn?0:1.5*base;
        } else {
          playReplayBranch(rr,shoe,ph,player,up.value,base,frozen,h,"MAIN",true,hc);
          if(dn){
            // No-peek settlement: all stakes committed before the reveal are exposed.
            for(PlayedHand x:ph)net-=x.stake;
          } else {
            boolean anyLive=false;for(PlayedHand x:ph)if(total(x.cards)<=21)anyLive=true;
            if(anyLive)while(total(dealer)<17)dealer.add(replayDraw(rr,shoe)); int dt=total(dealer);
            for(PlayedHand x:ph){int pt=total(x.cards);if(pt>21)net-=x.stake;else if(dt>21||pt>dt)net+=x.stake;else if(pt<dt)net-=x.stake;}
          }
        }

        // Only the exact Frozen source-validation path may apply an explicitly captured
        // platform settlement override (e.g. Session 5 Hand 4 displayed £22 rather than
        // theoretical £22.50). Counterfactual Casual never inherits an observed outcome.
        if(frozen)net+=stream.settlementAdjustments.getOrDefault(h,0.0);

        // For Frozen source validation, consume any remaining cards explicitly recorded
        // for this observed hand (for example dealer play-out after a player natural).
        // This does not invent cards and is not applied to the Casual counterfactual.
        if(frozen){
          Integer expected=stream.cardsPerHand.get(h);
          if(expected!=null){int used=shoe.pos-handStartPos;while(used<expected){replayDraw(rr,shoe);used++;}}
        }

        rr.exposure+=hc.committed;rr.bank+=net;rr.hands++;if(net>0)rr.w++;else if(net<0)rr.l++;else rr.p++;
        if(rr.bank>rr.peak){rr.peak=rr.bank;rr.peakHand=h;}if(rr.bank<rr.trough){rr.trough=rr.bank;rr.troughHand=h;}double dd=rr.peak-rr.bank;if(dd>rr.maxDD){rr.maxDD=dd;rr.maxDDHand=h;}
        if(rr.bank>=130)rr.reached130Replay=true;if(rr.bank>=200)rr.reached200=true;shoe.afterHand();
      }
      if(rr.hands==30)rr.reason="COMPLETED";
    }catch(EOFException e){rr.sourceExhausted=true;rr.reason="SOURCE_EXHAUSTED";}
    return rr;
  }


  // Counterfactual replay from a captured source start. Unlike Frozen source-validation,
  // this never inherits observed settlement overrides and never forces replay consumption
  // to match the source hand's original card count. Frozen and Casual therefore consume
  // independently from the same exact captured chronology.
  static ReplayResult runObservedCounterfactual(ObservedStream stream,boolean frozen){
    ReplayResult rr=new ReplayResult(); ExactObservedShoe shoe=new ExactObservedShoe(stream);
    try{
      for(int h=1;h<=30;h++){
        double base=frozen?frozenReplayWager(rr,h):15.0;
        if(rr.bank+0.001<15||rr.bank+0.001<base){rr.reason="BANKROLL";break;}
        shoe.beforeHand(h);
        Card p1=replayDraw(rr,shoe),hole=replayDraw(rr,shoe),p2=replayDraw(rr,shoe),up=replayDraw(rr,shoe);
        List<Card>player=new ArrayList<>(List.of(p1,p2)),dealer=new ArrayList<>(List.of(up,hole)); HandCtx hc=new HandCtx(base); List<PlayedHand>ph=new ArrayList<>();
        boolean pn=total(player)==21,dn=total(dealer)==21; double net=0;

        // 247 observed timing boundary retained: no-peek player decisions precede dealer-natural resolution.
        if(pn){
          ph.add(new PlayedHand(player,base));
          while(total(dealer)<17)dealer.add(replayDraw(rr,shoe));
          net=dn?0:1.5*base;
        } else {
          playReplayBranch(rr,shoe,ph,player,up.value,base,frozen,h,"MAIN",true,hc);
          if(dn){
            for(PlayedHand x:ph)net-=x.stake;
          } else {
            boolean anyLive=false;for(PlayedHand x:ph)if(total(x.cards)<=21)anyLive=true;
            if(anyLive)while(total(dealer)<17)dealer.add(replayDraw(rr,shoe)); int dt=total(dealer);
            for(PlayedHand x:ph){int pt=total(x.cards);if(pt>21)net-=x.stake;else if(dt>21||pt>dt)net+=x.stake;else if(pt<dt)net-=x.stake;}
          }
        }

        rr.exposure+=hc.committed;rr.bank+=net;rr.hands++;if(net>0)rr.w++;else if(net<0)rr.l++;else rr.p++;
        if(rr.bank>rr.peak){rr.peak=rr.bank;rr.peakHand=h;}if(rr.bank<rr.trough){rr.trough=rr.bank;rr.troughHand=h;}double dd=rr.peak-rr.bank;if(dd>rr.maxDD){rr.maxDD=dd;rr.maxDDHand=h;}
        if(rr.bank>=130)rr.reached130Replay=true;if(rr.bank>=200)rr.reached200=true;shoe.afterHand();
      }
      if(rr.hands==30)rr.reason="COMPLETED";
    }catch(EOFException e){rr.sourceExhausted=true;rr.reason="SOURCE_EXHAUSTED";}
    return rr;
  }

  static int trendScore(TrendState st){
    if(st.recent.isEmpty())return 0;
    int low=0,ten=0,cards=0,wins=0,losses=0;
    for(TrendHand h:st.recent){
      if(h.outcome>0)wins++; else if(h.outcome<0)losses++;
      for(Card c:h.visibleCards){cards++; if(c.value>=2&&c.value<=6)low++; if(c.value==10)ten++;}
    }
    int score=0;
    if(cards>=8){double lp=(double)low/cards,tp=(double)ten/cards; if(lp>=.45)score++; if(tp<=.18)score+=2; if(tp>=.42)score-=2;}
    if(wins>=3)score++; if(losses>=3)score--; if(st.winStreak>=3)score++; if(st.lossStreak>=3)score--;
    return score;
  }

  static String trendSnapshotReason(TrendState st){
    int low=0,ten=0,cards=0,wins=0,losses=0;
    for(TrendHand h:st.recent){if(h.outcome>0)wins++;else if(h.outcome<0)losses++;for(Card c:h.visibleCards){cards++;if(c.value>=2&&c.value<=6)low++;if(c.value==10)ten++;}}
    double lp=cards==0?0:100.0*low/cards,tp=cards==0?0:100.0*ten/cards;
    return String.format(Locale.ROOT,"last %d hands: low cards %.0f%%, 10-value cards %.0f%%, W/L %d/%d",st.recent.size(),lp,tp,wins,losses);
  }

  static int statWatchingMultiplier(TrendState st,int hand,double bank){
    if(hand<=4||st.recent.size()<3||hand-st.lastPressHand<3)return 1;
    int sc=trendScore(st);
    int mult=sc>=6?4:sc>=5?3:sc>=3?2:1;
    // Human-like restraint: after a large press or when bankroll is under pressure, do not escalate beyond 2x.
    if(bank<75&&mult>2)mult=2;
    return mult;
  }

  static boolean statWatchingShouldExit(TrendState st,int completedHands,double bank,double peak){
    if(completedHands<8)return false;
    int sc=trendScore(st); double dd=peak-bank;
    if(bank>=160&&sc<=0)return true;                         // protect a sizeable win when the visible trend cools
    if(bank>=125&&sc<=-2&&st.lossStreak>=2)return true;     // ahead, but recent table evidence has turned against him
    if(bank<=85&&(sc<=-1||st.lossStreak>=3))return true;    // loss-control exit
    if(completedHands>=10&&dd>=45&&sc<1)return true;        // enough-is-enough after a meaningful give-back
    return false;
  }

  static int[] recentFiveVisibleCardMix(TrendState st){
    List<Card> all=new ArrayList<>();
    for(TrendHand h:st.recent)all.addAll(h.visibleCards);
    int from=Math.max(0,all.size()-5),ten=0,low=0,n=0;
    for(int i=from;i<all.size();i++){Card c=all.get(i);n++;if(c.value==10)ten++;if(c.value>=2&&c.value<=6)low++;}
    return new int[]{n,ten,low};
  }

  static A statWatchingAction(TrendState st,List<Card> c,int up,boolean first,boolean pair,boolean canExtra,int hand){
    A base=casualReplayAction(c,up,first,pair,canExtra);
    int[] mix=recentFiveVisibleCardMix(st); int n=mix[0],tens=mix[1]; int t=total(c);
    // Final narrow working-memory extension: the last five visible cards may create a simple
    // 10-value-card impression. An override requires an unusually conspicuous extreme (0/5 or 4+/5),
    // so ordinary Casual-style play remains the default rather than every observation becoming a rule.
    // This is deliberately not a count, shoe model or learning rule.
    if(first&&n==5&&!soft(c)&&t>=12&&t<=16&&up>=4&&up<=6&&base==A.HIT&&tens==0){
      st.influences.add(new TrendInfluence(hand,"TEN-MEMORY","stood on hard "+t+" vs "+up+" after only "+tens+" 10-value card"+(tens==1?"":"s")+" appeared in the five most recently remembered visible cards"));
      return A.STAND;
    }
    if(first&&n==5&&(t==10||t==11)&&base==A.DOUBLE&&tens>=4){
      st.influences.add(new TrendInfluence(hand,"TEN-MEMORY","declined a Double on "+t+" after "+tens+" of the five most recently remembered visible cards were 10-value; took the ordinary Hit instead"));
      return A.HIT;
    }
    // Existing deliberately narrow revealed-hole-card hunch remains independent.
    if(first&&!soft(c)&&base==A.HIT){
      List<Integer> seen=st.revealedHoleByUp.get(up);
      if(t>=12&&t<=16&&seen!=null&&seen.size()>=2){
        double avg=seen.stream().mapToInt(Integer::intValue).average().orElse(10);
        if(avg<=5.0){st.influences.add(new TrendInfluence(hand,"HUNCH","stood on hard "+t+" vs "+up+" after prior revealed hole cards behind this up-card averaged "+String.format(Locale.ROOT,"%.1f",avg)));return A.STAND;}
      }
    }
    return base;
  }

  static void playStatWatchingBranch(StatWatchingResult rr,TrendState st,DrawSource shoe,List<PlayedHand> out,List<Card> cards,int up,double stake,int hand,String branch,boolean allowSplit,HandCtx hc)throws EOFException{
    boolean first=true;int ord=0;
    while(true){
      if(total(cards)>21){out.add(new PlayedHand(cards,stake));return;}
      boolean pair=allowSplit&&first&&cards.size()==2&&cards.get(0).value==cards.get(1).value;boolean canExtra=rr.bank-hc.committed+0.001>=stake;
      A act=statWatchingAction(st,cards,up,first,pair,canExtra,hand); rr.decisions.add(new ReplayDecision(decisionKey(hand,branch,ord++,cards,up),act));
      if(act==A.STAND){out.add(new PlayedHand(cards,stake));return;}
      if(act==A.DOUBLE&&canExtra){hc.committed+=stake;List<Card>d=new ArrayList<>(cards);d.add(replayDraw(rr,shoe));out.add(new PlayedHand(d,stake*2));return;}
      if(act==A.SPLIT&&pair&&canExtra){hc.committed+=stake;Card a=cards.get(0),b=cards.get(1);List<Card>x=new ArrayList<>(List.of(a,replayDraw(rr,shoe))),y=new ArrayList<>(List.of(b,replayDraw(rr,shoe)));if(a.value==11&&b.value==11){out.add(new PlayedHand(x,stake));out.add(new PlayedHand(y,stake));return;}playStatWatchingBranch(rr,st,shoe,out,x,up,stake,hand,branch+"A",false,hc);playStatWatchingBranch(rr,st,shoe,out,y,up,stake,hand,branch+"B",false,hc);return;}
      cards.add(replayDraw(rr,shoe));first=false;allowSplit=false;
    }
  }

  static StatWatchingResult runStatWatchingCounterfactual(ObservedStream stream,boolean honourExit){
    StatWatchingResult rr=new StatWatchingResult(); TrendState st=new TrendState(); ExactObservedShoe shoe=new ExactObservedShoe(stream);
    try{
      for(int h=1;h<=30;h++){
        if(honourExit&&statWatchingShouldExit(st,rr.hands,rr.bank,rr.peak)){
          rr.chosenExitHand=rr.hands;rr.chosenExitBank=rr.bank;rr.reason="BEHAVIOURAL_EXIT";
          st.influences.add(new TrendInfluence(rr.hands,"EXIT","walked away at £"+m(rr.bank)+" • "+trendSnapshotReason(st)+" • drawdown from peak £"+m(rr.peak-rr.bank)));
          break;
        }
        int mult=statWatchingMultiplier(st,h,rr.bank);double base=15.0*mult;
        if(rr.bank+0.001<TABLE_MINIMUM){rr.reason="BANKROLL";break;}
        while(mult>1&&rr.bank+0.001<base){mult--;base=15.0*mult;}
        if(rr.bank+0.001<base){rr.reason="BANKROLL";break;}
        if(mult>1){st.lastPressHand=h;st.influences.add(new TrendInfluence(h,"WAGER",mult+"x wager (£"+m(base)+") • "+trendSnapshotReason(st)));}
        rr.wagerMultipliers.add(mult);shoe.beforeHand(h);
        Card p1=replayDraw(rr,shoe),hole=replayDraw(rr,shoe),p2=replayDraw(rr,shoe),up=replayDraw(rr,shoe);
        List<Card>player=new ArrayList<>(List.of(p1,p2)),dealer=new ArrayList<>(List.of(up,hole));HandCtx hc=new HandCtx(base);List<PlayedHand>ph=new ArrayList<>();boolean pn=total(player)==21,dn=total(dealer)==21;double net=0;boolean dealerRevealed=false;
        if(pn){ph.add(new PlayedHand(player,base));while(total(dealer)<17)dealer.add(replayDraw(rr,shoe));dealerRevealed=true;net=dn?0:1.5*base;}
        else{
          playStatWatchingBranch(rr,st,shoe,ph,player,up.value,base,h,"MAIN",true,hc);
          if(dn){dealerRevealed=true;for(PlayedHand x:ph)net-=x.stake;}
          else{boolean anyLive=false;for(PlayedHand x:ph)if(total(x.cards)<=21)anyLive=true;if(anyLive){while(total(dealer)<17)dealer.add(replayDraw(rr,shoe));dealerRevealed=true;}int dt=total(dealer);for(PlayedHand x:ph){int pt=total(x.cards);if(pt>21)net-=x.stake;else if(dt>21||pt>dt)net+=x.stake;else if(pt<dt)net-=x.stake;}}
        }
        rr.exposure+=hc.committed;rr.bank+=net;rr.hands++;if(net>0)rr.w++;else if(net<0)rr.l++;else rr.p++;if(rr.bank>rr.peak){rr.peak=rr.bank;rr.peakHand=h;}if(rr.bank<rr.trough){rr.trough=rr.bank;rr.troughHand=h;}double dd=rr.peak-rr.bank;if(dd>rr.maxDD){rr.maxDD=dd;rr.maxDDHand=h;}rr.path.add(rr.bank);
        List<Card>visible=new ArrayList<>();for(PlayedHand x:ph)visible.addAll(x.cards);visible.add(up);Integer revealedHole=null;if(dealerRevealed){for(int i=1;i<dealer.size();i++)visible.add(dealer.get(i));revealedHole=hole.value;}
        st.observe(net>0?1:net<0?-1:0,visible,up.value,revealedHole);shoe.afterHand();
      }
      if(rr.hands==30)rr.reason="COMPLETED";
    }catch(EOFException e){rr.sourceExhausted=true;rr.reason="SOURCE_EXHAUSTED";}
    rr.influences.addAll(st.influences);return rr;
  }

  static String pathLine(List<Double> p){StringBuilder b=new StringBuilder();for(int i=0;i<p.size();i++){if(i>0)b.append(';');b.append('H').append(i).append('=').append(m(p.get(i)));}return b.toString();}

  static List<String> statWatchingEvidence(String sid,Path formalSource)throws Exception{
    LinkedPreambleEvidence pe=formalSource.equals(CUMULATIVE_OUTPUT)?null:linkedPreambleEvidence(formalSource);
    ObservedStream os; String replayBasis;
    if(pe!=null){os=loadPreambleStartStream(pe,formalSource);replayBasis="PREAMBLE_START";}
    else {
      if(formalSource.equals(CUMULATIVE_OUTPUT))throw new IOException("dedicated chronological live-session report is not available");
      os=loadObservedStream(formalSource,sid);replayBasis="FORMAL_START";
    }
    StatWatchingResult chosen=runStatWatchingCounterfactual(os,true),stay=runStatWatchingCounterfactual(os,false);
    List<String>x=new ArrayList<>();x.add("");x.add("================ STAT-WATCHING CASUAL | SESSION "+sid+" ================");
    x.add("EXPERIMENT STATUS: EXPLORATORY COUNTERFACTUAL / DETERMINISTIC / QUARANTINED FROM FROZEN-vs-CASUAL SCORECARD");
    x.add("REPLAY BASIS: "+replayBasis);
    if(pe!=null)x.add("PREAMBLE CONTEXT: source "+pe.file+" | preamble hands "+pe.hands+" | preamble cards "+pe.cards+" | observed preamble shuffles "+pe.shuffles+" | starts from first recorded preamble card, matching the established preamble-start comparison basis.");
    x.add("VISIBLE-EVIDENCE RULE: wager and exit decisions use only completed prior-hand evidence visible to the player; no future cards, shoe composition, or unrevealed dealer hole card is consulted. A hole-card hunch may affect a current play decision only after the dealer up-card is visible and only from previously revealed hole-card history.");
    x.add("POLICY: ordinary wager £15; rare evidence-triggered 2x/3x/4x presses; rolling last-five-hand table impression; deterministic walk-away rule from Hand 8 onward. Existing Casual and Frozen policies are unchanged.");
    x.add("STAY POLICY: the stay-at-table counterfactual keeps the SAME Stat-Watching Casual personality, wager logic, hunch logic and play policy; only the behavioural walk-away trigger is disabled. It never switches to Frozen style.");
    x.add("Chosen behavioural path: hands "+chosen.hands+" | bankroll £"+m(chosen.bank)+" | W/L/P "+chosen.w+"/"+chosen.l+"/"+chosen.p+" | exposure £"+m(chosen.exposure)+" | termination "+chosen.reason+(chosen.chosenExitHand>0?" | chosen exit Hand "+chosen.chosenExitHand+" at £"+m(chosen.chosenExitBank):""));
    x.add("Stay-at-table counterfactual: hands "+stay.hands+" | bankroll £"+m(stay.bank)+" | W/L/P "+stay.w+"/"+stay.l+"/"+stay.p+" | exposure £"+m(stay.exposure)+" | termination "+stay.reason+" | "+(replaySourceComplete(stay)?"SOURCE-COMPLETE":"NOT SOURCE-COMPLETE"));
    if(chosen.chosenExitHand>0&&replaySourceComplete(stay))x.add("Exit value versus staying: "+(chosen.chosenExitBank-stay.bank>=0?"+":"")+"£"+m(chosen.chosenExitBank-stay.bank));
    else x.add("Exit value versus staying: N/A unless a behavioural exit occurs and the stay-at-table replay is source-complete.");
    x.add("PATH_CHOSEN: "+pathLine(chosen.path));x.add("PATH_STAY: "+pathLine(stay.path));
    int shown=0;for(TrendInfluence e:chosen.influences){x.add("INFLUENCE | H"+e.hand+" | "+e.kind+" | "+e.detail);if(++shown>=12)break;}
    if(shown==0)x.add("INFLUENCE | NONE | No wager press, hunch override, or behavioural exit trigger fired in the source-supported path.");
    x.add("Publication boundary: exploratory side experiment only. Do not award established Frozen/Casual scorecard percentiles or alter the main comparison from this block without a later declared research phase.");
    x.add("================ END STAT-WATCHING CASUAL | SESSION "+sid+" ================");return x;
  }

  static boolean hasStatWatchingBlock(String text,String sid){
    return text!=null&&text.contains("================ STAT-WATCHING CASUAL | SESSION "+sid+" ================");
  }

  static List<String> ledgerSessionIds()throws IOException{
    LinkedHashSet<String> ids=new LinkedHashSet<>();
    if(!Files.exists(CUMULATIVE_OUTPUT))return new ArrayList<>();
    for(String line:Files.readAllLines(CUMULATIVE_OUTPUT,StandardCharsets.UTF_8)){
      String t=line.trim(); if(!t.startsWith("LEDGER_ENTRY | session "))continue;
      String[]p=t.split("\\|"); if(p.length<2)continue;
      String sid=p[1].trim().replaceFirst("^session\\s+",""); if(!sid.isBlank())ids.add(sid);
    }
    return new ArrayList<>(ids);
  }

  static String latestFrozenReplayGate(String sid){
    try{
      if(!Files.exists(CUMULATIVE_OUTPUT))return "UNKNOWN";
      String marker="OBSERVED-SOURCE FROZEN vs CASUAL RECONSTRUCTION | SESSION "+sid.replace("_CORRECTED_247_DEAL_ORDER","").replace("_CORRECTED","");
      String status="UNKNOWN"; boolean in=false;
      for(String line:Files.readAllLines(CUMULATIVE_OUTPUT,StandardCharsets.UTF_8)){
        String t=line.trim();
        if(t.contains(marker)){in=true;continue;}
        if(in&&t.startsWith("Frozen replay gate:")){status=t.endsWith("PASS")||t.contains(" | PASS")?"PASS":(t.endsWith("FAIL")||t.contains(" | FAIL")?"FAIL":status);}
        if(in&&t.startsWith("================ END OBSERVED-SOURCE RECONSTRUCTION"))in=false;
      }
      return status;
    }catch(Exception ex){return "UNKNOWN";}
  }

  static void backfillHistoricalStatWatchingEvidence(String excludeSid){
    try{
      if(!Files.exists(CUMULATIVE_OUTPUT))return;
      String text=Files.readString(CUMULATIVE_OUTPUT,StandardCharsets.UTF_8);
      List<String> audit=new ArrayList<>();
      for(String sid:ledgerSessionIds()){
        if(sid.equals(excludeSid)||hasStatWatchingBlock(text,sid))continue;
        Path source=reportForSession(sid);
        try{
          String gate=latestFrozenReplayGate(sid);
          if(gate.equals("FAIL"))throw new IllegalStateException("latest observed-source Frozen replay gate is FAIL; exploratory historical continuation is withheld rather than crossing an unverified chronology");
          List<String> ev=statWatchingEvidence(sid,source);
          Files.write(CUMULATIVE_OUTPUT,ev,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
          ensureEvidenceDirectory();
          Files.write(evidenceFile("blackjack_stat_watching_"+sid+".txt"),ev,StandardCharsets.UTF_8);
          text+=String.join("\n",ev)+"\n"; audit.add("BACKFILLED "+sid+" from "+source);
        }catch(Exception ex){
          List<String> na=List.of("", "================ STAT-WATCHING CASUAL | SESSION "+sid+" ================",
            "EXPERIMENT STATUS: HISTORICAL BACKFILL NOT AVAILABLE",
            "REPLAY BASIS: UNAVAILABLE",
            "BACKFILL BOUNDARY: exact source-supported chronology could not be replayed safely: "+ex.getMessage(),
            "Publication boundary: do not infer an exploratory bankroll or walk-away result for this session.",
            "================ END STAT-WATCHING CASUAL | SESSION "+sid+" ================");
          Files.write(CUMULATIVE_OUTPUT,na,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
          text+=String.join("\n",na)+"\n"; audit.add("N/A "+sid+" | "+ex.getMessage());
        }
      }
      if(!audit.isEmpty()){
        ensureEvidenceDirectory();
        List<String> lines=new ArrayList<>(); lines.add("STAT-WATCHING CASUAL HISTORICAL BACKFILL | "+LocalDateTime.now()); lines.addAll(audit);
        Files.write(evidenceFile("blackjack_stat_watching_backfill_audit.txt"),lines,StandardCharsets.UTF_8);
        System.out.println("\nSTAT-WATCHING CASUAL historical backfill: "+audit.size()+" session record(s) processed.");
      }
    }catch(Exception ex){System.out.println("\nSTAT-WATCHING CASUAL historical backfill skipped safely: "+ex.getMessage());}
  }

  static PairDiff compareReplayDecisions(ReplayResult f,ReplayResult c){
    Map<String,A> fm=new LinkedHashMap<>();for(ReplayDecision d:f.decisions)fm.putIfAbsent(d.key,d.action);PairDiff pd=new PairDiff();for(ReplayDecision d:c.decisions){A x=fm.get(d.key);if(x!=null){pd.comparable++;if(x!=d.action)pd.different++;}}return pd;
  }
  static String methodDescription(ShuffleMethod m){return switch(m){case A->"Observed-card permutation: shuffle only the captured session-card reservoir; no cards added.";case B->"Full six-deck RNG: generate and uniformly shuffle a fresh 312-card six-deck shoe.";case C1->"Penetration 50%: captured reservoir; reshuffle only remaining cards between hands at 50%.";case C2->"Penetration 65%: captured reservoir; reshuffle only remaining cards between hands at 65%.";case C3->"Penetration 75%: captured reservoir; reshuffle only remaining cards between hands at 75%.";case C4->"Penetration 80%: captured reservoir; reshuffle only remaining cards between hands at 80%.";case D->"Physical-style proxy: captured reservoir with deterministic riffle/strip/cut-style mixing.";case E->"Automatic/continuous proxy: captured reservoir; after each complete hand, used cards return and the full reservoir is remixed.";};}
  static double mean(List<Double>x){double s=0;for(double v:x)s+=v;return x.isEmpty()?Double.NaN:s/x.size();}
  static double median(List<Double>x){if(x.isEmpty())return Double.NaN;List<Double>y=new ArrayList<>(x);Collections.sort(y);int n=y.size();return n%2==1?y.get(n/2):(y.get(n/2-1)+y.get(n/2))/2;}

  static MethodAggregate runMethod(List<Card> reservoir,ShuffleMethod method,int reps){
    MethodAggregate a=new MethodAggregate(method,reps);System.out.println("\n"+method+" - "+methodDescription(method));
    for(int i=0;i<reps;i++){
      long seed=SHUFFLE_SEED_BASE+1000003L*method.ordinal()+i;ReplayResult f=runReplay(reservoir,method,seed,true),c=runReplay(reservoir,method,seed,false);PairDiff d=compareReplayDecisions(f,c);
      a.frozenFinal.add(f.bank);a.casualFinal.add(c.bank);a.frozenExposure+=f.exposure;a.casualExposure+=c.exposure;a.frozenDD+=f.maxDD;a.casualDD+=c.maxDD;if(f.reached200)a.frozenReach++;if(c.reached200)a.casualReach++;if(f.hands==30)a.frozenComplete++;if(c.hands==30)a.casualComplete++;if(f.hands==30&&c.hands==30){a.bothComplete++;a.pairedDelta+=f.bank-c.bank;}a.comparableActions+=d.comparable;a.differentActions+=d.different;
    }
    System.out.println("Completed "+reps+" paired replays | mean comparable-action differences "+String.format(Locale.ROOT,"%.4f",(double)a.differentActions/reps));return a;
  }

  static String latestPendingSessionId(){
    if(!Files.exists(CUMULATIVE_OUTPUT))return null;
    try{
      LinkedHashMap<String,String> status=new LinkedHashMap<>();
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(!t.startsWith("SHUFFLE_ANALYSIS_STATUS | session "))continue;
        String[]p=t.split("\\|");
        if(p.length<3)continue;
        String sid=p[1].trim().replaceFirst("^session\\s+","");
        String st=p[2].trim();
        status.put(sid,st);
      }
      // Process the oldest unresolved session first so a deferred analysis cannot
      // become hidden by a newer live session.
      for(Map.Entry<String,String> e:status.entrySet())
        if(e.getValue().equals("PENDING")||e.getValue().startsWith("DEFERRED"))return e.getKey();
      return null;
    }catch(Exception e){return null;}
  }
  static boolean hasPendingShuffleAnalysis(){return latestPendingSessionId()!=null;}
  static void appendAnalysisDeferral()throws Exception{String sid=latestPendingSessionId();if(sid!=null)Files.writeString(CUMULATIVE_OUTPUT,"SHUFFLE_ANALYSIS_STATUS | session "+sid+" | DEFERRED_BY_OPERATOR\n",StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);}


  static int publicationSessionNumber(String sid){
    if(sid==null)return -1;
    String base=sid.replace("_CORRECTED_247_DEAL_ORDER","").replace("_CORRECTED","");
    if(base.equals("20260903_004704"))return 1;
    if(base.equals("20260903_221223"))return 2;
    if(base.equals("20260905_174453"))return 3;
    if(base.equals("20260906_163019"))return 4;
    if(!Files.exists(CUMULATIVE_OUTPUT))return -1;
    try{
      LinkedHashSet<String> afterSession4=new LinkedHashSet<>();
      boolean after=false;
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(!t.startsWith("LEDGER_ENTRY | session "))continue;
        String[]p=t.split("\\|"); if(p.length<2)continue;
        String found=p[1].trim().replaceFirst("^session\\s+","");
        String clean=found.replace("_CORRECTED_247_DEAL_ORDER","").replace("_CORRECTED","");
        if(clean.equals("20260906_163019")){after=true;continue;}
        if(after)afterSession4.add(found);
        if(found.equals(sid))break;
      }
      if(afterSession4.contains(sid))return 4+new ArrayList<>(afterSession4).indexOf(sid)+1;
    }catch(Exception ignored){}
    return -1;
  }

  static String sessionLabel(int n,String sid){return n>0?"SESSION "+n+" ("+sid+")":"session "+sid;}

  static void printUpcomingPublicationWorkflowReminder(String latestSid){
    int completedN=publicationSessionNumber(latestSid);
    int targetN=completedN>0?completedN+1:-1;
    System.out.println("PUBLICATION WORKFLOW REMINDER:");
    System.out.println();
    if(latestSid!=null)System.out.println("Latest completed live session in output.txt: "+sessionLabel(completedN,latestSid));
    if(targetN>0)System.out.println("Current / next live-session workflow: SESSION "+targetN+". Its publication plate will use Session "+completedN+" for immediate continuity and SESSION_EVIDENCE\\Sessionplate_structure.zip for structural precedent.");
    else System.out.println("The next completed live session will require its own publication plate before the following live session can start.");
    System.out.println("Provide ChatGPT with AFTER Session "+(targetN>0?targetN:"<N>")+" and its S analysis are complete:");
    System.out.println("  1. output.txt - latest cumulative project output");
    System.out.println("  2. SESSION_EVIDENCE\\blackjack_live_session_<SESSION_ID>.txt - dedicated Session "+(targetN>0?targetN:"<N>")+" live-session report");
    System.out.println("  3. SESSION_EVIDENCE\\blackjack_shuffle_analysis_<SESSION_ID>.txt - dedicated Session "+(targetN>0?targetN:"<N>")+" A-E shuffle-analysis report");
    System.out.println("  4. If PREAMBLE YES: the linked SESSION_EVIDENCE\\blackjack_preamble_<PREAMBLE_ID>.txt report");
    if(completedN>0)System.out.println("  5. Session "+completedN+" plate - immediately previous accepted plate / continuity reference");
    else System.out.println("  5. Previous Session plate - immediately previous accepted plate / continuity reference (NOT REQUIRED for Session 1, because no previous Session plate exists)");
    System.out.println("  6. SESSION_EVIDENCE\\Sessionplate_structure.zip - permanent structural reference pack (SESSION_PLATE_REQUEST_ADDITION.txt + Session7.png + Session8.png)");
    System.out.println();
    System.out.println("PRE-FLIGHT FILE COMPLETENESS HARD GATE:");
    System.out.println("  Before any plate calculation or rendering, ChatGPT must verify that every file required for this session is actually available and readable.");
    System.out.println("  Required every time: output.txt, dedicated live-session report, dedicated shuffle-analysis report, and SESSION_EVIDENCE\\Sessionplate_structure.zip. An immediately previous accepted plate is also required for Session 2 onward; Session 1 is the explicit exception because no previous Session plate exists.");
    System.out.println("  Required when applicable: linked preamble report when PREAMBLE YES; any dedicated observed-reconstruction correction/addendum explicitly produced for the session.");
    System.out.println("  If any required file is missing/unreadable, STOP. Reply to the end user with the exact missing filename(s). Do not render, infer, substitute, backfill, or silently use a different session/reference.");
    System.out.println();
    System.out.println("Then make this exact request:");
    System.out.println();
    if(targetN>0){
      System.out.println("\"Create the Session "+targetN+" plate. PREFLIGHT FILE COMPLETENESS HARD GATE: before any analysis, manifest calculation, or rendering, first verify that every required input for this specific session is actually available and readable. Required every time: output.txt; the dedicated Session "+targetN+" live-session report; the dedicated Session "+targetN+" shuffle-analysis report; the immediately previous accepted Session "+completedN+" plate; and SESSION_EVIDENCE\\Sessionplate_structure.zip. Required when applicable: the linked dedicated preamble report when the current evidence states PREAMBLE YES, and any dedicated observed-reconstruction correction/addendum explicitly produced for this session. If any required file is missing or unreadable, STOP and reply to the end user with an explicit list of the missing filename(s); do not create or partially create the plate, do not infer their contents, do not substitute a different/older file, and do not rely on memory of a prior chat. Only continue once the complete required set is available. Use the attached Session "+completedN+" plate for immediate publication continuity, and BEFORE deciding the plate architecture inspect SESSION_EVIDENCE\\Sessionplate_structure.zip. That permanent reference pack contains SESSION_PLATE_REQUEST_ADDITION.txt plus canonical Session7.png and Session8.png examples. Follow the TXT instructions. Session 8 is the current master visual/style reference; Session 7 is the canonical no-preamble/started-within-existing-shoe middle-section precedent; Session 8 is the canonical observed-preamble/entry-context middle-section precedent. Do not assume the new session matches either precedent until the current evidence is inspected. If the new session introduces a genuinely different entry/evidence scenario, preserve the established Session 8 visual family and construct an evidence-appropriate middle module rather than forcing or inventing content. Treat all reference-pack plates and, when one exists, the previous accepted plate as structural/presentation evidence only; current-session evidence remains the scientific authority. Use output.txt, the dedicated Session "+targetN+" live-session report, and the dedicated Session "+targetN+" shuffle-analysis report as the authoritative evidence sources; if a dedicated observed-reconstruction correction/addendum exists for this session, include it as an additional authoritative source. If this session has a linked preamble, also use the dedicated preamble report and the preamble-start Frozen/Casual comparison produced by S as authoritative sources for preamble-specific evidence only. Keep the observed formal Session "+targetN+" journey and its metrics strictly separate from the preamble. The preamble must not be included in Session "+targetN+" hand count, bankroll path, exposure, W/L/P, drawdown, decision totals, scorecard calculations, or other formal-session metrics. Report the preamble only as entry/shoe context and, where source-verified, show the Frozen-from-preamble and Casual-from-preamble outcomes as a separate contextual comparison. SESSION PREAMBLE DISPLAY REQUIREMENT: when a linked preamble exists, the plate MUST contain a clearly separated, visibly located PREAMBLE / ENTRY CONTEXT section in the main plate body; it must not be relegated only to Key Findings, Important Notes, or prose. That section must state the observed preamble hand count and card count and must display the S preamble-start Frozen and Casual replay outcomes exactly as supported by the dedicated shuffle-analysis evidence. These preamble-start outcomes must not be replaced by N/A merely because the ordinary formal-session Casual reconstruction is unavailable. Clearly distinguish three separate evidence views: (1) the observed formal Session journey, (2) the ordinary formal-session Casual reconstruction status, and (3) the separate Frozen-from-preamble-start and Casual-from-preamble-start counterfactual results. If a preamble-start replay terminates under a valid bankroll/table-minimum/depletion rule and the source reports SOURCE-COMPLETE, label that replay completed/source-complete as supported. If a preamble-start replay terminates because captured cards are exhausted, display the last source-supported bankroll and other supported values but explicitly label SOURCE_EXHAUSTED / NOT SOURCE-COMPLETE; never present that bankroll as a completed journey. HYBRID SESSION DISPLAY REQUIREMENT: when the formal observed mode is HYBRID, the plate MUST clearly label the observed journey as HYBRID and keep its actual bankroll, W/L/P, exposure, wagers, actions and completion status authoritative for what was played. The plate MUST also contain a clearly separated HYBRID FORMAL-START COUNTERFACTUAL section using the dedicated S evidence, showing the unchanged Frozen-policy replay and fixed Casual-policy replay from the same captured formal-session starting cardstream. Do not relabel the observed Hybrid journey as Frozen. A SOURCE-COMPLETE counterfactual may publish its final bankroll as the outcome. If a Frozen or Casual counterfactual is NOT SOURCE-COMPLETE or SOURCE_EXHAUSTED, show outcome N/A and retain any bankroll only as the last source-supported bankroll; never present it as a completed result. Keep this Hybrid formal-start comparison distinct from any PREAMBLE / ENTRY CONTEXT comparison. COMPLETION-LABEL CHECK: do not append partial/incomplete to any observed or reconstructed hand count, W/L/P, exposure, bankroll, drawdown, wager, or provenance metric merely because fewer than 30 hands were played. Partial/incomplete is reserved only for genuinely unfinished evidence or reconstruction. Never infer absolute shoe penetration unless an observed shuffle establishes the shoe origin; otherwise report only the observed cards since the latest observed shuffle/start. Before rendering the plate, independently parse and recalculate every mechanically verifiable metric from those sources using executable code (for example Python), including the bankroll path, peak/trough and their hand indexes, maximum drawdown, exposure totals, W/L/P totals, wager counts, decision/provenance sums, card/shuffle counts, reconstruction arithmetic, and every A-E shuffle-table value. Freeze those checks into a pre-render verified manifest and use that manifest as the numeric input to the plate. Treat any applicable previous Session plate and all plates inside Sessionplate_structure.zip as presentation/structural evidence only: never copy, infer, backfill, or transplant a numeric value, zero, N/A, completion count, score, or session-specific statement from them. Use SESSION_PLATE_REQUEST_ADDITION.txt to choose the evidence-appropriate structural variant before rendering. ZERO IS DATA: every displayed zero or N/A must be supported by the current Session evidence; never insert zero or N/A merely to complete the layout. Where current authoritative sources explicitly report a metric as unavailable, incomplete, not source-verified, or N/A, preserve that boundary and do not infer or transplant a value from another session. For any derived scorecard value, recalculate it only from its frozen documented equation/reference definition; if that definition is not present in the supplied authoritative evidence, do not invent or reverse-engineer the score and instead flag that item as requiring the frozen scorecard reference before completion. Treat Risk and Journey Intensity as synthetic-reference profile scores, not real-casino population percentiles, predictions, or guarantees. PUBLICATION LEGIBILITY REQUIREMENT: every plate must remain comfortably readable at normal full-plate viewing size, including dense lower tables, decimal points, currency symbols, percentages, footnotes and multi-column values. Improve font weight, font size, spacing, alignment or rendering clarity where needed while preserving the accepted plate structure, headings and evidence values exactly; legibility work must never silently alter, round, reinterpret or replace evidence. FINAL VISUAL CHECK: before accepting a rendered plate, inspect every dense table for ambiguous decimal points, cramped values, clipped text, weak contrast and column misalignment; a value being technically present is not sufficient if it is difficult to read. FUTURE-PROOF SUPPORTING-PLATE RULE: universal reference/supporting plates must use wording such as Applies to all Session Scorecards and must not hard-code a current session range unless the plate specifically reports a bounded session set. The universal How to Read the Session Scorecards field guide and the separate Shuffle Robustness Table - Field Guide are publication reference aids; they explain terminology and columns but must not replace or modify current-session evidence. Once the Session "+targetN+" plate has been created, perform a post-render plate-to-manifest audit of every displayed number and substantive label against both the manifest and the authoritative evidence. Do not accept or mark the Session "+targetN+" plate COMPLETE until this verification has passed. Do not present the generated plate as publication-ready, verified, accepted, or COMPLETE until the post-render audit has actually been performed against the frozen pre-render manifest and authoritative evidence; rendering the plate is not completion. If any discrepancy is found, the plate remains PENDING until corrected and re-audited. If further modifications are required, audit the plate before and after each change and confirm that no unrelated data, wording, labels, or layout have changed. If making the requested modification would risk compromising the plate, provide only the specific segment that requires updating so it can be edited manually. For Session 2 onward, make the PNG visually similar to the uploaded previous-session plate, using that plate strictly as presentation/layout evidence only; for Session 1, use Sessionplate_structure.zip as the visual/layout precedent because no previous-session plate exists. Do not copy any reference-session numeric or session-specific content. A legitimate observed or reconstructed journey that terminates under a declared bankroll, table-minimum, depletion, or other valid completion rule is a COMPLETED journey and must not be labelled partial merely because it contains fewer than 30 hands; use partial/incomplete only when the evidence or reconstruction itself is genuinely unfinished. After all plate/code/correction work is finished, package every file that actually needs to be placed into the working blackjack environment into one ZIP using the exact final production filenames so no manual renaming is required; do not include presentation-only artifacts such as the PNG unless they are actually required by the runtime environment. FINAL HANDOVER AUDIT: immediately before handing over any PNG, Java code, correction/addendum, manifest, audit file, or environment ZIP, perform one final audit of the complete deliverable set. Re-check every displayed plate value and substantive label against the frozen manifest and authoritative current-session evidence; explicitly verify that legitimate completed journeys are not labelled partial; verify the Java compiles; verify every environment-required file is present under its exact production filename; and verify the ZIP contains those exact final audited files. Do not hand over or describe the deliverable set as final, verified, accepted, COMPLETE, or environment-ready until this final handover audit has passed.\"");
    }else{
      System.out.println("Create the next Session plate using the previous accepted Session plate for immediate continuity and SESSION_EVIDENCE\\Sessionplate_structure.zip for structural precedent; inspect the reference pack before deciding architecture, and use current-session evidence as the scientific authority.");
    }
    System.out.println();
    System.out.println("END OF PUBLICATION WORKFLOW REMINDER.");
  }

  static void printPublicationWorkflowReminder(String sid){
    int n=publicationSessionNumber(sid);
    System.out.println("PUBLICATION WORKFLOW REMINDER:");
    System.out.println();
    if(sid!=null)System.out.println("Latest completed live session in output.txt: "+sessionLabel(n,sid));
    if(n>0)System.out.println("Before another live session, the Session "+n+" publication plate must be created and checked after S analysis is complete.");
    else System.out.println("After a live session, complete S analysis and then create/check its publication plate before starting another live session.");
    System.out.println("Provide ChatGPT with:");
    System.out.println("  1. output.txt - latest cumulative project output");
    if(sid!=null)System.out.println("  2. SESSION_EVIDENCE\\blackjack_live_session_"+sid+".txt - latest dedicated live-session report");
    else System.out.println("  2. SESSION_EVIDENCE\\blackjack_live_session_<SESSION_ID>.txt - latest dedicated live-session report");
    if(sid!=null)System.out.println("  3. SESSION_EVIDENCE\\blackjack_shuffle_analysis_"+sid+".txt - dedicated shuffle-analysis report");
    else System.out.println("  3. SESSION_EVIDENCE\\blackjack_shuffle_analysis_<SESSION_ID>.txt - dedicated shuffle-analysis report");
    System.out.println("  4. If PREAMBLE YES: the linked SESSION_EVIDENCE\\blackjack_preamble_<PREAMBLE_ID>.txt report");
    if(n>1)System.out.println("  5. Session "+(n-1)+" plate - immediately previous accepted plate / continuity reference");
    else System.out.println("  5. Session 1 exception: no previous Session plate exists, so no previous-plate file is required; use SESSION_EVIDENCE\\Sessionplate_structure.zip for structural/presentation precedent");
    System.out.println("  6. SESSION_EVIDENCE\\Sessionplate_structure.zip - permanent structural reference pack (SESSION_PLATE_REQUEST_ADDITION.txt + Session7.png + Session8.png)");
    System.out.println();
    System.out.println("PRE-FLIGHT FILE COMPLETENESS HARD GATE:");
    System.out.println("  Required every session: output.txt, dedicated live-session report, dedicated shuffle-analysis report, and SESSION_EVIDENCE\\Sessionplate_structure.zip.");
    if(n>1)System.out.println("  Session "+n+" also requires the immediately previous accepted Session "+(n-1)+" plate for publication continuity.");
    else if(n==1)System.out.println("  SESSION 1 EXCEPTION: no previous Session plate exists. Do NOT stop or ask for one; Sessionplate_structure.zip supplies the canonical structural/presentation precedent.");
    else System.out.println("  Previous accepted plate is required only when the target is Session 2 or later; Session 1 has no previous-plate requirement.");
    System.out.println("  Required when applicable: linked preamble report when PREAMBLE YES; any dedicated observed-reconstruction correction/addendum explicitly produced for the session.");
    System.out.println("  If any genuinely required file is missing/unreadable, STOP and tell the end user the exact missing filename(s); never invent a nonexistent Session 0/previous plate for Session 1.");
    System.out.println();
    System.out.println("Then make this exact request:");
    System.out.println();
    if(n>0){
      String continuityRequirement = n==1
          ? "SESSION 1 EXCEPTION: there is no immediately previous Session plate, so a previous-plate file is NOT required and its absence must NOT trigger the preflight stop. Do not request, infer, invent, or substitute a Session 0 plate. Use SESSION_EVIDENCE\\Sessionplate_structure.zip as the canonical structural/presentation precedent. "
          : "The immediately previous accepted Session "+(n-1)+" plate is required for immediate publication continuity. Use it only as presentation/continuity evidence. ";
      String previousPlateWording = n==1
          ? "Because this is Session 1, references below to a previous accepted Session plate are not applicable; Sessionplate_structure.zip alone supplies structural precedent. "
          : "Treat the previous accepted Session "+(n-1)+" plate as presentation/continuity evidence only. ";
      System.out.println("\"Create the Session "+n+" plate. PREFLIGHT FILE COMPLETENESS HARD GATE: before any analysis, manifest calculation, or rendering, verify output.txt, the dedicated Session "+n+" live-session report, the dedicated Session "+n+" shuffle-analysis report, and SESSION_EVIDENCE\\Sessionplate_structure.zip are available and readable. "+continuityRequirement+"Required when applicable: the linked dedicated preamble report when the current evidence states PREAMBLE YES, and any dedicated observed-reconstruction correction/addendum explicitly produced for this session. If any genuinely required file is missing or unreadable, STOP and reply to the end user with the exact missing filename(s); do not partially create the plate, infer contents, substitute a different/older file, or rely on prior-chat memory. "+previousPlateWording+"BEFORE deciding the plate architecture inspect SESSION_EVIDENCE\\Sessionplate_structure.zip. That permanent reference pack contains SESSION_PLATE_REQUEST_ADDITION.txt plus canonical Session7.png and Session8.png examples. Follow the TXT instructions. Session 8 is the current master visual/style reference; Session 7 is the canonical no-preamble/started-within-existing-shoe middle-section precedent; Session 8 is the canonical observed-preamble/entry-context middle-section precedent. Do not assume the new session matches either precedent until the current evidence is inspected. If the new session introduces a genuinely different entry/evidence scenario, preserve the established Session 8 visual family and construct an evidence-appropriate middle module rather than forcing or inventing content. Treat all reference-pack plates and, when one exists, the previous accepted plate as structural/presentation evidence only; current-session evidence remains the scientific authority. Use output.txt, the dedicated Session "+n+" live-session report, and the dedicated Session "+n+" shuffle-analysis report as the authoritative evidence sources; if a dedicated observed-reconstruction correction/addendum exists for this session, include it as an additional authoritative source. If this session has a linked preamble, also use the dedicated preamble report and the preamble-start Frozen/Casual comparison produced by S as authoritative sources for preamble-specific evidence only. Keep the observed formal Session "+n+" journey and its metrics strictly separate from the preamble. The preamble must not be included in Session "+n+" hand count, bankroll path, exposure, W/L/P, drawdown, decision totals, scorecard calculations, or other formal-session metrics. Report the preamble only as entry/shoe context and, where source-verified, show the Frozen-from-preamble and Casual-from-preamble outcomes as a separate contextual comparison. SESSION PREAMBLE DISPLAY REQUIREMENT: when a linked preamble exists, the plate MUST contain a clearly separated, visibly located PREAMBLE / ENTRY CONTEXT section in the main plate body; it must not be relegated only to Key Findings, Important Notes, or prose. That section must state the observed preamble hand count and card count and must display the S preamble-start Frozen and Casual replay outcomes exactly as supported by the dedicated shuffle-analysis evidence. These preamble-start outcomes must not be replaced by N/A merely because the ordinary formal-session Casual reconstruction is unavailable. Clearly distinguish three separate evidence views: (1) the observed formal Session journey, (2) the ordinary formal-session Casual reconstruction status, and (3) the separate Frozen-from-preamble-start and Casual-from-preamble-start counterfactual results. If a preamble-start replay terminates under a valid bankroll/table-minimum/depletion rule and the source reports SOURCE-COMPLETE, label that replay completed/source-complete as supported. If a preamble-start replay terminates because captured cards are exhausted, display the last source-supported bankroll and other supported values but explicitly label SOURCE_EXHAUSTED / NOT SOURCE-COMPLETE; never present that bankroll as a completed journey. HYBRID SESSION DISPLAY REQUIREMENT: when the formal observed mode is HYBRID, the plate MUST clearly label the observed journey as HYBRID and keep its actual bankroll, W/L/P, exposure, wagers, actions and completion status authoritative for what was played. The plate MUST also contain a clearly separated HYBRID FORMAL-START COUNTERFACTUAL section using the dedicated S evidence, showing the unchanged Frozen-policy replay and fixed Casual-policy replay from the same captured formal-session starting cardstream. Do not relabel the observed Hybrid journey as Frozen. A SOURCE-COMPLETE counterfactual may publish its final bankroll as the outcome. If a Frozen or Casual counterfactual is NOT SOURCE-COMPLETE or SOURCE_EXHAUSTED, show outcome N/A and retain any bankroll only as the last source-supported bankroll; never present it as a completed result. Keep this Hybrid formal-start comparison distinct from any PREAMBLE / ENTRY CONTEXT comparison. COMPLETION-LABEL CHECK: do not append partial/incomplete to any observed or reconstructed hand count, W/L/P, exposure, bankroll, drawdown, wager, or provenance metric merely because fewer than 30 hands were played. Partial/incomplete is reserved only for genuinely unfinished evidence or reconstruction. Never infer absolute shoe penetration unless an observed shuffle establishes the shoe origin; otherwise report only the observed cards since the latest observed shuffle/start. Before rendering the plate, independently parse and recalculate every mechanically verifiable metric from those sources using executable code (for example Python), including the bankroll path, peak/trough and their hand indexes, maximum drawdown, exposure totals, W/L/P totals, wager counts, decision/provenance sums, card/shuffle counts, reconstruction arithmetic, and every A-E shuffle-table value. Freeze those checks into a pre-render verified manifest and use that manifest as the numeric input to the plate. Treat any applicable previous Session plate and all plates inside Sessionplate_structure.zip as presentation/structural evidence only: never copy, infer, backfill, or transplant a numeric value, zero, N/A, completion count, score, or session-specific statement from them. Use SESSION_PLATE_REQUEST_ADDITION.txt to choose the evidence-appropriate structural variant before rendering. ZERO IS DATA: every displayed zero or N/A must be supported by the current Session evidence; never insert zero or N/A merely to complete the layout. Where current authoritative sources explicitly report a metric as unavailable, incomplete, not source-verified, or N/A, preserve that boundary and do not infer or transplant a value from another session. For any derived scorecard value, recalculate it only from its frozen documented equation/reference definition; if that definition is not present in the supplied authoritative evidence, do not invent or reverse-engineer the score and instead flag that item as requiring the frozen scorecard reference before completion. Treat Risk and Journey Intensity as synthetic-reference profile scores, not real-casino population percentiles, predictions, or guarantees. PUBLICATION LEGIBILITY REQUIREMENT: every plate must remain comfortably readable at normal full-plate viewing size, including dense lower tables, decimal points, currency symbols, percentages, footnotes and multi-column values. Improve font weight, font size, spacing, alignment or rendering clarity where needed while preserving the accepted plate structure, headings and evidence values exactly; legibility work must never silently alter, round, reinterpret or replace evidence. FINAL VISUAL CHECK: before accepting a rendered plate, inspect every dense table for ambiguous decimal points, cramped values, clipped text, weak contrast and column misalignment; a value being technically present is not sufficient if it is difficult to read. FUTURE-PROOF SUPPORTING-PLATE RULE: universal reference/supporting plates must use wording such as Applies to all Session Scorecards and must not hard-code a current session range unless the plate specifically reports a bounded session set. The universal How to Read the Session Scorecards field guide and the separate Shuffle Robustness Table - Field Guide are publication reference aids; they explain terminology and columns but must not replace or modify current-session evidence. Once the Session "+n+" plate has been created, perform a post-render plate-to-manifest audit of every displayed number and substantive label against both the manifest and the authoritative evidence. Do not accept or mark the Session "+n+" plate COMPLETE until this verification has passed. Do not present the generated plate as publication-ready, verified, accepted, or COMPLETE until the post-render audit has actually been performed against the frozen pre-render manifest and authoritative evidence; rendering the plate is not completion. If any discrepancy is found, the plate remains PENDING until corrected and re-audited. If further modifications are required, audit the plate before and after each change and confirm that no unrelated data, wording, labels, or layout have changed. If making the requested modification would risk compromising the plate, provide only the specific segment that requires updating so it can be edited manually. For Session 2 onward, make the PNG visually similar to the uploaded previous-session plate, using that plate strictly as presentation/layout evidence only; for Session 1, use Sessionplate_structure.zip as the visual/layout precedent because no previous-session plate exists. Do not copy any reference-session numeric or session-specific content. A legitimate observed or reconstructed journey that terminates under a declared bankroll, table-minimum, depletion, or other valid completion rule is a COMPLETED journey and must not be labelled partial merely because it contains fewer than 30 hands; use partial/incomplete only when the evidence or reconstruction itself is genuinely unfinished. After all plate/code/correction work is finished, package every file that actually needs to be placed into the working blackjack environment into one ZIP using the exact final production filenames so no manual renaming is required; do not include presentation-only artifacts such as the PNG unless they are actually required by the runtime environment. FINAL HANDOVER AUDIT: immediately before handing over any PNG, Java code, correction/addendum, manifest, audit file, or environment ZIP, perform one final audit of the complete deliverable set. Re-check every displayed plate value and substantive label against the frozen manifest and authoritative current-session evidence; explicitly verify that legitimate completed journeys are not labelled partial; verify the Java compiles; verify every environment-required file is present under its exact production filename; and verify the ZIP contains those exact final audited files. Do not hand over or describe the deliverable set as final, verified, accepted, COMPLETE, or environment-ready until this final handover audit has passed.\"");
    }else{
      System.out.println("Create the current Session plate only after a PREFLIGHT FILE COMPLETENESS HARD GATE confirms output.txt, the dedicated live-session report, dedicated shuffle-analysis report, SESSION_EVIDENCE\\Sessionplate_structure.zip, and any applicable linked preamble/correction files are all available and readable. For Session 2 onward, the immediately previous accepted Session plate is additionally required for continuity. SESSION 1 EXCEPTION: no previous Session plate exists, so do not stop or ask for one; use Sessionplate_structure.zip as the structural/presentation precedent. If anything genuinely required is missing, STOP and tell the end user exactly which filename(s) are missing; do not infer or substitute. Inspect the reference pack before deciding architecture and preserve every explicit N/A/not-source-verified boundary.");
    }
    System.out.println();
    System.out.println("END OF PUBLICATION WORKFLOW REMINDER.");
  }

  static String latestLiveSessionId(){
    if(!Files.exists(CUMULATIVE_OUTPUT))return null;
    try{
      String latest=null;
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(!t.startsWith("LEDGER_ENTRY | session "))continue;
        String[]p=t.split("\\|");
        if(p.length<2)continue;
        latest=p[1].trim().replaceFirst("^session\\s+","");
      }
      return latest;
    }catch(Exception e){return null;}
  }

  static boolean establishedPublishedPlateBaseline(String sid){
    if(sid==null)return false;
    String clean=sid.replace("_CORRECTED_247_DEAL_ORDER","").replace("_CORRECTED","");
    return clean.equals("20260903_004704") ||
           clean.equals("20260903_221223") ||
           clean.equals("20260905_174453") ||
           clean.equals("20260906_163019");
  }

  static String plateStatusForSession(String sid){
    if(sid==null)return null;
    String status=null;
    if(Files.exists(CUMULATIVE_OUTPUT)){
      try{
        for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
          String t=x.trim();
          if(!t.startsWith("PLATE_STATUS | session "))continue;
          String[]p=t.split("\\|");
          if(p.length<3)continue;
          String found=p[1].trim().replaceFirst("^session\\s+","");
          if(found.equals(sid)) status=p[2].trim();
        }
      }catch(Exception ignored){}
    }
    // Migration baseline: Sessions 1-4 were already published and checked before
    // the persistent PLATE_STATUS gate was introduced. Do not reopen those plates
    // merely because an older output.txt contains PENDING or no COMPLETE marker.
    if(establishedPublishedPlateBaseline(sid))return "COMPLETE";
    return status;
  }

  static void appendPlateStatus(String sid,String status)throws Exception{
    if(sid==null)return;
    String ts=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    Files.writeString(CUMULATIVE_OUTPUT,
      "PLATE_STATUS | session "+sid+" | "+status+" | operator_confirmation "+ts+"\n",
      StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
  }

  static boolean confirmPreviousSessionPlateBeforeNewLive()throws Exception{
    String sid=latestLiveSessionId();
    if(sid==null)return true;
    String status=plateStatusForSession(sid);
    if("COMPLETE".equals(status))return true;

    int n=publicationSessionNumber(sid);
    System.out.println("\n*** LIVE SESSION BLOCKED - PUBLICATION PLATE OUTSTANDING ***");
    System.out.println("Latest completed live session in output.txt: "+sessionLabel(n,sid));
    if(n>0)System.out.println("The Session "+n+" publication plate must be created and checked before Session "+(n+1)+" can start.");
    System.out.println("The full publication instructions were already shown at startup / after S analysis, so they are not repeated here.");

    if(choice("Has the "+(n>0?"SESSION "+n:"session")+" plate been created and checked? [Y/N]: ","YN").equals("Y")){
      appendPlateStatus(sid,"COMPLETE");
      System.out.println("Plate confirmation recorded in output.txt. The live-session gate is now closed for "+sessionLabel(n,sid)+".");
      return true;
    }

    System.out.println("Plate remains outstanding for "+sessionLabel(n,sid)+". Another live session cannot start yet.");
    return false;
  }

  static Path reportForSession(String sid){
    Path direct=evidenceFile("blackjack_live_session_"+sid+".txt");
    if(Files.exists(direct))return direct;
    Path legacy=Path.of("blackjack_live_session_"+sid+".txt");
    if(Files.exists(legacy))return legacy;
    // Historical ledger ids sometimes carry a correction suffix while the retained source
    // report keeps the original timestamp filename. Prefer that dedicated file over a broad
    // output.txt fallback so a different session's preamble/context can never bleed in.
    String base=sid.replace("_CORRECTED_247_DEAL_ORDER","").replace("_CORRECTED","");
    if(!base.equals(sid)){
      Path d2=evidenceFile("blackjack_live_session_"+base+".txt"); if(Files.exists(d2))return d2;
      Path l2=Path.of("blackjack_live_session_"+base+".txt"); if(Files.exists(l2))return l2;
    }
    // lastSavedReport is safe only when it actually belongs to the requested SID.
    if(lastSavedReport!=null&&Files.exists(lastSavedReport)&&
       lastSavedReport.getFileName().toString().equals("blackjack_live_session_"+sid+".txt"))return lastSavedReport;
    return CUMULATIVE_OUTPUT;
  }
  static List<Card> loadCapturedReservoir(Path source,String sid)throws Exception{
    List<String> lines=Files.readAllLines(source);List<Card> cards=new ArrayList<>();boolean in=false;String sessionMarker="================ SESSION "+sid+" ================";
    if(source.equals(CUMULATIVE_OUTPUT)){boolean found=false;for(String line:lines){if(line.trim().equals(sessionMarker)){found=true;in=false;}else if(found&&line.startsWith("================ END SESSION "))break;else if(found&&line.trim().equals("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)"))in=true;else if(found&&in&&line.trim().equals("AUDIT LOG"))break;else if(found&&in)parseChronCard(line,cards);} }
    else {for(String line:lines){if(line.trim().equals("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)")||line.trim().equals("CHRONOLOGICAL CARD DATASET (CORRECTED RECONSTRUCTED DEAL ORDER)")){in=true;continue;}if(in&&line.trim().equals("AUDIT LOG"))break;if(in)parseChronCard(line,cards);}}
    if(cards.isEmpty())throw new IOException("No chronological captured-card dataset found for session "+sid);return cards;
  }
  static void parseChronCard(String line,List<Card> out){String t=line.trim();if(!t.matches("#\\d+.*\\|.*"))return;String raw=t.substring(t.lastIndexOf('|')+1).trim();Card c=parseCard(raw);if(c!=null&&!c.suit.isEmpty()&&!c.suit.equals("?"))out.add(c);}

  static ObservedStream loadObservedStream(Path source,String sid)throws Exception{
    List<String> lines=Files.readAllLines(source); List<String> body=new ArrayList<>(); boolean found=!source.equals(CUMULATIVE_OUTPUT),inSession=!source.equals(CUMULATIVE_OUTPUT);
    String marker="================ SESSION "+sid+" ================";
    for(String line:lines){
      if(source.equals(CUMULATIVE_OUTPUT)){if(line.trim().equals(marker)){found=true;inSession=true;continue;}if(inSession&&line.startsWith("================ END SESSION "))break;if(!inSession)continue;}
      if(found)body.add(line);
    }
    TreeSet<Integer> boundaries=new TreeSet<>(); boundaries.add(1);
    for(String line:body){
      if(line.startsWith("Shuffle log:")){java.util.regex.Matcher m=java.util.regex.Pattern.compile("Shuffle observed before hand (\\d+)").matcher(line);while(m.find())boundaries.add(Integer.parseInt(m.group(1)));}
    }
    ObservedStream os=new ObservedStream(); for(int h:boundaries){os.starts.add(h);os.segments.add(new ArrayList<>());}
    boolean in=false;
    for(String line:body){
      if(line.trim().equals("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)")||line.trim().equals("CHRONOLOGICAL CARD DATASET (CORRECTED RECONSTRUCTED DEAL ORDER)")){in=true;continue;} if(in&&line.trim().equals("AUDIT LOG"))break; if(!in)continue;
      String t=line.trim(); java.util.regex.Matcher hm=java.util.regex.Pattern.compile("#\\d+ \\| H(\\d+) \\|.*\\| (\\S+)\\s*$").matcher(t); if(!hm.find())continue;
      int hand=Integer.parseInt(hm.group(1)); Card c=parseCard(hm.group(2)); if(c==null||c.suit.isEmpty()||c.suit.equals("?"))continue;
      int seg=0;for(int i=0;i<os.starts.size();i++)if(os.starts.get(i)<=hand)seg=i;os.segments.get(seg).add(c);os.cardsPerHand.merge(hand,1,Integer::sum);
    }
    // Preserve only explicit source settlement overrides. Associate each override with
    // the immediately following committed HAND record in the audit log.
    Double pendingAdjustment=null;
    for(String line:body){
      java.util.regex.Matcher ov=java.util.regex.Pattern.compile("VALIDATION OVERRIDE \\| expected ([0-9.]+) entered ([0-9.]+)").matcher(line);
      if(ov.find()){
        if(line.toLowerCase(Locale.ROOT).contains("starting balance entered incorrectly")){pendingAdjustment=null;continue;}
        pendingAdjustment=Double.parseDouble(ov.group(2))-Double.parseDouble(ov.group(1));continue;
      }
      if(pendingAdjustment!=null){java.util.regex.Matcher hh=java.util.regex.Pattern.compile("^HAND (\\d+) \\|").matcher(line.trim());if(hh.find()){os.settlementAdjustments.merge(Integer.parseInt(hh.group(1)),pendingAdjustment,Double::sum);pendingAdjustment=null;}}
    }
    if(os.segments.isEmpty()||os.segments.stream().mapToInt(List::size).sum()==0)throw new IOException("No exact observed card stream found for session "+sid);
    return os;
  }

  static class SourceObservedMetrics {
    int hands=-1,w=-1,l=-1,p=-1,total=-1,textbook=-1,research=-1,primary=-1; double finalBank=Double.NaN,exposure=Double.NaN;
  }
  static SourceObservedMetrics sourceObservedMetrics(Path source,String sid)throws Exception{
    List<String> lines=Files.readAllLines(source);SourceObservedMetrics m=new SourceObservedMetrics();
    boolean active=!source.equals(CUMULATIVE_OUTPUT);String marker="================ SESSION "+sid+" ================";
    double correctedPlatformStart=Double.NaN, platformFinal=Double.NaN;
    for(String line:lines){String t=line.trim();if(source.equals(CUMULATIVE_OUTPUT)){if(t.equals(marker)){active=true;continue;}if(active&&t.startsWith("================ END SESSION "))break;if(!active)continue;}
      java.util.regex.Matcher x;
      x=java.util.regex.Pattern.compile("OFFSET RESEARCH-EQUIVALENT: Start £100\\.00 \\| Final £([0-9.-]+)").matcher(t);if(x.find())m.finalBank=Double.parseDouble(x.group(1));
      x=java.util.regex.Pattern.compile("Platform Final £([0-9.-]+).*Exposure £([0-9.-]+)").matcher(t);if(x.find()){platformFinal=Double.parseDouble(x.group(1));m.exposure=Double.parseDouble(x.group(2));}
      x=java.util.regex.Pattern.compile("actual pre-Hand-1 platform balance was .?£?([0-9.]+)",java.util.regex.Pattern.CASE_INSENSITIVE).matcher(t);if(x.find())correctedPlatformStart=Double.parseDouble(x.group(1));
      x=java.util.regex.Pattern.compile("W/L/P (\\d+)/(\\d+)/(\\d+)").matcher(t);if(x.matches()){m.w=Integer.parseInt(x.group(1));m.l=Integer.parseInt(x.group(2));m.p=Integer.parseInt(x.group(3));m.hands=m.w+m.l+m.p;}
      x=java.util.regex.Pattern.compile("TOTAL DECISION EVENTS: (\\d+).*").matcher(t);if(x.matches())m.total=Integer.parseInt(x.group(1));
      x=java.util.regex.Pattern.compile("TEXTBOOK / FROZEN-ALIGNED: (\\d+)").matcher(t);if(x.matches())m.textbook=Integer.parseInt(x.group(1));
      x=java.util.regex.Pattern.compile("FROZEN RESEARCH OVERRIDE events: (\\d+)").matcher(t);if(x.matches())m.research=Integer.parseInt(x.group(1));
      x=java.util.regex.Pattern.compile("FROZEN PRIMARY HARD11-vs-10 events: (\\d+)").matcher(t);if(x.matches())m.primary=Integer.parseInt(x.group(1));
    }
    if(!Double.isNaN(correctedPlatformStart)&&!Double.isNaN(platformFinal)){
      double correctedFloor=correctedPlatformStart-100.0;
      m.finalBank=platformFinal-correctedFloor;
    }
    return m;
  }

  static List<String> observedSourceEvidence(String sid,Path source)throws Exception{
    List<String> sourceLines=Files.readAllLines(source,StandardCharsets.UTF_8);
    boolean hybridSource=sourceLines.stream().anyMatch(x->x.trim().startsWith("MODE: HYBRID"));
    if(hybridSource){
      SourceObservedMetrics sm=sourceObservedMetrics(source,sid);
      List<String>x=new ArrayList<>(); x.add(""); x.add("================ OBSERVED HYBRID JOURNEY | SESSION "+sid+" ================");
      x.add("Observed mode: HYBRID | operator choices are authoritative for the live journey; this block does not relabel them as Frozen.");
      x.add("Observed Hybrid: hands "+sm.hands+" | final £"+(Double.isNaN(sm.finalBank)?"N/A":m(sm.finalBank))+" | W/L/P "+sm.w+"/"+sm.l+"/"+sm.p+" | exposure £"+(Double.isNaN(sm.exposure)?"N/A":m(sm.exposure)));
      x.add("Frozen/Casual formal-start counterfactual: DEFERRED TO S shuffle-analysis workflow, using the exact captured formal cardstream and source-completion rules.");
      x.add("================ END OBSERVED HYBRID JOURNEY | SESSION "+sid+" ================"); return x;
    }
    ObservedStream os=loadObservedStream(source,sid); ReplayResult f=runObservedReplay(os,true),c=runObservedReplay(os,false); PairDiff d=compareReplayDecisions(f,c); int matched=d.comparable-d.different;SourceObservedMetrics sm=sourceObservedMetrics(source,sid);
    boolean frozenGate=sm.hands>=0 && f.hands==sm.hands && Math.abs(f.bank-sm.finalBank)<0.001 && f.w==sm.w && f.l==sm.l && f.p==sm.p && Math.abs(f.exposure-sm.exposure)<0.001;
    // A legitimate table exit is a completed counterfactual journey, not reconstruction failure.
    boolean casualGate=(c.hands==30&&!c.sourceExhausted) || (c.reason.equals("BANKROLL")&&!c.sourceExhausted);
    boolean arithmetic=d.comparable==matched+d.different && d.comparable<=f.decisions.size() && d.comparable<=c.decisions.size();
    boolean verified=frozenGate&&casualGate&&arithmetic;
    List<String>x=new ArrayList<>();x.add("");x.add("================ OBSERVED-SOURCE FROZEN vs CASUAL RECONSTRUCTION | SESSION "+sid+" ================");
    x.add("V9.1.5 observed-source boundary: exact captured chronology is replayed in recorded order with observed 247 no-peek timing; every visibly observed shuffle boundary resets to the corresponding captured segment. No card is invented. A-E synthetic shuffle methodology is unchanged.");
    x.add("Frozen replay gate: hands "+f.hands+" | final £"+m(f.bank)+" | W/L/P "+f.w+"/"+f.l+"/"+f.p+" | exposure £"+m(f.exposure)+" | peak £"+m(f.peak)+" (hand "+f.peakHand+") | trough £"+m(f.trough)+" (hand "+f.troughHand+") | maxDD £"+m(f.maxDD)+" (hand "+f.maxDDHand+") | "+(frozenGate?"PASS":"FAIL"));
    x.add("Frozen observed provenance (authoritative live capture): Total "+sm.total+" | Textbook/Frozen-Aligned "+sm.textbook+" | Research Overrides "+sm.research+" | Primary H11v10 Overrides "+sm.primary);
    if(verified){
      x.add("Casual reconstructed: hands "+c.hands+" | final £"+m(c.bank)+" | W/L/P "+c.w+"/"+c.l+"/"+c.p+" | exposure £"+m(c.exposure)+" | peak £"+m(c.peak)+" (hand "+c.peakHand+") | trough £"+m(c.trough)+" (hand "+c.troughHand+") | maxDD £"+m(c.maxDD)+" (hand "+c.maxDDHand+") | termination "+c.reason);
      x.add("Casual decision provenance: Total "+c.decisions.size()+" | Textbook/Frozen-Aligned "+c.textbookAligned+" | Research Overrides "+c.researchOverrides+" | Primary H11v10 Overrides "+c.primaryH11v10);
      double align=d.comparable==0?Double.NaN:100.0*matched/d.comparable;
      x.add("Directly Comparable Action Decisions: "+d.comparable+" | Matched "+matched+" | Different "+d.different+" | Alignment "+(d.comparable==0?"N/A":String.format(Locale.ROOT,"%.1f%%",align)));
      x.add("RECONSTRUCTION STATUS: SOURCE-VERIFIED");
    }else{
      x.add("Casual decision provenance: N/A");x.add("Directly Comparable Action Decisions: N/A | Matched N/A | Different N/A | Alignment N/A");
      x.add("RECONSTRUCTION STATUS: NOT SOURCE-VERIFIED | Frozen gate "+(frozenGate?"PASS":"FAIL")+" | Casual completion "+(casualGate?"PASS":"FAIL")+" | Arithmetic "+(arithmetic?"PASS":"FAIL"));
      x.add("Publication rule: do not infer or transplant missing Casual/comparison values from another session.");
    }
    x.add("================ END OBSERVED-SOURCE RECONSTRUCTION | SESSION "+sid+" ================");return x;
  }

  static void runObservedReconstructionCorrection()throws Exception{
    String sid=latestLiveSessionId();if(sid==null){System.out.println("\nNo completed live session was found in output.txt.");return;}
    Path source=reportForSession(sid);List<String> ev=observedSourceEvidence(sid,source);
    List<String> out=new ArrayList<>();out.add("");out.add("================ OBSERVED_RECONSTRUCTION_CORRECTION | SESSION "+sid+" ================");
    out.add("Correction provenance: V9.1.8 supersedes the prior observed-source reconstruction block only; original live capture, ledger, A-E shuffle results and prior text remain unchanged.");
    out.add("Reason: observed 247 no-peek timing/card consumption is honoured; an explicitly documented pre-Hand-1 starting-balance correction is treated as a start correction, not as a hand-settlement adjustment; legitimate Casual bankroll/table exit counts as a completed counterfactual journey.");
    out.addAll(ev);out.add("OBSERVED_RECONSTRUCTION_CORRECTION_STATUS | session "+sid+" | COMPLETE");out.add("================ END OBSERVED_RECONSTRUCTION_CORRECTION | SESSION "+sid+" ================");
    Files.write(CUMULATIVE_OUTPUT,out,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
    ensureEvidenceDirectory(); Path rf=evidenceFile("blackjack_observed_reconstruction_correction_"+sid+".txt");Files.write(rf,out,StandardCharsets.UTF_8);
    System.out.println("\nObserved reconstruction revalidation appended without overwriting prior evidence.");System.out.println("Correction report: "+rf.toAbsolutePath());
    for(String line:ev)System.out.println(line);
  }

  static void backupOutputBeforeAnalysis(String sid)throws Exception{
    if(!Files.exists(CUMULATIVE_OUTPUT))return;ensureBackupsDirectory();int n=publicationSessionNumber(sid);String base="outputbackup_SESSION"+(n>0?n:"X")+"_"+sid;Path b=BACKUPS_DIR.resolve(base+".txt");int k=2;while(Files.exists(b))b=BACKUPS_DIR.resolve(base+"_"+(k++)+".txt");Files.copy(CUMULATIVE_OUTPUT,b);System.out.println("Pre-S evidence backup created: "+b.toAbsolutePath());
  }

  static int parseIntField(List<String> lines,String prefix){
    for(String line:lines){String t=line.trim();if(t.startsWith(prefix)){try{return Integer.parseInt(t.substring(prefix.length()).trim());}catch(Exception ignored){}}}
    return 0;
  }

  static String parseStringField(List<String> lines,String prefix){
    for(String line:lines){String t=line.trim();if(t.startsWith(prefix))return t.substring(prefix.length()).trim();}
    return "";
  }

  static Path resolveEvidencePath(String raw){
    if(raw==null||raw.isBlank())return null;
    String normalized=raw.trim().replace('\\',java.io.File.separatorChar).replace('/',java.io.File.separatorChar);
    Path p=Path.of(normalized);
    if(Files.exists(p))return p;
    Path name=p.getFileName();
    if(name!=null){Path e=evidenceFile(name.toString());if(Files.exists(e))return e;}
    return p;
  }

  static LinkedPreambleEvidence linkedPreambleEvidence(Path formalSource)throws Exception{
    List<String> lines=Files.readAllLines(formalSource,StandardCharsets.UTF_8);
    String yes=parseStringField(lines,"PREAMBLE: ");
    if(!yes.equalsIgnoreCase("YES"))return null;
    LinkedPreambleEvidence e=new LinkedPreambleEvidence();
    e.id=parseStringField(lines,"PREAMBLE_ID: ");
    e.hands=parseIntField(lines,"PREAMBLE_HANDS: ");
    e.cards=parseIntField(lines,"PREAMBLE_CARDS_RECORDED: ");
    e.shuffles=parseIntField(lines,"PREAMBLE_SHUFFLES_OBSERVED: ");
    e.cardsAtFormalEntry=parseIntField(lines,"CARDS SINCE MOST RECENT OBSERVED SHUFFLE/PREAMBLE START AT FORMAL HAND 1: ");
    e.file=resolveEvidencePath(parseStringField(lines,"PREAMBLE_SOURCE: "));
    if(e.file==null||!Files.exists(e.file))throw new IOException("Linked preamble file not found: "+(e.file==null?"N/A":e.file));
    return e;
  }

  static void addObservedSegmentStart(ObservedStream os,int start){
    if(os.starts.contains(start))return;
    int at=0;while(at<os.starts.size()&&os.starts.get(at)<start)at++;
    os.starts.add(at,start);os.segments.add(at,new ArrayList<>());
  }

  static void appendObservedFileToCombined(ObservedStream os,Path source,String chronologyHeader,int handOffset,boolean captureStartBeginsSegment)throws Exception{
    List<String> lines=Files.readAllLines(source,StandardCharsets.UTF_8);
    TreeSet<Integer> localStarts=new TreeSet<>();if(captureStartBeginsSegment)localStarts.add(1);
    for(String line:lines){
      if(line.startsWith("Shuffle log:")){
        java.util.regex.Matcher m=java.util.regex.Pattern.compile("Shuffle observed before hand (\\d+)").matcher(line);
        while(m.find())localStarts.add(Integer.parseInt(m.group(1)));
      }
    }
    for(int h:localStarts)addObservedSegmentStart(os,handOffset+h);
    boolean in=false;
    for(String line:lines){
      if(line.trim().equals(chronologyHeader)){in=true;continue;}
      if(in&&line.trim().equals("AUDIT LOG"))break;
      if(!in)continue;
      String t=line.trim();java.util.regex.Matcher hm=java.util.regex.Pattern.compile("#\\d+ \\| H(\\d+) \\|.*\\| (\\S+)\\s*$").matcher(t);if(!hm.find())continue;
      int sourceHand=Integer.parseInt(hm.group(1));int combinedHand=handOffset+sourceHand;Card c=parseCard(hm.group(2));if(c==null||c.suit.isEmpty()||c.suit.equals("?"))continue;
      int seg=0;for(int i=0;i<os.starts.size();i++)if(os.starts.get(i)<=combinedHand)seg=i;os.segments.get(seg).add(c);os.cardsPerHand.merge(combinedHand,1,Integer::sum);
    }
  }

  static ObservedStream loadPreambleStartStream(LinkedPreambleEvidence pe,Path formalSource)throws Exception{
    ObservedStream os=new ObservedStream();
    appendObservedFileToCombined(os,pe.file,"CHRONOLOGICAL CARD DATASET (PREAMBLE)",0,true);
    appendObservedFileToCombined(os,formalSource,"CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)",pe.hands,false);
    int cards=0;for(List<Card> seg:os.segments)cards+=seg.size();
    if(cards==0)throw new IOException("No exact rank+suit chronology found across linked preamble + formal session.");
    return os;
  }

  static boolean replaySourceComplete(ReplayResult r){return !r.sourceExhausted&&(r.hands==30||r.reason.equals("BANKROLL"));}

  static List<String> preambleStartCounterfactualEvidence(String sid,Path formalSource)throws Exception{
    LinkedPreambleEvidence pe=linkedPreambleEvidence(formalSource);if(pe==null)return List.of();
    ObservedStream os=loadPreambleStartStream(pe,formalSource);ReplayResult f=runObservedCounterfactual(os,true),c=runObservedCounterfactual(os,false);
    int captured=0;for(List<Card> seg:os.segments)captured+=seg.size();
    List<String>x=new ArrayList<>();
    x.add("");x.add("================ PREAMBLE-START COUNTERFACTUAL | SESSION "+sid+" ================");
    x.add("PURPOSE: bounded context check only - replay the two established profiles from the first recorded preamble card before formal Hand 1. This is not a new formal session and does not alter Session "+sid+" metrics.");
    x.add("Preamble source: "+pe.file+" | preamble hands "+pe.hands+" | preamble cards "+pe.cards+" | observed preamble shuffles "+pe.shuffles);
    x.add("FORMAL ENTRY CONTEXT: "+pe.cardsAtFormalEntry+" recorded cards since the most recent observed shuffle/preamble start at formal Hand 1.");
    x.add("Captured chronology available to this check: "+captured+" exact rank+suit cards across preamble + formal evidence.");
    x.add("Boundary: both replays start at research £100.00 on the first recorded preamble card, consume independently, preserve observed shuffle boundaries at their recorded source-hand positions, and never invent a card or inherit the observed player's settlement outcome.");
    x.add("Frozen from preamble start: hands "+f.hands+" | final £"+m(f.bank)+" | W/L/P "+f.w+"/"+f.l+"/"+f.p+" | cards consumed "+f.cards+" | termination "+f.reason+" | "+(replaySourceComplete(f)?"SOURCE-COMPLETE":"NOT SOURCE-COMPLETE"));
    x.add("Casual from preamble start: hands "+c.hands+" | final £"+m(c.bank)+" | W/L/P "+c.w+"/"+c.l+"/"+c.p+" | cards consumed "+c.cards+" | termination "+c.reason+" | "+(replaySourceComplete(c)?"SOURCE-COMPLETE":"NOT SOURCE-COMPLETE"));
    x.add("Publication boundary: use these two values only as the declared preamble-start context check; do not infer +/- card-start alternatives or treat the preamble as part of formal Session "+sid+" W/L/P, exposure, bankroll score or ledger.");
    x.add("================ END PREAMBLE-START COUNTERFACTUAL | SESSION "+sid+" ================");
    return x;
  }

  static List<String> hybridFormalCounterfactualEvidence(String sid,Path formalSource)throws Exception{
    List<String> lines=Files.readAllLines(formalSource,StandardCharsets.UTF_8);
    boolean hybrid=lines.stream().anyMatch(x->x.trim().startsWith("MODE: HYBRID"));
    if(!hybrid)return List.of();
    ObservedStream os=loadObservedStream(formalSource,sid);
    ReplayResult f=runObservedCounterfactual(os,true), c=runObservedCounterfactual(os,false);
    int captured=os.segments.stream().mapToInt(List::size).sum();
    List<String>x=new ArrayList<>(); x.add(""); x.add("================ HYBRID FORMAL-START COUNTERFACTUAL | SESSION "+sid+" ================");
    x.add("PURPOSE: replay the unchanged Frozen and fixed Casual policies from the same formal-session starting captured cardstream. The observed Hybrid journey remains separate and authoritative for what was actually played.");
    x.add("Captured formal chronology available: "+captured+" exact rank+suit cards. Each policy consumes independently; no card or settlement is invented.");
    x.add(counterfactualLine("Frozen",f));
    x.add(counterfactualLine("Casual",c));
    x.add("Publication rule: if a replay is NOT SOURCE-COMPLETE, its outcome is N/A; the displayed bankroll is only the last source-supported bankroll. Do not present it as a completed journey.");
    x.add("================ END HYBRID FORMAL-START COUNTERFACTUAL | SESSION "+sid+" ================"); return x;
  }

  static String counterfactualLine(String label,ReplayResult r){
    boolean complete=replaySourceComplete(r);
    return label+" from formal start: outcome "+(complete?("£"+m(r.bank)):"N/A")+" | hands "+r.hands+" | W/L/P "+r.w+"/"+r.l+"/"+r.p+" | exposure £"+m(r.exposure)+" | cards consumed "+r.cards+" | termination "+r.reason+" | "+(complete?"SOURCE-COMPLETE":"NOT SOURCE-COMPLETE")+(complete?"":" | last source-supported bankroll £"+m(r.bank));
  }

  static void runAutomaticShuffleAnalysis()throws Exception{
    String sid=latestPendingSessionId();
    // One-time/lazy migration: populate the exploratory Stat-Watching chart for any historical
    // session whose exact retained chronology is still available. The currently pending session
    // is excluded here because its block is produced below as part of this same S run.
    backfillHistoricalStatWatchingEvidence(sid);
    if(sid==null){System.out.println("\nNo PENDING live-session shuffle analysis was found in output.txt.");return;}backupOutputBeforeAnalysis(sid);Path source=reportForSession(sid);List<Card> reservoir=loadCapturedReservoir(source,sid);
    System.out.println("\nPOST-SESSION SHUFFLE ROBUSTNESS - SESSION "+sid);System.out.println("Captured reservoir: "+reservoir.size()+" cards | paired replays per method: "+DEFAULT_SHUFFLE_REPLICATES);System.out.println("Frozen and fixed Casual receive the same seeded starting source per replicate and then consume independently.");
    List<MethodAggregate> all=new ArrayList<>();for(ShuffleMethod m:ShuffleMethod.values())all.add(runMethod(reservoir,m,DEFAULT_SHUFFLE_REPLICATES));
    List<String> out=new ArrayList<>();out.add("");out.add("================ AUTOMATIC SHUFFLE ROBUSTNESS | SESSION "+sid+" ================");out.add("ENGINE VERSION: integrated-post-session-v3 | paired_replays_per_method "+DEFAULT_SHUFFLE_REPLICATES+" | seed_base "+SHUFFLE_SEED_BASE);out.add("Captured-card reservoir: "+reservoir.size()+" cards");out.add("Comparable-action definition: same replay hand, branch, decision ordinal, player-rank state and dealer upcard in both independently consumed paths.");
    for(MethodAggregate a:all){double md=(double)a.differentActions/a.reps;out.add(a.method+" | "+methodDescription(a.method));out.add(String.format(Locale.ROOT,"  Frozen median %.2f | Casual median %.2f | Frozen mean %.2f | Casual mean %.2f",median(a.frozenFinal),median(a.casualFinal),mean(a.frozenFinal),mean(a.casualFinal)));out.add(String.format(Locale.ROOT,"  £200 reach Frozen %.2f%% | Casual %.2f%% | Mean exposure Frozen %.2f | Casual %.2f | Mean maxDD Frozen %.2f | Casual %.2f",100.0*a.frozenReach/a.reps,100.0*a.casualReach/a.reps,a.frozenExposure/a.reps,a.casualExposure/a.reps,a.frozenDD/a.reps,a.casualDD/a.reps));out.add("  30-hand completion Frozen "+a.frozenComplete+" | Casual "+a.casualComplete+" | Both "+a.bothComplete);out.add(String.format(Locale.ROOT,"  Mean paired final-bankroll difference Frozen-Casual (both completed): %s",a.bothComplete==0?"N/A":String.format(Locale.ROOT,"%.2f",a.pairedDelta/a.bothComplete)));out.add("  Comparable action decisions total: "+a.comparableActions+" | Different actions total: "+a.differentActions+" | Mean Comparable Action Differences: "+String.format(Locale.ROOT,"%.4f",md));}
    out.add("Method E boundary: after each COMPLETE hand, consumed cards return to the captured reservoir and the entire reservoir is remixed; no captured card is added, removed or substituted.");out.add("Method D boundary: declared mathematical riffle/strip/cut proxy; not a proprietary casino-shuffler reproduction.");
    List<String> hybridCheck;
    try{hybridCheck=hybridFormalCounterfactualEvidence(sid,source);}catch(Exception ex){hybridCheck=List.of("", "HYBRID FORMAL-START COUNTERFACTUAL STATUS: NOT AVAILABLE | "+ex.getMessage(), "Publication rule: do not infer missing Frozen/Casual counterfactual values.");}
    out.addAll(hybridCheck);
    List<String> preambleCheck;
    try{preambleCheck=preambleStartCounterfactualEvidence(sid,source);}catch(Exception ex){preambleCheck=List.of("", "PREAMBLE-START COUNTERFACTUAL STATUS: NOT AVAILABLE | "+ex.getMessage(), "Publication rule: do not infer missing preamble-start Frozen/Casual values.");}
    out.addAll(preambleCheck);
    List<String> statWatch;
    try{statWatch=statWatchingEvidence(sid,source);}catch(Exception ex){statWatch=List.of("", "STAT-WATCHING CASUAL STATUS: NOT AVAILABLE | "+ex.getMessage(), "Boundary: no exploratory outcome inferred when exact formal-session chronology is unavailable.");}
    out.addAll(statWatch);
    out.add("SHUFFLE_ANALYSIS_STATUS | session "+sid+" | COMPLETE");out.add("================ END AUTOMATIC SHUFFLE ROBUSTNESS | SESSION "+sid+" ================");Files.write(CUMULATIVE_OUTPUT,out,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);ensureEvidenceDirectory(); Path rf=evidenceFile("blackjack_shuffle_analysis_"+sid+".txt");Files.write(rf,out,StandardCharsets.UTF_8);System.out.println("\nShuffle analysis complete. Results appended to "+CUMULATIVE_OUTPUT.toAbsolutePath());System.out.println("Standalone analysis report: "+rf.toAbsolutePath());
    if(!hybridCheck.isEmpty()){System.out.println("\nHYBRID FORMAL-START COUNTERFACTUAL:");for(String line:hybridCheck)System.out.println(line);}
    if(!preambleCheck.isEmpty()){System.out.println("\nPREAMBLE-START CONTEXT CHECK:");for(String line:preambleCheck)System.out.println(line);}
    if(!statWatch.isEmpty()){System.out.println("\nSTAT-WATCHING CASUAL (EXPLORATORY):");for(String line:statWatch)System.out.println(line);}
    int n=publicationSessionNumber(sid);
    System.out.println("PUBLICATION NEXT STEP:");
    System.out.println("S analysis is COMPLETE for "+sessionLabel(n,sid)+". The publication plate is still required before another live session.");
    printPublicationWorkflowReminder(sid);
  }

}
