@mcptoolshop/sovereign 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <title>Sovereign · Solo / Digital · Phase 6 Telemetry · Batch</title>
5
+ <title>Sovereign Solo / Digital Mode</title>
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
  <style>
8
8
  :root {
@@ -405,7 +405,1409 @@ input[type="number"] { font-family:var(--mono); font-size:13px; padding:5px 8px;
405
405
  .batch-modal .winrate-bar { height:10px; background:var(--parchment); border:0.5px solid var(--rule-soft); position:relative; }
406
406
  .batch-modal .winrate-bar .fill { height:100%; background:var(--commercial-infrastructure); }
407
407
  .batch-modal .ex-actions { display:flex; gap:8px; margin-top:10px; flex-wrap:wrap; }
408
+
409
+ /* =====================================================================
410
+ v0.18 POLISHED — visual identity, event emphasis, orientation panel
411
+ No mechanic changes. CSS additions only.
412
+ ===================================================================== */
413
+
414
+ /* Topbar identity treatment — strong "Sovereign" wordmark, secondary mode line */
415
+ .brand .title { font-family:var(--display); font-size:34px; line-height:.96; letter-spacing:-.015em; margin-top:1px; display:flex; align-items:baseline; gap:14px; }
416
+ .brand .title .title-mode { font-family:var(--display); font-style:italic; font-weight:400; font-size:13px; letter-spacing:0; opacity:.7; }
417
+ .brand .sub { font-family:var(--display); font-style:italic; font-size:12.5px; line-height:1.4; margin-top:5px; max-width:62ch; }
418
+ .brand .eyebrow { font-family:var(--ui); font-size:9px; letter-spacing:.32em; text-transform:uppercase; color:var(--national-finance); }
419
+
420
+ .version-pill { font-family:var(--mono); font-size:9px; letter-spacing:.14em; padding:4px 9px; background:var(--parchment-2); border:0.5px solid var(--rule); color:var(--ink); opacity:.75; }
421
+
422
+ /* ---- Ledger event emphasis: CREDIT_CRISIS / DEFAULT / REBELLION stand out ---- */
423
+ .ledger .row.event { background:rgba(200,57,46,.05); border-left:2px solid var(--highlight); padding-left:6px; }
424
+ .ledger .row.event .actor { color:var(--highlight); }
425
+ .ledger .row.row-credit-crisis { background:rgba(200,57,46,.12); border-left:3px solid var(--highlight); padding-left:5px; }
426
+ .ledger .row.row-credit-crisis .actor { color:var(--highlight); font-weight:800; }
427
+ .ledger .row.row-credit-crisis::before { content:"⚠"; position:absolute; }
428
+ .ledger .row.row-default { background:#1A1612; color:var(--parchment); border-left:3px solid var(--highlight); padding:2px 6px; }
429
+ .ledger .row.row-default .actor { color:var(--highlight); font-weight:800; letter-spacing:.14em; }
430
+ .ledger .row.row-default .stamp { color:var(--parchment); opacity:.6; }
431
+ .ledger .row.row-rebellion { background:var(--revolutionary-debt); color:var(--parchment); border-left:3px solid var(--ink); padding:2px 6px; }
432
+ .ledger .row.row-rebellion .actor { color:#F0E6CD; font-weight:800; letter-spacing:.14em; }
433
+ .ledger .row.row-rebellion .stamp { color:var(--parchment); opacity:.7; }
434
+
435
+ /* Subtle stripes for track changes to distinguish from events */
436
+ .ledger .row.track { opacity:.92; }
437
+
438
+ /* ---- Tracks panel: warning band shading on Credit ≤ 4 zone + critical at 0 ---- */
439
+ .tracks-panel .t-row.credit .scale .tk[data-pos="0"] { background:#1A1612; color:var(--parchment); font-weight:700; }
440
+ .tracks-panel .t-row.credit .scale .tk[data-pos="1"],
441
+ .tracks-panel .t-row.credit .scale .tk[data-pos="2"],
442
+ .tracks-panel .t-row.credit .scale .tk[data-pos="3"],
443
+ .tracks-panel .t-row.credit .scale .tk[data-pos="4"] { background:rgba(200,57,46,.18); }
444
+ .tracks-panel .t-row.resist .scale .tk[data-pos="12"] { background:var(--revolutionary-debt); color:var(--parchment); font-weight:700; }
445
+ .tracks-panel .t-row.resist .scale .tk[data-pos="10"],
446
+ .tracks-panel .t-row.resist .scale .tk[data-pos="11"] { background:rgba(110,31,30,.22); }
447
+
448
+ /* Crisis state ribbon on track row when value is in danger zone */
449
+ .tracks-panel .t-row.credit.in-warning { border-left:3px solid var(--highlight); padding-left:6px; margin-left:-6px; }
450
+ .tracks-panel .t-row.credit.in-default { border-left:3px solid var(--highlight); padding-left:6px; margin-left:-6px; background:rgba(26,22,18,.08); }
451
+ .tracks-panel .t-row.resist.in-warning { border-left:3px solid var(--revolutionary-debt); padding-left:6px; margin-left:-6px; }
452
+
453
+ .tracks-panel .crisis-tag { font-family:var(--ui); font-size:8px; font-weight:800; letter-spacing:.2em; text-transform:uppercase; padding:2px 7px; margin-top:4px; display:inline-block; }
454
+ .tracks-panel .crisis-tag.warning { background:var(--highlight); color:#fff; }
455
+ .tracks-panel .crisis-tag.locked { background:var(--ink); color:var(--parchment); }
456
+
457
+ /* ---- Orientation panel: dismissible explainer, first-load + recall ---- */
458
+ .orient-overlay { position:fixed; inset:0; z-index:70; background:rgba(26,22,18,0.78); display:flex; align-items:center; justify-content:center; padding:32px 16px; overflow-y:auto; }
459
+ .orient-panel { background:var(--parchment); border:1.5px solid var(--ink); max-width:680px; width:100%; padding:22px 28px 24px; position:relative; }
460
+ .orient-panel::before { content:""; position:absolute; inset:5px; border:0.5px solid var(--rule-soft); pointer-events:none; }
461
+ .orient-panel .eyebrow { font-family:var(--ui); font-size:9px; letter-spacing:.32em; text-transform:uppercase; color:var(--national-finance); }
462
+ .orient-panel h2 { font-family:var(--display); font-weight:400; font-size:28px; line-height:1; margin:4px 0 2px; }
463
+ .orient-panel .lede { font-family:var(--display); font-style:italic; font-size:13.5px; margin:0 0 14px; line-height:1.45; max-width:54ch; }
464
+ .orient-panel ol { list-style:none; padding:0; margin:0 0 12px; counter-reset:o; }
465
+ .orient-panel ol li { position:relative; padding:7px 0 9px 38px; border-bottom:0.5px dashed var(--rule-soft); counter-increment:o; font-family:var(--body); font-size:12.5px; line-height:1.55; }
466
+ .orient-panel ol li::before { content:counter(o); position:absolute; left:0; top:7px; width:26px; height:26px; border-radius:50%; background:var(--ink); color:var(--parchment); font-family:var(--display); font-size:13px; display:flex; align-items:center; justify-content:center; font-weight:700; }
467
+ .orient-panel ol li:last-child { border-bottom:0; }
468
+ .orient-panel ol li strong { font-family:var(--display); font-weight:700; font-style:normal; color:var(--ink); }
469
+ .orient-panel .crisis-row { padding:8px 12px; background:rgba(200,57,46,.08); border-left:3px solid var(--highlight); font-family:var(--body); font-size:12px; line-height:1.5; margin:10px 0; }
470
+ .orient-panel .crisis-row strong { font-family:var(--display); font-weight:700; color:var(--highlight); }
471
+ .orient-panel .default-row { padding:8px 12px; background:var(--ink); color:var(--parchment); border-left:3px solid var(--highlight); font-family:var(--body); font-size:12px; line-height:1.5; margin:8px 0; }
472
+ .orient-panel .default-row strong { font-family:var(--display); font-weight:700; color:var(--highlight); }
473
+ .orient-panel .rebellion-row { padding:8px 12px; background:var(--revolutionary-debt); color:var(--parchment); border-left:3px solid var(--ink); font-family:var(--body); font-size:12px; line-height:1.5; margin:8px 0; }
474
+ .orient-panel .rebellion-row strong { font-family:var(--display); font-weight:700; color:#F0E6CD; }
475
+ .orient-panel .footer-row { display:flex; justify-content:space-between; align-items:center; margin-top:14px; gap:10px; }
476
+ .orient-panel .footer-row .meta { font-family:var(--mono); font-size:9px; letter-spacing:.14em; opacity:.6; }
477
+
478
+ /* ---- Card drawer: a little more breathing room + improved chip contrast ---- */
479
+ .card-drawer .drawer-card { padding:12px 14px; }
480
+ .card-drawer .effect { font-size:12px; line-height:1.55; }
481
+ .card-drawer .chip { padding:3px 8px; font-size:9px; border:1.5px solid var(--ink); }
482
+
483
+ /* ---- Endgame: distinguish stable / strained / collapsed credit posture, crisis chip ---- */
484
+ .endgame .posture-row { margin:14px 0 0; display:flex; gap:8px; flex-wrap:wrap; }
485
+ .endgame .posture-chip { font-family:var(--ui); font-size:9.5px; letter-spacing:.18em; text-transform:uppercase; font-weight:700; padding:5px 11px; border:1.5px solid var(--ink); }
486
+ .endgame .posture-chip.stable { background:var(--commercial-infrastructure); color:#fff; }
487
+ .endgame .posture-chip.strained { background:var(--revenue-system); color:var(--ink); }
488
+ .endgame .posture-chip.collapsed { background:var(--highlight); color:#fff; }
489
+ .endgame .posture-chip.avoided { background:var(--parchment-2); color:var(--ink); }
490
+ .endgame .posture-chip.fired { background:var(--ink); color:var(--parchment); }
491
+
492
+ /* ---- Responsive: keep readable on narrow viewports ---- */
493
+ @media (max-width: 880px) {
494
+ .topbar { grid-template-columns:1fr; }
495
+ .controls { justify-content:flex-start; }
496
+ .brand .title { font-size:26px; }
497
+ .brand .sub { font-size:11.5px; max-width:none; }
498
+ .grid { grid-template-columns:1fr; grid-template-rows:auto auto auto; }
499
+ .panel-pane { grid-column:1; grid-row:2; max-height:none; }
500
+ .ledger-pane { grid-column:1; grid-row:3; }
501
+ }
502
+
503
+ /* ---- Print stylesheet: screenshot-friendly export ---- */
504
+ @media print {
505
+ body { background:#fff; }
506
+ .app { border:0; box-shadow:none; max-width:none; }
507
+ .controls, .controls-bar, .resume-pill, .orient-overlay, .batch-overlay, .replay-overlay { display:none !important; }
508
+ .ledger { max-height:none; }
509
+ .panel-pane { max-height:none; overflow:visible; }
510
+ }
511
+
512
+ /* =====================================================================
513
+ v0.18 POLISH v2 — whole-game art direction layer
514
+ Deeper polish across all 16 surfaces. CSS additions only.
515
+ No mechanic, ID, class-removal, or token changes.
516
+ ===================================================================== */
517
+
518
+ /* ---- Color tokens, severity tiers, decorative variables ---- */
519
+ :root {
520
+ --severity-warning: #C8392E; /* same as --highlight */
521
+ --severity-catastrophic: #6E1F1E; /* deep oxblood for Default / Rebellion */
522
+ --severity-pass: #2E7A6B; /* commercial-infrastructure green */
523
+ --severity-neutral: #E6DABC; /* parchment-2 */
524
+ --foil: #8C6B2A; /* federalist gilt */
525
+ --foil-soft: #B89554;
526
+ --rule-strong: rgba(26,22,18,0.78);
527
+ --rule-hair: rgba(26,22,18,0.14);
528
+ --shadow-card: 0 2px 0 rgba(26,22,18,0.10), 0 6px 14px -8px rgba(26,22,18,0.35);
529
+ --shadow-deep: 0 4px 0 rgba(26,22,18,0.12), 0 16px 32px -14px rgba(26,22,18,0.5);
530
+ }
531
+
532
+ /* ---- App frame: gilt accent on outer border ---- */
533
+ .app { box-shadow:0 0 0 1px var(--foil-soft) inset, var(--shadow-deep); }
534
+ body { background:#221E1A; }
535
+
536
+ /* ---- Topbar refinement: subtle rule, version pill leveled ---- */
537
+ .topbar { border-bottom:1.5px solid var(--ink); position:relative; }
538
+ .topbar::after { content:""; position:absolute; left:14px; right:14px; bottom:-3px; height:1px; background:var(--foil-soft); opacity:.55; }
539
+ .brand .eyebrow::before { content:"§ "; color:var(--foil); font-weight:700; }
540
+ .brand .title { letter-spacing:-.02em; }
541
+ .brand .title .title-mode { color:var(--ink); opacity:.55; }
542
+
543
+ /* Controls bar: tighter spacing, better wrap behavior */
544
+ .controls { gap:5px; row-gap:6px; }
545
+ .controls button { font-size:10px; padding:6px 11px; letter-spacing:.1em; }
546
+ .controls .seed-pill, .controls .phase-pill, .controls .active-pill, .controls .version-pill { white-space:nowrap; }
547
+
548
+ /* ---- Board: gilt frame + corner crests + system band gloss ---- */
549
+ .board-view { background:linear-gradient(180deg, var(--parchment) 0%, var(--parchment-2) 100%); box-shadow:inset 0 0 0 1px var(--foil-soft), var(--shadow-card); }
550
+ .board-view::before { border-color:var(--foil-soft); }
551
+ .board-grid { gap:0; background:var(--ink); }
552
+ .board-cell { padding:3px 4px 4px; transition:background-color 120ms ease, outline-color 120ms ease; }
553
+ .board-cell .band { height:6px; box-shadow:inset 0 -1px 0 rgba(26,22,18,0.18); }
554
+ .board-cell .num { letter-spacing:.06em; font-weight:600; }
555
+ .board-cell .nm { padding-top:5px; }
556
+
557
+ /* Corner crests — distinct treatments */
558
+ .board-cell.corner { background:linear-gradient(135deg, var(--parchment-2) 0%, var(--parchment) 100%); border:1px solid var(--foil-soft); }
559
+ .board-cell.corner .num { display:none; }
560
+ .board-cell.corner .nm { font-size:9.5px; font-weight:700; padding-top:18px; text-align:center; letter-spacing:.02em; }
561
+ .board-cell.corner::before { content:""; position:absolute; top:5px; left:50%; transform:translateX(-50%); width:24px; height:24px; border-radius:50%; border:1.5px solid var(--foil); background:var(--parchment); display:flex; align-items:center; justify-content:center; }
562
+ .board-cell.corner::after { content:"§"; position:absolute; top:5px; left:0; right:0; text-align:center; font-family:var(--display); font-size:14px; color:var(--foil); line-height:24px; font-weight:700; }
563
+ .board-cell[data-num="0"]::after { content:"★"; color:var(--foil); } /* Treasury Opens */
564
+ .board-cell[data-num="10"]::after { content:"!"; color:var(--severity-warning); } /* Constitutional Crisis */
565
+ .board-cell[data-num="20"]::after { content:"$"; color:var(--severity-pass); } /* National Dividend */
566
+ .board-cell[data-num="30"]::after { content:"→"; color:var(--severity-warning); } /* Go to Crisis */
567
+
568
+ /* Card spaces: paper texture hint */
569
+ .board-cell.card-debate { background:repeating-linear-gradient(45deg, var(--parchment) 0 4px, var(--parchment-2) 4px 5px); }
570
+ .board-cell.card-shock { background:repeating-linear-gradient(135deg, var(--parchment) 0 4px, var(--parchment-2) 4px 5px); }
571
+ .board-cell.card-debate .nm, .board-cell.card-shock .nm { font-style:italic; }
572
+
573
+ /* Institution spaces: subtle stipple ground */
574
+ .board-cell.institution { background:radial-gradient(rgba(26,22,18,.05) 0.7px, transparent 1.2px); background-size:5px 5px; background-color:var(--parchment-2); }
575
+
576
+ /* Route spaces: dashed crawl */
577
+ .board-cell.route .nm { text-transform:uppercase; font-family:var(--ui); font-size:7px; letter-spacing:.12em; }
578
+
579
+ /* Tax / Speculation Scandal: clear danger flag */
580
+ .board-cell.tax { background:rgba(200,57,46,.06); }
581
+ .board-cell.tax .nm { color:var(--severity-warning); font-weight:700; }
582
+
583
+ /* Owner tokens + tier glyphs — non-color-only differentiation */
584
+ .board-cell .owner-dot { width:10px; height:10px; border-width:1.5px; box-shadow:0 1px 2px rgba(26,22,18,.3); }
585
+ .board-cell.owned-p0 .owner-dot { border-radius:50%; }
586
+ .board-cell.owned-p1 .owner-dot { border-radius:2px; transform:rotate(45deg); }
587
+ .board-cell.owned-p2 .owner-dot { border-radius:2px; }
588
+ .board-cell .tier { font-family:var(--display); font-weight:700; font-size:8px; letter-spacing:.04em; background:var(--foil); color:var(--ink); border-color:var(--ink); padding:0 3px; }
589
+
590
+ /* Player pawns on board: shape per slot, with hairline + drop shadow */
591
+ .board-cell .tokens .tok { box-shadow:0 1px 2px rgba(26,22,18,.4); }
592
+ .board-cell .tokens .tok.p0 { border-radius:50%; }
593
+ .board-cell .tokens .tok.p1 { border-radius:2px; transform:rotate(45deg); }
594
+ .board-cell .tokens .tok.p2 { border-radius:2px; }
595
+
596
+ /* Active space pulse */
597
+ @keyframes activeSpacePulse { 0%,100% { outline-color:var(--highlight); } 50% { outline-color:var(--foil); } }
598
+ .board-cell.active-space { outline:3px solid var(--highlight); animation:activeSpacePulse 1800ms ease-in-out infinite; z-index:2; }
599
+
600
+ /* Center plaque: gilt rule + serif body */
601
+ .board-center { background:radial-gradient(ellipse at center, var(--parchment) 0%, var(--parchment-2) 100%); }
602
+ .board-center::before, .board-center::after { background:var(--foil-soft); opacity:.7; }
603
+ .bc-eyebrow { color:var(--foil); }
604
+ .bc-title { letter-spacing:.005em; }
605
+ .bc-sub { color:var(--ink); opacity:.7; }
606
+ .bc-tracks .t { font-family:var(--ui); font-weight:700; text-transform:uppercase; letter-spacing:.08em; }
607
+
608
+ /* ---- Panels: refined heads with gilt rule + better hierarchy ---- */
609
+ .panel { box-shadow:var(--shadow-card); }
610
+ .panel .panel-head { position:relative; }
611
+ .panel .panel-head::after { content:""; position:absolute; left:0; right:0; bottom:-2px; height:1px; background:var(--foil-soft); opacity:.55; }
612
+ .panel .panel-head .name { color:var(--ink); }
613
+ .panel .panel-head .surface-id { background:var(--foil); color:var(--ink); font-weight:700; }
614
+
615
+ /* Treasury — emphasis cash + holdings as small cards */
616
+ .treasury .cash { color:var(--ink); letter-spacing:-.01em; }
617
+ .treasury .cash::before { content:"₸ "; font-family:var(--display); color:var(--foil); opacity:.8; }
618
+ .treasury .role-tag { background:var(--p0); border:1px solid var(--ink); }
619
+ .treasury .h-row { padding:5px 4px; align-items:center; }
620
+ .treasury .h-row .sw { width:11px; height:11px; box-shadow:inset 0 0 0 0.5px rgba(255,255,255,.4); }
621
+ .treasury .up-btn { background:var(--ink); color:var(--parchment); font-weight:700; letter-spacing:.12em; }
622
+ .treasury .up-btn:hover { background:var(--foil); color:var(--ink); }
623
+ .treasury .tier-badge { font-family:var(--display); font-weight:700; font-size:9px; background:var(--foil); color:var(--ink); padding:1px 5px; }
624
+
625
+ /* Opponents — slot color accent stripe at left */
626
+ .opp-card { position:relative; padding-left:14px; }
627
+ .opp-card::before { content:""; position:absolute; left:0; top:0; bottom:0; width:4px; }
628
+ .opp-card.p1::before { background:var(--p1); }
629
+ .opp-card.p2::before { background:var(--p2); }
630
+ .opp-card .head .pdot { box-shadow:0 1px 2px rgba(26,22,18,.3); }
631
+ .opp-card .cash::before { content:"₸ "; color:var(--foil); opacity:.7; }
632
+
633
+ /* Acts — refined vote chrome */
634
+ .acts-panel .current-act { box-shadow:inset 0 0 0 1px var(--foil-soft); position:relative; }
635
+ .acts-panel .current-act::before { content:""; position:absolute; top:5px; right:6px; width:18px; height:18px; border:1px solid var(--foil-soft); border-radius:50%; }
636
+ .acts-panel .current-act::after { content:"⚖"; position:absolute; top:5px; right:6px; width:18px; height:18px; line-height:18px; text-align:center; color:var(--foil); font-size:11px; }
637
+ .acts-panel .pretitle { color:var(--foil-soft); }
638
+ .acts-panel .vote-row { transition:background-color 120ms ease; }
639
+ .acts-panel .vote-row.pending { opacity:.78; }
640
+ .acts-panel .ballot { box-shadow:0 1px 0 rgba(26,22,18,.18); }
641
+ .acts-panel .slot { position:relative; padding-left:18px; }
642
+ .acts-panel .slot::before { content:""; position:absolute; left:5px; top:50%; transform:translateY(-50%); width:6px; height:6px; border-radius:50%; background:var(--severity-pass); }
643
+
644
+ /* Narration entry: small foil flourish */
645
+ .narration-entry { box-shadow:0 1px 0 rgba(26,22,18,.06); }
646
+ .narration-entry::before { content:""; position:absolute; left:0; top:0; bottom:0; width:3px; background:var(--foil-soft); }
647
+ .narration-entry .trig::after { content:" §"; color:var(--foil); }
648
+
649
+ /* Tracks panel — bigger scale cells + sharper marker + labelled zones */
650
+ .tracks-panel .lbl-t::before { content:""; display:inline-block; width:8px; height:8px; border-radius:50%; vertical-align:middle; margin-right:6px; }
651
+ .tracks-panel .t-row.credit .lbl-t::before { background:var(--national-finance); }
652
+ .tracks-panel .t-row.resist .lbl-t::before { background:var(--highlight); }
653
+ .tracks-panel .t-row.indust .lbl-t::before { background:var(--manufactures); }
654
+ .tracks-panel .scale { height:22px; }
655
+ .tracks-panel .tk { font-size:9px; font-weight:700; color:rgba(26,22,18,.55); }
656
+ .tracks-panel .tk.marker { color:transparent; }
657
+ .tracks-panel .tk.marker::after { inset:3px; box-shadow:0 0 0 1px var(--parchment), 0 1px 3px rgba(26,22,18,.4); }
658
+ .tracks-panel .t-row.credit .scale .tk[data-pos="8"],
659
+ .tracks-panel .t-row.credit .scale .tk[data-pos="12"] { background:rgba(46,122,107,.18); color:var(--severity-pass); }
660
+
661
+ /* Inspector + Card drawer: card-feel ---- */
662
+ .inspector .card-art, .card-drawer .drawer-card, .auction .auction-card { box-shadow:var(--shadow-card); padding:11px 13px 12px; }
663
+ .inspector .band, .card-drawer .band, .auction .band { padding:5px 8px; font-weight:800; box-shadow:inset 0 -1px 0 rgba(255,255,255,.18); }
664
+ .inspector .nm, .card-drawer .nm, .auction .nm { letter-spacing:-.005em; font-size:17px; }
665
+ .inspector .flavor, .card-drawer .alert { color:var(--ink); opacity:.85; }
666
+ .inspector .meta-row::before { content:"§ "; color:var(--foil); }
667
+ .inspector .pay-table { box-shadow:inset 0 0 0 1px var(--rule-hair); }
668
+
669
+ /* Card drawer: Market Shock vs Republic Debate distinct backs */
670
+ .card-drawer .drawer-card[data-deck="market"] { background:linear-gradient(180deg, #F2E4C6 0%, #E5D2A5 100%); }
671
+ .card-drawer .drawer-card[data-deck="market"] .band { background:linear-gradient(90deg, var(--highlight) 0 50%, var(--ink) 50%); color:#fff; }
672
+ .card-drawer .drawer-card[data-deck="debate"] { background:linear-gradient(180deg, #ECE5D2 0%, #D8CCAD 100%); }
673
+ .card-drawer .drawer-card[data-deck="debate"] .band { background:var(--ink); color:var(--parchment); }
674
+ .card-drawer .effect { background:rgba(255,255,255,.4); padding:8px 12px; border:0.5px dashed var(--rule); }
675
+ .card-drawer .chip { box-shadow:0 1px 0 rgba(26,22,18,.2); }
676
+
677
+ /* Auction — bid rows with subtle alternating ground */
678
+ .auction .bid-row:nth-child(even) { background:var(--parchment-2); }
679
+ .auction .bid-row .amt::before { content:"$"; color:var(--foil); opacity:.7; }
680
+ .auction .high-bid { font-weight:700; }
681
+
682
+ /* ---- Ledger refinements: cleaner stamps, severity-toned hairlines ---- */
683
+ .ledger { font-feature-settings:"tnum" 1; }
684
+ .ledger .head { font-size:10px; }
685
+ .ledger .row { padding:3px 0 4px; }
686
+ .ledger .row .stamp { font-size:9px; }
687
+ .ledger .row .actor { font-size:9.5px; }
688
+ .ledger .row.track .actor { font-weight:800; }
689
+ .ledger .row.cash .actor::after { content:" ₸"; color:var(--foil); opacity:.7; }
690
+ .ledger .row.event { box-shadow:inset 2px 0 0 var(--highlight); }
691
+ .ledger .row.row-credit-crisis { box-shadow:inset 3px 0 0 var(--highlight); }
692
+ .ledger .row.row-default { box-shadow:inset 3px 0 0 var(--severity-warning); }
693
+ .ledger .row.row-rebellion { box-shadow:inset 3px 0 0 var(--ink); }
694
+
695
+ /* ---- Endgame: more dramatic winner laurel ---- */
696
+ .endgame { padding:24px 28px 32px; }
697
+ .endgame h2 { font-size:38px; line-height:1; letter-spacing:-.01em; }
698
+ .endgame .sub { font-size:14px; }
699
+ .endgame .winner { position:relative; overflow:hidden; padding:18px 22px; box-shadow:inset 0 0 0 1px rgba(255,255,255,.25), var(--shadow-card); }
700
+ .endgame .winner::before { content:""; position:absolute; left:0; right:0; top:0; height:3px; background:linear-gradient(90deg, var(--foil) 0%, #E5C36A 50%, var(--foil) 100%); }
701
+ .endgame .winner::after { content:"§"; position:absolute; right:18px; top:50%; transform:translateY(-50%); font-family:var(--display); font-size:60px; opacity:.18; color:#fff; }
702
+ .endgame .winner .lbl { color:rgba(255,255,255,.8); }
703
+ .endgame .winner .nm { font-size:28px; line-height:1.05; }
704
+ .endgame .winner .score { font-size:13px; opacity:.92; }
705
+ .endgame .posture-row { margin-top:12px; }
706
+ .endgame .posture-chip { box-shadow:0 1px 0 rgba(26,22,18,.18); }
707
+ .endgame .pcol h3 { letter-spacing:-.01em; }
708
+ .endgame .pcol .total::before { content:"= "; color:var(--foil); font-family:var(--display); font-weight:400; opacity:.55; }
709
+ .endgame .actions button { padding:9px 16px; font-weight:700; }
710
+
711
+ /* Endgame narration block: subtle inner rule */
712
+ .endgame-narration { background:linear-gradient(180deg, var(--parchment-2) 0%, var(--parchment) 100%); }
713
+ .endgame-narration h3 { color:var(--ink); font-size:20px; }
714
+ .endgame-narration p { line-height:1.65; }
715
+ .endgame-narration p::first-letter { font-family:var(--display); font-size:26px; line-height:1; padding-right:2px; color:var(--foil); }
716
+
717
+ /* ---- Replay overlay refinement ---- */
718
+ .replay-overlay { background:rgba(34,30,26,0.88); }
719
+ .replay-overlay .replay-frame { box-shadow:0 0 0 1px var(--foil-soft) inset, var(--shadow-deep); }
720
+ .scrubber .ticks { background:linear-gradient(180deg, var(--parchment) 0%, var(--parchment-2) 100%); }
721
+ .scrubber .tick.lap-start { background:linear-gradient(180deg, rgba(140,107,42,.18) 0%, transparent 100%); }
722
+ .scrubber .marker { box-shadow:0 0 6px rgba(200,57,46,.5); }
723
+ .scrubber .scrub-meta { font-weight:700; letter-spacing:.16em; }
724
+
725
+ /* ---- Batch / balance modal refinement ---- */
726
+ .batch-modal { box-shadow:var(--shadow-deep); }
727
+ .batch-modal .bm-head .ttl { letter-spacing:-.005em; }
728
+ .batch-modal .bm-head .ttl::before { content:"§ "; color:var(--foil); opacity:.75; font-weight:400; }
729
+ .batch-modal .ctrl-block { background:linear-gradient(180deg, var(--parchment-2) 0%, var(--parchment) 100%); }
730
+ .batch-modal .ctrl-block .lbl { color:var(--ink); }
731
+ .batch-modal .ctrl-row label.sel { background:var(--ink); color:var(--parchment); box-shadow:inset 0 0 0 1px var(--foil-soft); }
732
+ .batch-modal .progress-row { background:var(--ink); }
733
+ .batch-modal .progress-row .pct { color:var(--foil-soft); }
734
+ .batch-modal .progress-bar > .fill { background:linear-gradient(90deg, var(--severity-pass) 0%, #58B19F 100%); }
735
+
736
+ /* Resume pill: ink panel with foil accent */
737
+ .resume-pill { box-shadow:0 0 0 1px var(--foil-soft) inset, var(--shadow-deep); }
738
+ .resume-pill .lbl::before { content:"§ "; color:var(--foil); opacity:.75; }
739
+
740
+ /* Orientation overlay: foil border */
741
+ .orient-panel { box-shadow:0 0 0 1px var(--foil-soft) inset, var(--shadow-deep); }
742
+ .orient-panel h2 { letter-spacing:-.01em; }
743
+ .orient-panel ol li::before { box-shadow:0 0 0 1px var(--foil-soft) inset; }
744
+
745
+ /* ---- Accessibility: keyboard focus, sr-only ---- */
746
+ button:focus-visible, select:focus-visible, input:focus-visible { outline:2.5px solid var(--foil); outline-offset:2px; }
747
+ .sr-only { position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }
748
+
749
+ /* ---- Responsive ≤768px ---- */
750
+ @media (max-width: 768px) {
751
+ .topbar { padding:10px 14px; }
752
+ .brand .title { font-size:22px; }
753
+ .brand .title .title-mode { display:none; }
754
+ .brand .sub { font-size:11px; line-height:1.35; }
755
+ .grid { padding:10px 12px; gap:10px; }
756
+ .board-view { max-width:none; }
757
+ .ledger { font-size:9.5px; }
758
+ .ledger .row { grid-template-columns:60px 100px 1fr; gap:6px; }
759
+ .orient-panel { padding:18px 20px; }
760
+ .orient-panel h2 { font-size:22px; }
761
+ .batch-modal .bm-controls { grid-template-columns:1fr; }
762
+ .endgame .results { grid-template-columns:1fr; }
763
+ }
764
+
765
+ /* ---- Print polish ---- */
766
+ @media print {
767
+ .board-view { box-shadow:none; max-width:none; }
768
+ .endgame { padding:18px 24px; }
769
+ .endgame .winner { background:#fff !important; color:var(--ink) !important; border:2px solid var(--ink); }
770
+ .endgame .winner::after { display:none; }
771
+ .endgame .winner .lbl, .endgame .winner .score { color:var(--ink) !important; }
772
+ .endgame-narration { background:#fff; }
773
+ .ledger { background:#fff; }
774
+ .ledger .row.row-default, .ledger .row.row-rebellion { background:#fff !important; color:var(--ink) !important; border:1px solid var(--ink); }
775
+ .ledger .row.row-default .actor, .ledger .row.row-rebellion .actor { color:var(--ink) !important; }
776
+ }
777
+
778
+ /* =====================================================================
779
+ v0.18 PLAYTEST FIXES — readability + action banner + player picker
780
+ No mechanic changes. CSS + render layer only.
781
+ ===================================================================== */
782
+
783
+ /* Bump base type so the whole game reads larger */
784
+ html, body { font-size:15px; }
785
+
786
+ /* Board cell legibility — bigger names + numbers */
787
+ .board-cell .nm { font-size:10.5px; line-height:1.15; padding-top:6px; font-weight:500; }
788
+ .board-cell .num { font-size:9.5px; opacity:.7; font-weight:600; }
789
+ .board-cell.corner .nm { font-size:11px; padding-top:24px; }
790
+ .board-cell .owner-dot { width:13px; height:13px; }
791
+ .board-cell .tier { font-size:10px; padding:1px 5px; top:8px; }
792
+ .board-cell .tokens .tok { width:13px; height:13px; }
793
+ .board-cell .tokens .tok.glyph { font-family:var(--display); font-size:10px; font-weight:700; color:#fff; line-height:13px; text-align:center; display:inline-block; text-shadow:0 1px 0 rgba(0,0,0,.4); }
794
+
795
+ /* Board center plaque */
796
+ .bc-eyebrow { font-size:9px; letter-spacing:.32em; }
797
+ .bc-title { font-size:26px; }
798
+ .bc-sub { font-size:11.5px; }
799
+ .bc-lap { font-size:11px; padding:4px 12px; }
800
+ .bc-tracks .t { font-size:10px; padding:3px 9px; }
801
+ .bc-tracks .t .v { font-size:12px; margin-left:5px; }
802
+ .bc-acts { font-size:9px; }
803
+ .bc-active { font-size:11px; padding:3px 10px; }
804
+
805
+ /* Panel heads — read more easily */
806
+ .panel .panel-head .name { font-size:10.5px; letter-spacing:.22em; }
807
+ .panel .panel-head .surface-id { font-size:10px; padding:2px 7px; }
808
+
809
+ /* Treasury — louder cash, clearer holdings */
810
+ .treasury .cash { font-size:28px; }
811
+ .treasury .lbl { font-size:10px; letter-spacing:.18em; }
812
+ .treasury .role-tag { font-size:9.5px; padding:2px 8px; }
813
+ .treasury .inf { font-size:10.5px; margin-top:6px; }
814
+ .treasury .h-row { font-size:12px; padding:6px 6px; }
815
+ .treasury .tier-badge { font-size:10px; padding:1px 6px; }
816
+ .treasury .up-btn { font-size:9.5px; padding:4px 9px; }
817
+
818
+ /* Opponents — clearer cards */
819
+ .opp-card { padding:10px 12px 10px 16px; }
820
+ .opp-card .head .nm { font-size:14.5px; }
821
+ .opp-card .head .pdot { width:14px; height:14px; }
822
+ .opp-card .pl { font-size:9.5px; letter-spacing:.18em; }
823
+ .opp-card .cash { font-size:16px; }
824
+ .opp-card .holdings { font-size:11.5px; line-height:1.5; }
825
+ .opp-card .strategy { font-size:11.5px; line-height:1.5; }
826
+
827
+ /* Tracks — bigger values, larger scale */
828
+ .tracks-panel .lbl-t { font-size:10.5px; }
829
+ .tracks-panel .val { font-size:22px; }
830
+ .tracks-panel .scale { height:24px; }
831
+ .tracks-panel .tk { font-size:10px; }
832
+ .tracks-panel .reason { font-size:10px; }
833
+
834
+ /* Acts — readable vote rows */
835
+ .acts-panel .pretitle { font-size:9px; }
836
+ .acts-panel .nm { font-size:19px; }
837
+ .acts-panel .effect { font-size:11.5px; }
838
+ .acts-panel .vote-row { padding:6px 10px; font-size:11.5px; }
839
+ .acts-panel .vote-row .who { font-size:10px; }
840
+ .acts-panel .vote-row .reason { font-size:11px; }
841
+ .acts-panel .vote-row .ballot { font-size:14px; padding:3px 10px; min-width:46px; }
842
+ .acts-panel .tally { font-size:11px; padding:5px 9px; }
843
+ .acts-panel .slot { font-size:11px; padding:4px 9px 4px 19px; }
844
+
845
+ /* Inspector + Card drawer — bigger card body */
846
+ .inspector .nm, .card-drawer .nm, .auction .nm { font-size:18px; }
847
+ .inspector .subkind { font-size:9.5px; }
848
+ .inspector .flavor, .card-drawer .alert { font-size:11.5px; line-height:1.5; }
849
+ .inspector .pay-table th, .inspector .pay-table td { font-size:10.5px; padding:4px 7px; }
850
+ .inspector .pay-table td.val { font-size:12.5px; }
851
+ .inspector .meta-row { font-size:10.5px; }
852
+ .card-drawer .effect { font-size:13px; }
853
+ .card-drawer .chip { font-size:10px; padding:3px 9px; }
854
+
855
+ /* Auction — readable bids */
856
+ .auction .bid-row { font-size:11.5px; padding:5px 8px; grid-template-columns:14px 92px 70px 1fr; }
857
+ .auction .bid-row .who { font-size:10px; }
858
+ .auction .bid-row .amt { font-size:14.5px; }
859
+ .auction .bid-row .reason { font-size:10.5px; }
860
+ .auction .high-bid { font-size:11.5px; padding:6px 10px; }
861
+
862
+ /* Ledger — readable rows */
863
+ .ledger { font-size:11px; }
864
+ .ledger .head { font-size:10.5px; }
865
+ .ledger .row { padding:4px 0 5px; grid-template-columns:96px 150px 1fr; gap:10px; }
866
+ .ledger .row .stamp { font-size:10px; }
867
+ .ledger .row .actor { font-size:10.5px; }
868
+
869
+ /* Topbar pills + controls — louder */
870
+ .controls .seed-pill, .controls .phase-pill, .controls .active-pill, .controls .version-pill { font-size:10.5px; padding:6px 11px; }
871
+ .controls button { font-size:11.5px; padding:7px 14px; letter-spacing:.1em; }
872
+
873
+ /* =====================================================================
874
+ ACTION BANNER — always tells the user what to do next
875
+ ===================================================================== */
876
+ .action-banner {
877
+ display:flex;
878
+ align-items:center;
879
+ justify-content:space-between;
880
+ gap:16px;
881
+ padding:14px 20px;
882
+ background:linear-gradient(180deg, var(--parchment) 0%, var(--parchment-2) 100%);
883
+ border:2px solid var(--ink);
884
+ box-shadow:inset 0 0 0 1px var(--foil-soft), 0 3px 0 rgba(26,22,18,0.18), 0 8px 18px -10px rgba(26,22,18,.4);
885
+ position:relative;
886
+ margin:4px 0 2px;
887
+ min-height:64px;
888
+ flex-wrap:wrap;
889
+ }
890
+ @media (max-width: 1200px) {
891
+ .action-banner { flex-direction:column; align-items:stretch; gap:10px; padding:14px 18px; }
892
+ .action-banner .ab-actions { justify-content:flex-start; }
893
+ }
894
+ .action-banner::before {
895
+ content:"";
896
+ position:absolute; left:0; right:0; top:0; height:3px;
897
+ background:linear-gradient(90deg, var(--foil) 0%, #E5C36A 50%, var(--foil) 100%);
898
+ }
899
+ .action-banner.opponent-turn {
900
+ background:var(--parchment-2);
901
+ box-shadow:inset 0 0 0 1px var(--rule-soft);
902
+ border-color:var(--rule);
903
+ border-width:1px;
904
+ min-height:48px;
905
+ padding:10px 18px;
906
+ }
907
+ .action-banner.opponent-turn::before { display:none; }
908
+
909
+ .action-banner .ab-text { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
910
+ .action-banner .ab-prompt {
911
+ font-family:var(--display);
912
+ font-size:18px;
913
+ line-height:1.15;
914
+ letter-spacing:-.005em;
915
+ font-weight:400;
916
+ color:var(--ink);
917
+ }
918
+ .action-banner.opponent-turn .ab-prompt { font-style:italic; opacity:.75; font-size:14px; }
919
+ .action-banner .ab-hint {
920
+ font-family:var(--body);
921
+ font-size:13px;
922
+ line-height:1.4;
923
+ opacity:.78;
924
+ }
925
+ .action-banner .ab-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
926
+ .action-banner .ab-actions button {
927
+ font-family:var(--ui);
928
+ font-size:12.5px;
929
+ letter-spacing:.12em;
930
+ text-transform:uppercase;
931
+ font-weight:700;
932
+ padding:10px 18px;
933
+ border:1.5px solid var(--ink);
934
+ background:var(--parchment);
935
+ color:var(--ink);
936
+ cursor:pointer;
937
+ box-shadow:0 2px 0 rgba(26,22,18,.18);
938
+ transition:transform 80ms ease, box-shadow 80ms ease;
939
+ }
940
+ .action-banner .ab-actions button:hover:not([disabled]) { transform:translateY(-1px); box-shadow:0 3px 0 rgba(26,22,18,.22); }
941
+ .action-banner .ab-actions button:active:not([disabled]) { transform:translateY(1px); box-shadow:0 1px 0 rgba(26,22,18,.18); }
942
+ .action-banner .ab-actions button.primary { background:var(--ink); color:var(--parchment); box-shadow:0 2px 0 rgba(26,22,18,.3); }
943
+ .action-banner .ab-actions button.primary:hover:not([disabled]) { background:var(--national-finance); box-shadow:0 3px 0 rgba(26,22,18,.35); }
944
+ .action-banner .ab-actions button:focus-visible { outline:2.5px solid var(--foil); outline-offset:3px; }
945
+ .action-banner .ab-actions button[disabled] { opacity:.45; cursor:not-allowed; box-shadow:none; }
946
+
947
+ .controls-bar { padding:6px 0 8px; min-height:60px; }
948
+
949
+ /* =====================================================================
950
+ PLAYER PICKER (in orientation overlay)
951
+ ===================================================================== */
952
+ .orient-panel .pick-section {
953
+ margin:14px 0 0;
954
+ padding:14px 16px;
955
+ background:var(--parchment-2);
956
+ border:1.5px solid var(--ink);
957
+ position:relative;
958
+ }
959
+ .orient-panel .pick-section::before { content:""; position:absolute; left:0; right:0; top:0; height:2px; background:var(--foil-soft); }
960
+ .orient-panel .pick-section h3 {
961
+ font-family:var(--ui);
962
+ font-size:10.5px;
963
+ letter-spacing:.26em;
964
+ text-transform:uppercase;
965
+ font-weight:700;
966
+ margin:0 0 10px;
967
+ color:var(--foil);
968
+ }
969
+ .orient-panel .pick-row { display:flex; gap:14px; align-items:flex-start; flex-wrap:wrap; }
970
+ .orient-panel .pick-field { flex:1; min-width:180px; display:flex; flex-direction:column; gap:4px; }
971
+ .orient-panel .pick-field label {
972
+ font-family:var(--ui);
973
+ font-size:10px;
974
+ letter-spacing:.18em;
975
+ text-transform:uppercase;
976
+ font-weight:700;
977
+ color:var(--ink);
978
+ }
979
+ .orient-panel .pick-field input[type="text"] {
980
+ font-family:var(--display);
981
+ font-size:18px;
982
+ padding:8px 12px;
983
+ border:1.5px solid var(--ink);
984
+ background:var(--parchment);
985
+ color:var(--ink);
986
+ width:100%;
987
+ }
988
+ .orient-panel .pick-field input[type="text"]:focus-visible { outline:2.5px solid var(--foil); outline-offset:2px; }
989
+ .glyph-picker { display:flex; gap:6px; flex-wrap:wrap; margin-top:2px; }
990
+ .glyph-picker button {
991
+ width:42px; height:42px;
992
+ border:1.5px solid var(--ink);
993
+ background:var(--parchment);
994
+ color:var(--ink);
995
+ font-family:var(--display);
996
+ font-size:22px;
997
+ line-height:1;
998
+ cursor:pointer;
999
+ display:flex; align-items:center; justify-content:center;
1000
+ transition:transform 80ms ease;
1001
+ }
1002
+ .glyph-picker button:hover { transform:translateY(-1px); background:var(--parchment-2); }
1003
+ .glyph-picker button.sel {
1004
+ background:var(--state-debt);
1005
+ color:#fff;
1006
+ box-shadow:inset 0 0 0 2px var(--ink), 0 2px 0 rgba(26,22,18,.3);
1007
+ }
1008
+ .glyph-picker button:focus-visible { outline:2.5px solid var(--foil); outline-offset:2px; }
1009
+
1010
+ /* Role picker — three cards for Treasury / Merchant / Manufacturer */
1011
+ .role-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:8px; margin:6px 0 4px; }
1012
+ .role-grid .role-card {
1013
+ text-align:left; padding:12px 14px; background:var(--parchment); color:var(--ink);
1014
+ border:1.5px solid var(--ink); cursor:pointer;
1015
+ box-shadow:0 2px 0 rgba(26,22,18,.12);
1016
+ transition:transform 80ms ease, box-shadow 80ms ease, background-color 80ms ease;
1017
+ position:relative;
1018
+ }
1019
+ .role-grid .role-card::before { content:""; position:absolute; left:0; top:0; bottom:0; width:4px; background:var(--rule-soft); }
1020
+ .role-grid .role-card:hover { transform:translateY(-1px); box-shadow:0 3px 0 rgba(26,22,18,.18); }
1021
+ .role-grid .role-card.sel { background:var(--ink); color:var(--parchment); }
1022
+ .role-grid .role-card.sel::before { background:var(--foil); }
1023
+ .role-grid .role-card[data-role="treasury"].sel::before { background:var(--national-finance); }
1024
+ .role-grid .role-card[data-role="merchant"].sel::before { background:var(--commercial-infrastructure); }
1025
+ .role-grid .role-card[data-role="manufacturer"].sel::before { background:var(--manufactures); }
1026
+ .role-grid .role-card .role-eyebrow {
1027
+ font-family:var(--ui); font-size:9px; letter-spacing:.22em; text-transform:uppercase;
1028
+ font-weight:800; color:var(--foil);
1029
+ }
1030
+ .role-grid .role-card.sel .role-eyebrow { color:var(--foil-soft); }
1031
+ .role-grid .role-card .role-title {
1032
+ font-family:var(--display); font-size:15px; line-height:1.1; margin-top:3px; font-weight:400;
1033
+ }
1034
+ .role-grid .role-card .role-blurb {
1035
+ font-family:var(--body); font-size:11.5px; line-height:1.45; margin-top:6px; opacity:.85;
1036
+ }
1037
+ .role-grid .role-card:focus-visible { outline:2.5px solid var(--foil); outline-offset:2px; }
1038
+
1039
+ /* Player token glyph on board (Unicode character inside the dot) */
1040
+ .board-cell .tokens .tok {
1041
+ display:flex; align-items:center; justify-content:center;
1042
+ font-family:var(--display); color:#fff; font-weight:700;
1043
+ font-size:9px; line-height:1; text-shadow:0 1px 0 rgba(0,0,0,.45);
1044
+ }
1045
+ /* =====================================================================
1046
+ HUMAN PLAYABILITY REBUILD — overrides + new layers
1047
+ ===================================================================== */
1048
+
1049
+ /* Bigger board: push away from "panel" feel toward "this is the game" */
1050
+ .board-view { max-width:820px; }
1051
+
1052
+ /* Board space typography — readable at a glance, centered in each cell */
1053
+ .board-cell { padding:5px 6px 6px; text-align:center; align-items:center; }
1054
+ .board-cell .nm { font-size:11.5px; line-height:1.16; padding-top:7px; font-weight:600; text-align:center; text-wrap:balance; }
1055
+ .board-cell .num { font-size:9.5px; opacity:.7; text-align:center; }
1056
+ .board-cell.corner .nm { font-size:11.5px; padding-top:26px; font-weight:700; text-align:center; }
1057
+
1058
+ /* Owner-mark: small initial/sigil badge in the lower-right of an owned tile.
1059
+ Distinct from the player's pawn-token; signals ownership without re-using shape. */
1060
+ .board-cell .owner-mark {
1061
+ position:absolute; bottom:3px; right:3px;
1062
+ width:15px; height:15px;
1063
+ display:flex; align-items:center; justify-content:center;
1064
+ font-family:var(--display); font-weight:800; font-size:10px;
1065
+ color:#fff; text-shadow:0 1px 0 rgba(0,0,0,.45);
1066
+ border:1.5px solid var(--ink);
1067
+ box-shadow:0 1px 2px rgba(26,22,18,.4);
1068
+ }
1069
+ .board-cell.owned-p0 .owner-mark { background:var(--p0); border-radius:50%; }
1070
+ .board-cell.owned-p1 .owner-mark { background:var(--p1); border-radius:3px; transform:rotate(45deg); }
1071
+ .board-cell.owned-p1 .owner-mark > span { transform:rotate(-45deg); display:inline-block; }
1072
+ .board-cell.owned-p2 .owner-mark { background:var(--p2); border-radius:3px; }
1073
+ .board-cell.owned-p0 { box-shadow:inset 0 0 0 2px var(--p0); }
1074
+ .board-cell.owned-p1 { box-shadow:inset 0 0 0 2px var(--p1); }
1075
+ .board-cell.owned-p2 { box-shadow:inset 0 0 0 2px var(--p2); }
1076
+ .board-cell .owner-dot { display:none; } /* replaced by .owner-mark */
1077
+
1078
+ /* Tier marker now sits top-left so it doesn't clash with owner-mark */
1079
+ .board-cell .tier { top:8px; right:auto; left:3px; font-size:10px; padding:1px 5px; }
1080
+
1081
+ /* Player pawns on the board — bigger, with initial/sigil inside */
1082
+ .board-cell .tokens { gap:2px; top:7px; }
1083
+ .board-cell .tokens .tok {
1084
+ width:18px; height:18px;
1085
+ display:flex; align-items:center; justify-content:center;
1086
+ font-family:var(--display); color:#fff; font-weight:800;
1087
+ font-size:11px; line-height:1; text-shadow:0 1px 0 rgba(0,0,0,.55);
1088
+ box-shadow:0 1px 3px rgba(26,22,18,.45);
1089
+ border-width:1.5px;
1090
+ }
1091
+ .board-cell .tokens .tok.p1 > span,
1092
+ .board-cell.owned-p1 .owner-mark > span { transform:rotate(-45deg); display:inline-block; line-height:1; }
1093
+
1094
+ /* HUMAN PLAYABILITY ROUND 3 — make player positions unmistakable */
1095
+ .board-cell .tokens {
1096
+ /* Center pawns horizontally at the bottom of the cell, overlapping cell name */
1097
+ position:absolute; top:auto; left:50%; transform:translateX(-50%); bottom:4px;
1098
+ display:flex; gap:3px; z-index:5;
1099
+ }
1100
+ .board-cell .tokens .tok {
1101
+ width:26px; height:26px;
1102
+ font-size:15px; font-weight:800;
1103
+ border:2.5px solid var(--ink);
1104
+ box-shadow:0 2px 4px rgba(26,22,18,.55), 0 0 0 2px rgba(255,255,255,.55) inset;
1105
+ }
1106
+ /* The active player's token pulses with a foil halo so it really pops */
1107
+ .board-cell.active-space .tokens .tok.active-pawn {
1108
+ animation:activePawnPulse 1400ms ease-in-out infinite;
1109
+ box-shadow:0 2px 4px rgba(26,22,18,.55), 0 0 0 2px rgba(255,255,255,.55) inset, 0 0 0 4px var(--foil), 0 0 14px 4px rgba(140,107,42,.5);
1110
+ }
1111
+ @keyframes activePawnPulse {
1112
+ 0%, 100% { transform:scale(1); }
1113
+ 50% { transform:scale(1.12); }
1114
+ }
1115
+ /* "Your position" pointer — a Federalist gilt chevron above the active player's space */
1116
+ .board-cell.you-here::before {
1117
+ content:"";
1118
+ position:absolute; top:-14px; left:50%; transform:translateX(-50%);
1119
+ width:0; height:0;
1120
+ border-left:9px solid transparent; border-right:9px solid transparent;
1121
+ border-top:10px solid var(--foil);
1122
+ z-index:6;
1123
+ filter:drop-shadow(0 1px 1px rgba(0,0,0,.4));
1124
+ }
1125
+ .board-cell.you-here::after {
1126
+ content:"YOU";
1127
+ position:absolute; top:-30px; left:50%; transform:translateX(-50%);
1128
+ font-family:var(--ui); font-size:9px; font-weight:800;
1129
+ letter-spacing:.18em; color:var(--foil);
1130
+ background:var(--ink); padding:2px 7px;
1131
+ border:1px solid var(--foil);
1132
+ white-space:nowrap;
1133
+ z-index:6;
1134
+ }
1135
+ /* The active-space outline becomes solid foil instead of red — so "active" reads "you" */
1136
+ .board-cell.active-space { outline-color:var(--foil); }
1137
+ .board-cell.you-here.active-space { outline-color:var(--foil); }
1138
+ /* Opponent active space stays red so it's distinct */
1139
+ .board-cell.opponent-active { outline:3px solid var(--highlight); outline-offset:-3px; }
1140
+ .board-cell.opponent-active::before {
1141
+ content:""; position:absolute; top:-14px; left:50%; transform:translateX(-50%);
1142
+ width:0; height:0;
1143
+ border-left:8px solid transparent; border-right:8px solid transparent;
1144
+ border-top:9px solid var(--highlight);
1145
+ z-index:6; filter:drop-shadow(0 1px 1px rgba(0,0,0,.35));
1146
+ }
1147
+ .board-cell.opponent-active::after {
1148
+ content:attr(data-opp-label);
1149
+ position:absolute; top:-28px; left:50%; transform:translateX(-50%);
1150
+ font-family:var(--ui); font-size:8.5px; font-weight:700;
1151
+ letter-spacing:.16em; color:#fff;
1152
+ background:var(--highlight); padding:2px 6px;
1153
+ border:1px solid var(--ink);
1154
+ white-space:nowrap; z-index:6;
1155
+ }
1156
+
1157
+ /* Positions status line — printed above the action rail so you always know
1158
+ where everyone is, even when the board is tight */
1159
+ .positions-strip {
1160
+ display:flex; gap:18px; flex-wrap:wrap;
1161
+ padding:8px 14px;
1162
+ background:var(--parchment-2);
1163
+ border:1px solid var(--ink);
1164
+ margin:0 0 6px;
1165
+ font-family:var(--ui); font-size:11px;
1166
+ }
1167
+ .positions-strip .pos-row { display:flex; align-items:center; gap:8px; }
1168
+ .positions-strip .pos-tok {
1169
+ width:22px; height:22px; border:2px solid var(--ink);
1170
+ display:flex; align-items:center; justify-content:center;
1171
+ font-family:var(--display); color:#fff; font-weight:800;
1172
+ font-size:12px; line-height:1;
1173
+ box-shadow:0 1px 2px rgba(0,0,0,.35);
1174
+ }
1175
+ .positions-strip .pos-row.p0 .pos-tok { background:var(--p0); border-radius:50%; }
1176
+ .positions-strip .pos-row.p1 .pos-tok { background:var(--p1); border-radius:3px; transform:rotate(45deg); }
1177
+ .positions-strip .pos-row.p1 .pos-tok > span { transform:rotate(-45deg); display:inline-block; }
1178
+ .positions-strip .pos-row.p2 .pos-tok { background:var(--p2); border-radius:3px; }
1179
+ .positions-strip .pos-name { font-weight:700; letter-spacing:.06em; text-transform:uppercase; font-size:10px; }
1180
+ .positions-strip .pos-space {
1181
+ font-family:var(--display); font-style:italic; font-weight:400;
1182
+ font-size:13px; letter-spacing:0; text-transform:none;
1183
+ }
1184
+ .positions-strip .pos-row.is-active .pos-name { color:var(--foil); }
1185
+ .positions-strip .pos-row.is-active .pos-tok { box-shadow:0 0 0 2px var(--foil), 0 1px 2px rgba(0,0,0,.4); }
1186
+ .positions-strip .pos-ip {
1187
+ font-family:var(--ui); font-size:10.5px; letter-spacing:.06em;
1188
+ font-weight:800; color:var(--foil); margin-left:4px;
1189
+ }
1190
+ .positions-strip .pos-mandate {
1191
+ font-family:var(--ui); font-size:9.5px; letter-spacing:.12em;
1192
+ text-transform:uppercase; font-weight:800;
1193
+ margin-top:3px;
1194
+ padding:2px 7px;
1195
+ background:var(--foil); color:var(--ink);
1196
+ border:1px solid var(--ink); display:inline-block;
1197
+ }
1198
+
1199
+ /* =====================================================================
1200
+ PORTFOLIO STRIP — compact "your holdings" near the action rail
1201
+ ===================================================================== */
1202
+ .portfolio-strip {
1203
+ display:flex; gap:14px; align-items:stretch; flex-wrap:wrap;
1204
+ padding:9px 14px;
1205
+ background:var(--parchment);
1206
+ border:1px solid var(--ink);
1207
+ border-top:0;
1208
+ margin:0 0 6px;
1209
+ }
1210
+ .portfolio-strip .pf-head {
1211
+ display:flex; flex-direction:column; gap:2px; min-width:118px;
1212
+ }
1213
+ .portfolio-strip .pf-eyebrow {
1214
+ font-family:var(--ui); font-size:9px; letter-spacing:.22em;
1215
+ text-transform:uppercase; color:var(--foil); font-weight:800;
1216
+ }
1217
+ .portfolio-strip .pf-eyebrow::before { content:"§ "; }
1218
+ .portfolio-strip .pf-line {
1219
+ font-family:var(--display); font-size:14px; line-height:1.15;
1220
+ letter-spacing:-.005em; font-weight:400;
1221
+ }
1222
+ .portfolio-strip .pf-cash {
1223
+ font-family:var(--mono); font-size:11.5px; font-weight:700;
1224
+ color:var(--ink); margin-top:2px;
1225
+ }
1226
+ .portfolio-strip .pf-cash::before { content:"₸ "; color:var(--foil); opacity:.7; }
1227
+ .portfolio-strip .pf-list {
1228
+ display:flex; gap:6px; flex-wrap:wrap; align-items:center;
1229
+ flex:1; min-width:0;
1230
+ }
1231
+ .portfolio-strip .pf-empty {
1232
+ font-family:var(--display); font-style:italic; font-size:13px;
1233
+ opacity:.7; align-self:center;
1234
+ }
1235
+ .portfolio-strip .pf-chip {
1236
+ display:inline-flex; align-items:center; gap:6px;
1237
+ padding:5px 10px;
1238
+ background:var(--parchment-2);
1239
+ border:1px solid var(--ink);
1240
+ font-family:var(--display); font-size:12.5px; font-weight:400;
1241
+ line-height:1.1;
1242
+ position:relative; padding-left:14px;
1243
+ }
1244
+ .portfolio-strip .pf-chip::before {
1245
+ content:""; position:absolute; left:0; top:0; bottom:0; width:5px;
1246
+ }
1247
+ .portfolio-strip .pf-chip[data-sys="revolutionary-debt"]::before { background:var(--revolutionary-debt); }
1248
+ .portfolio-strip .pf-chip[data-sys="state-debt"]::before { background:var(--state-debt); }
1249
+ .portfolio-strip .pf-chip[data-sys="revenue-system"]::before { background:var(--revenue-system); }
1250
+ .portfolio-strip .pf-chip[data-sys="commercial-infrastructure"]::before { background:var(--commercial-infrastructure); }
1251
+ .portfolio-strip .pf-chip[data-sys="national-finance"]::before { background:var(--national-finance); }
1252
+ .portfolio-strip .pf-chip[data-sys="internal-improvements"]::before { background:var(--internal-improvements); }
1253
+ .portfolio-strip .pf-chip[data-sys="manufactures"]::before { background:var(--manufactures); }
1254
+ .portfolio-strip .pf-chip[data-sys="strategic-industry"]::before { background:var(--strategic-industry); }
1255
+ .portfolio-strip .pf-chip[data-kind="route"]::before { background:var(--ink); background-image:repeating-linear-gradient(90deg,transparent 0 3px,rgba(255,255,255,.5) 3px 4px); }
1256
+ .portfolio-strip .pf-chip[data-kind="institution"]::before { background:var(--ink); background-image:radial-gradient(rgba(255,255,255,.6) .6px,transparent 1.1px); background-size:3px 3px; }
1257
+ .portfolio-strip .pf-chip .tier-roman {
1258
+ font-family:var(--display); font-weight:800; font-size:10.5px;
1259
+ background:var(--foil); color:var(--ink); padding:1px 5px;
1260
+ border:1px solid var(--ink); border-radius:2px; letter-spacing:.04em;
1261
+ }
1262
+ .portfolio-strip .pf-open {
1263
+ font-family:var(--ui); font-size:10.5px; letter-spacing:.16em;
1264
+ text-transform:uppercase; font-weight:800;
1265
+ padding:7px 12px; border:1.5px solid var(--ink); background:var(--parchment);
1266
+ color:var(--ink); cursor:pointer;
1267
+ box-shadow:0 2px 0 rgba(26,22,18,.18);
1268
+ align-self:center;
1269
+ }
1270
+ .portfolio-strip .pf-open:hover { background:var(--ink); color:var(--parchment); }
1271
+ .portfolio-strip .pf-open:focus-visible { outline:2.5px solid var(--foil); outline-offset:2px; }
1272
+
1273
+ /* =====================================================================
1274
+ LANDING OUTCOME BAND — explains what just happened on your roll
1275
+ ===================================================================== */
1276
+ .landing-band {
1277
+ display:flex; gap:14px; align-items:center;
1278
+ padding:10px 16px;
1279
+ background:var(--parchment);
1280
+ border:1.5px solid var(--ink);
1281
+ border-top:0;
1282
+ margin:0 0 6px;
1283
+ position:relative;
1284
+ box-shadow:inset 0 0 0 1px var(--foil-soft);
1285
+ }
1286
+ .landing-band::before {
1287
+ content:""; position:absolute; left:0; right:0; top:-1px; height:2px;
1288
+ background:linear-gradient(90deg, var(--foil) 0%, #E5C36A 50%, var(--foil) 100%);
1289
+ }
1290
+ .landing-band .lb-eyebrow {
1291
+ font-family:var(--ui); font-size:9px; letter-spacing:.26em;
1292
+ text-transform:uppercase; font-weight:800; color:var(--foil);
1293
+ white-space:nowrap;
1294
+ }
1295
+ .landing-band .lb-eyebrow::before { content:"§ "; }
1296
+ .landing-band .lb-text {
1297
+ font-family:var(--display); font-size:15px; line-height:1.25;
1298
+ letter-spacing:-.005em; font-weight:400; color:var(--ink);
1299
+ flex:1;
1300
+ }
1301
+ .landing-band .lb-text em { font-style:italic; }
1302
+ .landing-band.is-paid { background:rgba(200,57,46,.06); }
1303
+ .landing-band.is-paid .lb-text strong { color:var(--highlight); font-weight:700; font-family:var(--display); }
1304
+ .landing-band.is-own { background:rgba(46,122,107,.08); }
1305
+ .landing-band.is-own .lb-text strong { color:var(--severity-pass); font-weight:700; font-family:var(--display); }
1306
+ .landing-band.is-neutral { background:var(--parchment-2); }
1307
+
1308
+ /* =====================================================================
1309
+ FIRST-LAP HINT — extra calm guidance under the rail on lap 1 only
1310
+ ===================================================================== */
1311
+ .action-banner .first-lap-hint {
1312
+ grid-column:1 / -1;
1313
+ margin-top:10px;
1314
+ padding:10px 14px;
1315
+ background:rgba(140,107,42,.10);
1316
+ border-left:3px solid var(--foil);
1317
+ font-family:var(--body); font-size:12.5px; line-height:1.5;
1318
+ font-style:italic; color:var(--ink);
1319
+ }
1320
+ .action-banner .first-lap-hint::before {
1321
+ content:"§ Helping hand: "; font-style:normal;
1322
+ font-family:var(--ui); font-size:9.5px; letter-spacing:.16em;
1323
+ text-transform:uppercase; font-weight:800; color:var(--foil);
1324
+ margin-right:4px;
1325
+ }
1326
+
1327
+ /* =====================================================================
1328
+ ACTION RAIL — primary interaction surface
1329
+ ===================================================================== */
1330
+ .controls-bar { padding:10px 0 4px; min-height:128px; align-items:stretch; flex-direction:column; }
1331
+ .action-banner {
1332
+ display:grid;
1333
+ grid-template-columns:1fr auto;
1334
+ gap:18px;
1335
+ padding:16px 22px 18px;
1336
+ align-items:center;
1337
+ background:linear-gradient(180deg, var(--parchment) 0%, var(--parchment-2) 100%);
1338
+ border:2px solid var(--ink);
1339
+ box-shadow:inset 0 0 0 1px var(--foil-soft), 0 4px 0 rgba(26,22,18,0.18), 0 12px 22px -10px rgba(26,22,18,.4);
1340
+ position:relative;
1341
+ min-height:96px;
1342
+ }
1343
+ .action-banner::before {
1344
+ content:""; position:absolute; left:0; right:0; top:0; height:4px;
1345
+ background:linear-gradient(90deg, var(--foil) 0%, #E5C36A 50%, var(--foil) 100%);
1346
+ }
1347
+ .action-banner .ab-text { display:flex; flex-direction:column; gap:4px; min-width:0; }
1348
+ .action-banner .ab-eyebrow {
1349
+ font-family:var(--ui); font-size:10px; letter-spacing:.26em; text-transform:uppercase;
1350
+ color:var(--foil); font-weight:800;
1351
+ }
1352
+ .action-banner .ab-prompt {
1353
+ font-family:var(--display); font-size:22px; line-height:1.15;
1354
+ letter-spacing:-.005em; color:var(--ink); font-weight:400;
1355
+ }
1356
+ .action-banner .ab-prompt em { font-style:italic; font-weight:400; }
1357
+ .action-banner .ab-hint {
1358
+ font-family:var(--body); font-size:13.5px; line-height:1.45; opacity:.78;
1359
+ max-width:54ch;
1360
+ }
1361
+ .action-banner .ab-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; justify-content:flex-end; }
1362
+ .action-banner .ab-actions button {
1363
+ font-family:var(--ui); font-size:13px; letter-spacing:.14em; text-transform:uppercase; font-weight:800;
1364
+ padding:13px 22px; border:1.5px solid var(--ink); background:var(--parchment); color:var(--ink);
1365
+ cursor:pointer; box-shadow:0 3px 0 rgba(26,22,18,.2);
1366
+ transition:transform 80ms ease, box-shadow 80ms ease, background-color 80ms ease;
1367
+ }
1368
+ .action-banner .ab-actions button:hover:not([disabled]) { transform:translateY(-1px); box-shadow:0 4px 0 rgba(26,22,18,.25); }
1369
+ .action-banner .ab-actions button:active:not([disabled]) { transform:translateY(1px); box-shadow:0 1px 0 rgba(26,22,18,.18); }
1370
+ .action-banner .ab-actions button.primary {
1371
+ background:var(--ink); color:var(--parchment); box-shadow:0 3px 0 rgba(26,22,18,.35);
1372
+ font-family:var(--display); font-weight:700; letter-spacing:.06em; text-transform:none; font-size:17px;
1373
+ padding:14px 26px;
1374
+ }
1375
+ .action-banner .ab-actions button.primary:hover:not([disabled]) { background:var(--national-finance); }
1376
+ .action-banner .ab-actions button:focus-visible { outline:2.5px solid var(--foil); outline-offset:3px; }
1377
+ .action-banner .ab-actions button[disabled] { opacity:.4; cursor:not-allowed; box-shadow:none; }
1378
+ .action-banner .ab-actions input[type="number"] {
1379
+ font-family:var(--mono); font-size:14px; padding:11px 12px; width:108px;
1380
+ border:1.5px solid var(--ink); background:var(--parchment);
1381
+ }
1382
+
1383
+ /* Opponent-turn rail state — quiet */
1384
+ .action-banner.opponent-turn {
1385
+ background:var(--parchment-2);
1386
+ box-shadow:inset 0 0 0 1px var(--rule-soft);
1387
+ border-color:var(--rule);
1388
+ border-width:1px;
1389
+ min-height:64px;
1390
+ padding:12px 20px;
1391
+ }
1392
+ .action-banner.opponent-turn::before { display:none; }
1393
+ .action-banner.opponent-turn .ab-prompt { font-style:italic; opacity:.78; font-size:16px; }
1394
+ .action-banner.opponent-turn .ab-eyebrow { color:var(--neutral); }
1395
+
1396
+ /* Crisis-fired rail state — warning red */
1397
+ .action-banner.crisis-rail { border-color:var(--severity-warning); }
1398
+ .action-banner.crisis-rail::before { background:linear-gradient(90deg, var(--severity-warning) 0%, #E66B5F 50%, var(--severity-warning) 100%); }
1399
+ .action-banner.crisis-rail .ab-eyebrow { color:var(--severity-warning); }
1400
+
1401
+ @media (max-width: 1200px) {
1402
+ .action-banner { grid-template-columns:1fr; gap:12px; }
1403
+ .action-banner .ab-actions { justify-content:flex-start; }
1404
+ }
1405
+
1406
+ /* =====================================================================
1407
+ DIM-DURING-DECISION — quiet side panels when human action is pending
1408
+ ===================================================================== */
1409
+ body.decision-active .panel-pane { opacity:.72; transition:opacity 200ms ease; }
1410
+ body.decision-active .panel-pane:hover { opacity:.95; }
1411
+ body.decision-active .ledger-pane { opacity:.62; transition:opacity 200ms ease; }
1412
+ body.decision-active .ledger-pane:hover { opacity:.92; }
1413
+
1414
+ /* =====================================================================
1415
+ WELCOME FLOW — three steps replacing the old single orient overlay
1416
+ ===================================================================== */
1417
+ .orient-overlay { background:rgba(26,22,18,0.86); }
1418
+ .orient-panel { max-width:640px; padding:34px 40px 28px; }
1419
+ .orient-panel.welcome-step .eyebrow,
1420
+ .orient-panel.welcome-step h2,
1421
+ .orient-panel.welcome-step .lede,
1422
+ .orient-panel.welcome-step .welcome-lede,
1423
+ .orient-panel.welcome-step ol,
1424
+ .orient-panel.welcome-step .crisis-row,
1425
+ .orient-panel.welcome-step .default-row,
1426
+ .orient-panel.welcome-step .rebellion-row,
1427
+ .orient-panel.welcome-step .pick-section,
1428
+ .orient-panel.welcome-step .footer-row,
1429
+ .orient-panel.welcome-step .tour-section { display:none; }
1430
+
1431
+ /* Step indicators */
1432
+ .welcome-progress {
1433
+ display:flex; gap:8px; align-items:center; justify-content:center;
1434
+ margin:0 0 24px;
1435
+ }
1436
+ .welcome-progress .dot { width:8px; height:8px; border-radius:50%; background:var(--rule-soft); border:1px solid var(--ink); }
1437
+ .welcome-progress .dot.sel { background:var(--ink); box-shadow:0 0 0 2px var(--foil-soft); }
1438
+
1439
+ /* Step 1 — Welcome */
1440
+ .orient-panel[data-step="1"] .eyebrow,
1441
+ .orient-panel[data-step="1"] h2,
1442
+ .orient-panel[data-step="1"] .welcome-lede,
1443
+ .orient-panel[data-step="1"] .footer-row { display:block; }
1444
+ .orient-panel[data-step="1"] .footer-row { display:flex; }
1445
+ .orient-panel[data-step="1"] h2 { font-size:38px; line-height:1; margin:8px 0 18px; letter-spacing:-.02em; }
1446
+ .orient-panel[data-step="1"] .welcome-lede {
1447
+ font-family:var(--display); font-style:italic;
1448
+ font-size:20px; line-height:1.45; max-width:30ch;
1449
+ margin:0 auto 28px; text-align:center; color:var(--ink); opacity:.92;
1450
+ }
1451
+ .orient-panel[data-step="1"] { text-align:center; }
1452
+ .orient-panel[data-step="1"] .footer-row { justify-content:center; }
1453
+ .orient-panel[data-step="1"] .eyebrow { text-align:center; }
1454
+
1455
+ /* Step 2 — Delegate picker */
1456
+ .orient-panel[data-step="2"] .eyebrow,
1457
+ .orient-panel[data-step="2"] h2,
1458
+ .orient-panel[data-step="2"] .pick-section,
1459
+ .orient-panel[data-step="2"] .footer-row { display:block; }
1460
+ .orient-panel[data-step="2"] .footer-row { display:flex; }
1461
+ .orient-panel[data-step="2"] h2 { font-size:30px; line-height:1; margin:6px 0 18px; }
1462
+ .orient-panel[data-step="2"] .pick-section { background:transparent; border:0; padding:0; }
1463
+ .orient-panel[data-step="2"] .pick-section::before { display:none; }
1464
+ .orient-panel[data-step="2"] .pick-section h3 { font-size:11px; color:var(--foil); }
1465
+ .orient-panel[data-step="2"] .pick-section .preview-row {
1466
+ display:flex; gap:18px; align-items:center; justify-content:center;
1467
+ margin:16px 0 8px;
1468
+ padding:14px 16px;
1469
+ background:var(--parchment-2);
1470
+ border:1px solid var(--ink);
1471
+ }
1472
+ .preview-token-set { display:flex; gap:14px; align-items:center; }
1473
+ .preview-token { display:flex; flex-direction:column; align-items:center; gap:4px; }
1474
+ .preview-token .ptok {
1475
+ width:38px; height:38px; border:2px solid var(--ink);
1476
+ display:flex; align-items:center; justify-content:center;
1477
+ font-family:var(--display); color:#fff; font-weight:800;
1478
+ font-size:18px; box-shadow:0 2px 0 rgba(26,22,18,.25);
1479
+ }
1480
+ .preview-token.p0 .ptok { background:var(--p0); border-radius:50%; }
1481
+ .preview-token.p1 .ptok { background:var(--p1); border-radius:5px; transform:rotate(45deg); }
1482
+ .preview-token.p1 .ptok > span { transform:rotate(-45deg); display:inline-block; }
1483
+ .preview-token.p2 .ptok { background:var(--p2); border-radius:5px; }
1484
+ .preview-token .ptok-label {
1485
+ font-family:var(--ui); font-size:9.5px; letter-spacing:.16em;
1486
+ text-transform:uppercase; font-weight:700; color:var(--ink);
1487
+ }
1488
+
1489
+ /* Step 3 — On-board tour */
1490
+ .orient-panel[data-step="3"] .eyebrow,
1491
+ .orient-panel[data-step="3"] h2,
1492
+ .orient-panel[data-step="3"] .tour-section,
1493
+ .orient-panel[data-step="3"] .footer-row { display:block; }
1494
+ .orient-panel[data-step="3"] .footer-row { display:flex; }
1495
+ .orient-panel[data-step="3"] h2 { font-size:30px; line-height:1; margin:6px 0 14px; }
1496
+ .orient-panel[data-step="3"] .tour-section { font-family:var(--body); font-size:14px; line-height:1.55; }
1497
+ .orient-panel[data-step="3"] .tour-section .tour-row {
1498
+ display:grid; grid-template-columns:48px 1fr;
1499
+ gap:14px; padding:10px 0 14px;
1500
+ border-bottom:0.5px dashed var(--rule-soft); align-items:center;
1501
+ }
1502
+ .orient-panel[data-step="3"] .tour-section .tour-row:last-child { border-bottom:0; }
1503
+ .orient-panel[data-step="3"] .tour-section .tour-num {
1504
+ width:36px; height:36px; border-radius:50%;
1505
+ background:var(--ink); color:var(--parchment);
1506
+ display:flex; align-items:center; justify-content:center;
1507
+ font-family:var(--display); font-size:16px; font-weight:700;
1508
+ box-shadow:inset 0 0 0 1px var(--foil-soft);
1509
+ }
1510
+ .orient-panel[data-step="3"] .tour-section .tour-row strong {
1511
+ font-family:var(--display); font-weight:700; font-style:normal;
1512
+ }
1513
+
1514
+ /* On-board tour spotlight rings (added when step 3 active) */
1515
+ body.tour-step-tracks .panel.tracks-panel,
1516
+ body.tour-step-rail .controls-bar,
1517
+ body.tour-step-ledger .ledger {
1518
+ box-shadow:0 0 0 3px var(--foil), 0 0 0 5px rgba(140,107,42,.4), var(--shadow-card);
1519
+ position:relative; z-index:2;
1520
+ }
1521
+
1522
+ /* Step 1 of the welcome — also hide the version pill / how-to-play during welcome */
1523
+ body.welcome-active .topbar .controls,
1524
+ body.welcome-active .grid,
1525
+ body.welcome-active .endgame { filter:saturate(.8); }
1526
+
1527
+ /* =====================================================================
1528
+ DESIGNER GATE — hide Balance Sweep + designer chrome by default
1529
+ ===================================================================== */
1530
+ #btnBatch { display:none; }
1531
+ body.designer-mode #btnBatch { display:inline-block; }
1532
+ body.designer-mode::after {
1533
+ content:"Designer mode";
1534
+ position:fixed; bottom:8px; left:8px;
1535
+ font-family:var(--mono); font-size:9px; letter-spacing:.18em;
1536
+ text-transform:uppercase;
1537
+ padding:4px 8px; background:var(--foil); color:var(--ink);
1538
+ z-index:90;
1539
+ border:1px solid var(--ink);
1540
+ }
1541
+
1542
+ /* =====================================================================
1543
+ LEDGER as SECONDARY surface — quieter by default, expand on toggle
1544
+ ===================================================================== */
1545
+ .ledger-pane { transition:opacity 200ms ease, max-height 280ms ease; }
1546
+ body:not(.ledger-expanded) .ledger { opacity:.88; }
1547
+ body.ledger-expanded .ledger-pane { opacity:1; }
1548
+ body.ledger-expanded .ledger { opacity:1; }
1549
+ .ledger .head { gap:8px; }
1550
+ .ledger-toggle {
1551
+ font-family:var(--ui); font-size:9.5px; letter-spacing:.18em;
1552
+ text-transform:uppercase; font-weight:700;
1553
+ padding:3px 9px; border:1px solid var(--ink); background:var(--parchment);
1554
+ color:var(--ink); cursor:pointer; margin-left:auto;
1555
+ }
1556
+ .ledger-toggle:hover { background:var(--ink); color:var(--parchment); }
1557
+ body.ledger-expanded .ledger-toggle::after { content:" ▲"; }
1558
+ body:not(.ledger-expanded) .ledger-toggle::after { content:" ▼"; }
1559
+
1560
+ /* Hide the version pill from the player by default (designer-only) */
1561
+ body:not(.designer-mode) .version-pill { display:none; }
1562
+ body:not(.designer-mode) .phase-pill { display:none; }
1563
+ body:not(.designer-mode) #seedPill { display:none; }
1564
+
1565
+ /* Hide the "Replay" button until the game ends — already set hidden in HTML;
1566
+ keep it hidden in default styling outside endgame */
1567
+ #btnReplay { display:none; }
1568
+ .endgame-active #btnReplay { display:inline-block; }
1569
+
1570
+ /* Endgame "VIEW FINAL REPORT" CTA on game-over rail */
1571
+ .action-banner.endgame-rail { background:linear-gradient(180deg, var(--parchment-2) 0%, #DDC890 100%); }
1572
+ .action-banner.endgame-rail::before { background:linear-gradient(90deg, var(--foil) 0%, #E5C36A 50%, var(--foil) 100%); }
1573
+ .action-banner.endgame-rail .ab-eyebrow { color:var(--foil); }
1574
+
1575
+ /* =====================================================================
1576
+ DICE ROLL ANIMATION — overlay with two flashing dice
1577
+ ===================================================================== */
1578
+ .dice-overlay {
1579
+ position:fixed; inset:0; z-index:75;
1580
+ background:rgba(26,22,18,0.55);
1581
+ display:flex; align-items:center; justify-content:center;
1582
+ pointer-events:none;
1583
+ }
1584
+ .dice-overlay.hidden { display:none; }
1585
+ .dice-roll {
1586
+ display:flex; gap:18px;
1587
+ padding:24px 28px;
1588
+ background:var(--parchment);
1589
+ border:2px solid var(--ink);
1590
+ box-shadow:inset 0 0 0 1px var(--foil-soft), 0 8px 28px -8px rgba(0,0,0,.6);
1591
+ }
1592
+ .dice-roll .die {
1593
+ width:72px; height:72px;
1594
+ background:var(--parchment-2);
1595
+ border:1.5px solid var(--ink);
1596
+ border-radius:10px;
1597
+ display:flex; align-items:center; justify-content:center;
1598
+ font-family:var(--display); font-size:48px; font-weight:700;
1599
+ color:var(--ink);
1600
+ box-shadow:0 3px 0 rgba(26,22,18,.18), inset 0 0 0 1px rgba(140,107,42,.4);
1601
+ animation:diceTumble 320ms steps(1,end) infinite;
1602
+ }
1603
+ .dice-roll .die:nth-child(2) { animation-delay:160ms; }
1604
+ @keyframes diceTumble {
1605
+ 0% { transform:rotate(0deg) scale(1); }
1606
+ 25% { transform:rotate(-7deg) scale(1.04); }
1607
+ 50% { transform:rotate(0deg) scale(.96); }
1608
+ 75% { transform:rotate(7deg) scale(1.04); }
1609
+ 100% { transform:rotate(0deg) scale(1); }
1610
+ }
1611
+ .dice-overlay.settled .die { animation:none; transform:none; box-shadow:0 3px 0 rgba(26,22,18,.25), inset 0 0 0 1px var(--foil); }
1612
+ .dice-roll .sum {
1613
+ align-self:center;
1614
+ font-family:var(--ui); font-size:11px; letter-spacing:.26em;
1615
+ text-transform:uppercase; font-weight:700;
1616
+ color:var(--foil); margin-left:4px;
1617
+ min-width:60px;
1618
+ }
1619
+
1620
+ /* =====================================================================
1621
+ EVENT TOAST — pops up for Act pass/fail, Crisis, Default, Rebellion, etc.
1622
+ ===================================================================== */
1623
+ .event-toast-stack {
1624
+ position:fixed; top:96px; left:50%; transform:translateX(-50%);
1625
+ z-index:65; display:flex; flex-direction:column; gap:10px;
1626
+ pointer-events:none;
1627
+ max-width:540px; width:90%;
1628
+ }
1629
+ .event-toast {
1630
+ background:var(--parchment);
1631
+ border:2px solid var(--ink);
1632
+ box-shadow:inset 0 0 0 1px var(--foil-soft), 0 6px 22px -8px rgba(0,0,0,.45);
1633
+ padding:14px 20px 16px;
1634
+ position:relative;
1635
+ animation:toastIn 220ms ease-out, toastOut 320ms ease-in forwards;
1636
+ animation-delay:0s, 3.4s;
1637
+ pointer-events:auto;
1638
+ }
1639
+ .event-toast::before {
1640
+ content:""; position:absolute; left:0; right:0; top:0; height:4px;
1641
+ background:linear-gradient(90deg, var(--foil) 0%, #E5C36A 50%, var(--foil) 100%);
1642
+ }
1643
+ @keyframes toastIn {
1644
+ from { opacity:0; transform:translateY(-12px) scale(.98); }
1645
+ to { opacity:1; transform:translateY(0) scale(1); }
1646
+ }
1647
+ @keyframes toastOut {
1648
+ to { opacity:0; transform:translateY(-12px) scale(.98); }
1649
+ }
1650
+ .event-toast .toast-eyebrow {
1651
+ font-family:var(--ui); font-size:10px; letter-spacing:.26em; text-transform:uppercase;
1652
+ font-weight:800; color:var(--foil);
1653
+ }
1654
+ .event-toast .toast-title {
1655
+ font-family:var(--display); font-size:22px; line-height:1.15;
1656
+ letter-spacing:-.005em; margin-top:2px;
1657
+ }
1658
+ .event-toast .toast-body {
1659
+ font-family:var(--body); font-size:13px; line-height:1.5; margin-top:4px; opacity:.85;
1660
+ }
1661
+ .event-toast.pass { border-color:var(--severity-pass); }
1662
+ .event-toast.pass::before { background:linear-gradient(90deg, var(--severity-pass) 0%, #58B19F 50%, var(--severity-pass) 100%); }
1663
+ .event-toast.pass .toast-eyebrow { color:var(--severity-pass); }
1664
+ .event-toast.fail { border-color:var(--highlight); }
1665
+ .event-toast.fail::before { background:linear-gradient(90deg, var(--highlight) 0%, #E66B5F 50%, var(--highlight) 100%); }
1666
+ .event-toast.fail .toast-eyebrow { color:var(--highlight); }
1667
+ .event-toast.crisis { background:rgba(200,57,46,.08); border-color:var(--highlight); }
1668
+ .event-toast.crisis::before { background:linear-gradient(90deg, var(--highlight) 0%, #E66B5F 50%, var(--highlight) 100%); }
1669
+ .event-toast.crisis .toast-eyebrow { color:var(--highlight); }
1670
+ .event-toast.default { background:var(--ink); color:var(--parchment); border-color:var(--highlight); }
1671
+ .event-toast.default::before { background:var(--highlight); }
1672
+ .event-toast.default .toast-eyebrow { color:var(--highlight); }
1673
+ .event-toast.rebellion { background:var(--revolutionary-debt); color:var(--parchment); border-color:var(--ink); }
1674
+ .event-toast.rebellion::before { background:var(--ink); }
1675
+ .event-toast.rebellion .toast-eyebrow { color:#F0E6CD; }
1676
+
1677
+ /* =====================================================================
1678
+ LAP RECAP CARD — story beat between laps
1679
+ ===================================================================== */
1680
+ .lap-recap-overlay {
1681
+ position:fixed; inset:0; z-index:72;
1682
+ background:rgba(26,22,18,0.84);
1683
+ display:flex; align-items:center; justify-content:center;
1684
+ padding:24px;
1685
+ animation:lapRecapIn 320ms ease-out;
1686
+ }
1687
+ .lap-recap-overlay.hidden { display:none; }
1688
+ @keyframes lapRecapIn { from { opacity:0; } to { opacity:1; } }
1689
+ .lap-recap {
1690
+ max-width:620px; width:100%;
1691
+ background:linear-gradient(180deg, var(--parchment) 0%, var(--parchment-2) 100%);
1692
+ border:2px solid var(--ink);
1693
+ padding:28px 36px 26px;
1694
+ position:relative;
1695
+ box-shadow:inset 0 0 0 1px var(--foil-soft), 0 14px 40px -10px rgba(0,0,0,.55);
1696
+ }
1697
+ .lap-recap::before {
1698
+ content:""; position:absolute; left:0; right:0; top:0; height:5px;
1699
+ background:linear-gradient(90deg, var(--foil) 0%, #E5C36A 50%, var(--foil) 100%);
1700
+ }
1701
+ .lap-recap .eyebrow {
1702
+ font-family:var(--ui); font-size:11px; letter-spacing:.32em; text-transform:uppercase;
1703
+ font-weight:800; color:var(--foil); text-align:center;
1704
+ }
1705
+ .lap-recap h2 {
1706
+ font-family:var(--display); font-size:36px; line-height:1; margin:6px 0 4px;
1707
+ text-align:center; letter-spacing:-.015em; font-weight:400;
1708
+ }
1709
+ .lap-recap .lap-sub {
1710
+ font-family:var(--display); font-style:italic; font-size:15px;
1711
+ text-align:center; opacity:.78; margin-bottom:18px;
1712
+ }
1713
+ .lap-recap .recap-body {
1714
+ font-family:var(--body); font-size:15px; line-height:1.65;
1715
+ text-wrap:pretty; max-width:60ch; margin:0 auto;
1716
+ }
1717
+ .lap-recap .recap-body::first-letter {
1718
+ font-family:var(--display); font-size:30px; line-height:1;
1719
+ padding-right:3px; color:var(--foil); font-weight:400;
1720
+ }
1721
+ .lap-recap .recap-meta {
1722
+ margin-top:18px; padding-top:12px; border-top:0.5px dashed var(--rule-soft);
1723
+ display:flex; justify-content:center; gap:18px;
1724
+ font-family:var(--mono); font-size:11px; letter-spacing:.12em;
1725
+ text-transform:uppercase; opacity:.78;
1726
+ }
1727
+ .lap-recap .recap-meta .m { display:flex; flex-direction:column; align-items:center; gap:2px; }
1728
+ .lap-recap .recap-meta .m .v { font-family:var(--display); font-size:18px; font-weight:400; opacity:1; }
1729
+ .lap-recap .recap-actions { margin-top:20px; display:flex; justify-content:center; }
1730
+ .lap-recap .recap-actions button {
1731
+ font-family:var(--display); font-size:17px; font-weight:700;
1732
+ padding:12px 28px; background:var(--ink); color:var(--parchment);
1733
+ border:1.5px solid var(--ink); cursor:pointer; letter-spacing:.04em;
1734
+ box-shadow:0 3px 0 rgba(26,22,18,.35);
1735
+ transition:transform 80ms ease, background-color 80ms ease;
1736
+ }
1737
+ .lap-recap .recap-actions button:hover { background:var(--national-finance); transform:translateY(-1px); }
1738
+ .lap-recap .recap-actions button:focus-visible { outline:2.5px solid var(--foil); outline-offset:3px; }
1739
+
1740
+ /* =====================================================================
1741
+ COLLAPSIBLE RIGHT SIDEBAR — defaults to collapsed for board focus
1742
+ ===================================================================== */
1743
+ .grid { transition:grid-template-columns 220ms ease; }
1744
+ body.sidebar-collapsed .grid { grid-template-columns:minmax(0, 1fr); }
1745
+ body.sidebar-collapsed .panel-pane { display:none; }
1746
+ body.sidebar-collapsed .ledger-pane { grid-column:1; grid-row:2; }
1747
+ body.sidebar-collapsed .board-view { max-width:1100px; }
1748
+
1749
+ /* Sidebar toggle — a prominent gilt tab so it's easy to find */
1750
+ .sidebar-toggle {
1751
+ position:fixed; top:96px; right:14px;
1752
+ z-index:55;
1753
+ font-family:var(--ui); font-size:11.5px; letter-spacing:.18em;
1754
+ text-transform:uppercase; font-weight:800;
1755
+ padding:10px 16px;
1756
+ background:var(--foil); color:var(--ink);
1757
+ border:2px solid var(--ink);
1758
+ cursor:pointer;
1759
+ box-shadow:0 3px 0 rgba(26,22,18,.25), inset 0 0 0 1px rgba(255,255,255,.35);
1760
+ transition:background-color 100ms ease, transform 80ms ease, box-shadow 80ms ease;
1761
+ display:flex; align-items:center; gap:8px;
1762
+ }
1763
+ .sidebar-toggle:hover { background:#E5C36A; transform:translateY(-1px); box-shadow:0 4px 0 rgba(26,22,18,.3); }
1764
+ .sidebar-toggle:active { transform:translateY(1px); box-shadow:0 1px 0 rgba(26,22,18,.18); }
1765
+ .sidebar-toggle:focus-visible { outline:2.5px solid var(--ink); outline-offset:3px; }
1766
+ .sidebar-toggle::before { content:"◀ "; font-family:var(--display); font-size:13px; }
1767
+ body:not(.sidebar-collapsed) .sidebar-toggle::before { content:"▶ "; }
1768
+ body:not(.sidebar-collapsed) .sidebar-toggle { background:var(--parchment); }
1769
+ body:not(.sidebar-collapsed) .sidebar-toggle:hover { background:var(--parchment-2); }
1770
+
1771
+ /* Gentle attract pulse when the sidebar is closed so newcomers see the toggle */
1772
+ @keyframes sidebarTogglePulse {
1773
+ 0%, 100% { box-shadow:0 3px 0 rgba(26,22,18,.25), inset 0 0 0 1px rgba(255,255,255,.35), 0 0 0 0 rgba(140,107,42,0); }
1774
+ 60% { box-shadow:0 3px 0 rgba(26,22,18,.25), inset 0 0 0 1px rgba(255,255,255,.35), 0 0 0 8px rgba(140,107,42,0); }
1775
+ 100% { box-shadow:0 3px 0 rgba(26,22,18,.25), inset 0 0 0 1px rgba(255,255,255,.35), 0 0 0 0 rgba(140,107,42,0); }
1776
+ }
1777
+ body.sidebar-collapsed .sidebar-toggle { animation:sidebarTogglePulse 2400ms ease-in-out infinite; }
1778
+ body.sidebar-collapsed .sidebar-toggle:hover { animation:none; }
1779
+
1780
+ /* Positions strip — add cash per player so collapsing the sidebar doesn't hide critical info */
1781
+ .positions-strip .pos-cash {
1782
+ font-family:var(--mono); font-size:11px; font-weight:700;
1783
+ color:var(--ink); margin-top:1px;
1784
+ }
1785
+ .positions-strip .pos-cash::before { content:"₸ "; color:var(--foil); opacity:.7; }
1786
+
1787
+ @media (max-width: 768px) {
1788
+ .sidebar-toggle { top:auto; bottom:14px; right:10px; writing-mode:horizontal-tb; padding:8px 14px; }
1789
+ body.sidebar-collapsed .sidebar-toggle { animation:none; top:auto; bottom:14px; right:14px; padding:10px 16px; }
1790
+ .action-banner { padding:14px 16px; min-height:0; }
1791
+ .action-banner .ab-prompt { font-size:18px; }
1792
+ .action-banner .ab-actions button.primary { font-size:15px; padding:11px 18px; }
1793
+ .dice-roll .die { width:54px; height:54px; font-size:36px; }
1794
+ }
408
1795
  </style>
1796
+ <template id="__bundler_thumbnail" data-bg-color="#2A2622">
1797
+ <svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
1798
+ <rect x="0" y="0" width="1200" height="800" fill="#2A2622"/>
1799
+ <g transform="translate(600 400)">
1800
+ <circle r="240" fill="none" stroke="#F0E6CD" stroke-width="6"/>
1801
+ <circle r="220" fill="none" stroke="#F0E6CD" stroke-width="1.5"/>
1802
+ <circle r="170" fill="#F0E6CD"/>
1803
+ <text x="0" y="42" text-anchor="middle" font-family="Baskerville, 'Big Caslon', 'Hoefler Text', Garamond, 'Times New Roman', serif" font-size="220" font-weight="400" fill="#1A1612" font-style="italic">S</text>
1804
+ <g font-family="Baskerville, serif" font-size="22" fill="#F0E6CD" letter-spacing="8">
1805
+ <text x="0" y="-200" text-anchor="middle">SOVEREIGN</text>
1806
+ <text x="0" y="218" text-anchor="middle" font-size="14" letter-spacing="6">v0.18 · FAILURE-PRESSURE</text>
1807
+ </g>
1808
+ </g>
1809
+ </svg>
1810
+ </template>
409
1811
  </head>
410
1812
  <body>
411
1813
 
@@ -418,32 +1820,29 @@ input[type="number"] { font-family:var(--mono); font-size:13px; padding:5px 8px;
418
1820
  <input type="file" id="loadFile" accept="application/json,.json" class="hidden" />
419
1821
  <header class="topbar">
420
1822
  <div class="brand">
421
- <div class="eyebrow">Sovereign · Solo / Digital · Phase 6.1 · v0.10 balance candidate</div>
422
- <div class="title">Telemetry · Batch</div>
423
- <div class="sub">v0.10 balance candidate cash scoring softened to +1 IP per 400 TN.</div>
1823
+ <div class="eyebrow">Solo / Digital Mode · The Hamilton System</div>
1824
+ <div class="title">Sovereign<span class="title-mode">Federal economy in twelve rounds</span></div>
1825
+ <div class="sub">Steer the early American republic through debt, credit, public resistance, and industrial capacity. The ledger on the right records every payment, vote, track shift, and event.</div>
424
1826
  </div>
425
1827
  <div class="controls">
426
1828
  <span class="seed-pill" id="seedPill">seed: ----</span>
427
1829
  <span class="phase-pill" id="phasePill">phase: setup</span>
428
1830
  <span class="active-pill" id="activePill">Active: You</span>
429
- <button id="btnSave" title="Download decision log + localStorage autosave">Save</button>
430
- <button id="btnLoad" title="Import a saved JSON or load the autosave">Load</button>
431
- <button id="btnReplay" class="hidden" title="Scrub through this finished game">Replay</button>
432
- <button id="btnBatch" title="Run a scripted batch of 10 / 50 / 100 games">Batch</button>
433
- <button id="btnNewSeed">New seed</button>
434
- <button id="btnReset">Reset (same seed)</button>
1831
+ <button id="btnHelp" title="Orientation panel for new players">How to play</button>
1832
+ <button id="btnSave" title="Save your game (download a file + write to browser storage)">Save game</button>
1833
+ <button id="btnLoad" title="Load a saved game from file or browser storage">Load game</button>
1834
+ <button id="btnReplay" class="hidden" title="Step through this finished game turn by turn">Replay this game</button>
1835
+ <button id="btnBatch" title="Run a scripted batch of 10 / 50 / 100 games">Balance Sweep</button>
1836
+ <button id="btnNewSeed">New game</button>
1837
+ <button id="btnReset">Restart this game</button>
1838
+ <span class="version-pill" title="Build provenance · no mechanic change since v0.18 candidate">v0.18 · Phase 6.1</span>
435
1839
  </div>
436
1840
  </header>
437
1841
 
438
1842
  <main class="grid" id="mainGrid">
439
1843
  <section class="board-pane">
440
- <div class="panel" style="padding:6px 12px; border-bottom-width:0">
441
- <div class="panel-head" style="margin-bottom:0; padding-bottom:0; border-bottom:0">
442
- <span class="name">Board View</span><span class="surface-id">A</span>
443
- </div>
444
- </div>
445
- <div class="board-view"><div class="board-grid" id="boardGrid"></div></div>
446
1844
  <div class="controls-bar" id="controlsBar"></div>
1845
+ <div class="board-view"><div class="board-grid" id="boardGrid"></div></div>
447
1846
  </section>
448
1847
 
449
1848
  <aside class="panel-pane">
@@ -513,10 +1912,127 @@ input[type="number"] { font-family:var(--mono); font-size:13px; padding:5px 8px;
513
1912
  <section class="endgame hidden" id="endgameView"></section>
514
1913
  </div>
515
1914
 
1915
+ <div class="lap-recap-overlay hidden" id="lapRecapOverlay" role="dialog" aria-modal="true">
1916
+ <div class="lap-recap">
1917
+ <div class="eyebrow">§ Round closes</div>
1918
+ <h2 id="lapRecapTitle">Round 1 of 12</h2>
1919
+ <div class="lap-sub" id="lapRecapSub">After round 1</div>
1920
+ <div class="recap-body" id="lapRecapBody"></div>
1921
+ <div class="recap-meta" id="lapRecapMeta"></div>
1922
+ <div class="recap-actions"><button type="button" id="lapRecapDismiss">Continue</button></div>
1923
+ </div>
1924
+ </div>
1925
+
1926
+ <div class="event-toast-stack" id="toastStack"></div>
1927
+
1928
+ <button class="sidebar-toggle" id="sidebarToggle" type="button" aria-label="Toggle right panel"><span id="sidebarToggleLabel">Show panels</span></button>
1929
+
1930
+ <div class="dice-overlay hidden" id="diceOverlay" aria-hidden="true">
1931
+ <div class="dice-roll">
1932
+ <div class="die" id="die1">⚀</div>
1933
+ <div class="die" id="die2">⚀</div>
1934
+ <div class="sum" id="diceSum">rolling…</div>
1935
+ </div>
1936
+ </div>
1937
+
1938
+ <div class="orient-overlay hidden" id="orientOverlay" role="dialog" aria-modal="true" aria-labelledby="orientTitle">
1939
+ <div class="orient-panel welcome-step" id="welcomePanel" data-step="1">
1940
+ <div class="welcome-progress" id="welcomeProgress">
1941
+ <span class="dot sel" data-step="1"></span>
1942
+ <span class="dot" data-step="2"></span>
1943
+ <span class="dot" data-step="3"></span>
1944
+ </div>
1945
+ <div class="eyebrow">Solo / Digital Mode · The Hamilton System</div>
1946
+ <h2 id="orientTitle">Sovereign</h2>
1947
+
1948
+ <!-- Step 1: Welcome -->
1949
+ <p class="welcome-lede">You are trying to build a durable republic. Public Credit is trust, Public Resistance is political strain, Industrial Capacity is productive power. Each turn you roll, land, and choose. The game will guide you. The ledger will record everything.</p>
1950
+
1951
+ <!-- Step 2: Delegate picker -->
1952
+ <div class="pick-section">
1953
+ <h3>§ Choose your role</h3>
1954
+ <div class="role-grid" id="roleGrid" role="radiogroup" aria-label="Pick your role">
1955
+ <button type="button" class="role-card sel" data-role="treasury">
1956
+ <div class="role-eyebrow">Treasury</div>
1957
+ <div class="role-title">Treasury / Finance</div>
1958
+ <div class="role-blurb">The Hamiltonian apex. Public Credit, federal debt, the Bank of the United States.</div>
1959
+ </button>
1960
+ <button type="button" class="role-card" data-role="merchant">
1961
+ <div class="role-eyebrow">Merchant</div>
1962
+ <div class="role-title">Merchant / Infrastructure</div>
1963
+ <div class="role-blurb">The commerce line. Routes, ports, exchanges. Works whenever Credit holds.</div>
1964
+ </button>
1965
+ <button type="button" class="role-card" data-role="manufacturer">
1966
+ <div class="role-eyebrow">Manufacturer</div>
1967
+ <div class="role-title">Manufacturer / Industry</div>
1968
+ <div class="role-blurb">The industrial line. Manufactures, Strategic Industry, Capacity scoring.</div>
1969
+ </button>
1970
+ </div>
1971
+ <h3 style="margin-top:18px">§ Choose your delegate</h3>
1972
+ <div class="pick-row">
1973
+ <div class="pick-field">
1974
+ <label for="playerNameInput">Your name</label>
1975
+ <input type="text" id="playerNameInput" maxlength="28" placeholder="You" autocomplete="off" />
1976
+ </div>
1977
+ <div class="pick-field">
1978
+ <label>Your sigil</label>
1979
+ <div class="glyph-picker" id="glyphPicker" role="radiogroup" aria-label="Pick your sigil">
1980
+ <button type="button" data-glyph="§" title="Treasury">§</button>
1981
+ <button type="button" data-glyph="★" title="Star">★</button>
1982
+ <button type="button" data-glyph="⚖" title="Justice">⚖</button>
1983
+ <button type="button" data-glyph="⚓" title="Anchor">⚓</button>
1984
+ <button type="button" data-glyph="⚙" title="Industry">⚙</button>
1985
+ <button type="button" data-glyph="⚔" title="Arms">⚔</button>
1986
+ <button type="button" data-glyph="⚜" title="Fleur-de-lis">⚜</button>
1987
+ <button type="button" data-glyph="♛" title="Crown">♛</button>
1988
+ </div>
1989
+ </div>
1990
+ </div>
1991
+ <div class="preview-row">
1992
+ <div class="preview-token-set">
1993
+ <div class="preview-token p0"><div class="ptok" id="previewYouTok"><span>§</span></div><div class="ptok-label" id="previewYouName">You</div></div>
1994
+ <div class="preview-token p1"><div class="ptok" id="previewOpp1Tok"><span>H</span></div><div class="ptok-label" id="previewOpp1Name">Hamilton</div></div>
1995
+ <div class="preview-token p2"><div class="ptok" id="previewOpp2Tok"><span>M</span></div><div class="ptok-label" id="previewOpp2Name">Morris</div></div>
1996
+ </div>
1997
+ </div>
1998
+ </div>
1999
+
2000
+ <!-- Step 3: On-board tour -->
2001
+ <div class="tour-section">
2002
+ <p style="font-family:var(--display); font-style:italic; font-size:15px; line-height:1.55; margin:0 0 16px; opacity:.88">A few things to know before you begin.</p>
2003
+ <div class="tour-row">
2004
+ <div class="tour-num">1</div>
2005
+ <div><strong>Your turn loop</strong> is <em>Roll → Land → Choose → Consequence</em>. The rail above the board will tell you what to do next.</div>
2006
+ </div>
2007
+ <div class="tour-row">
2008
+ <div class="tour-num">2</div>
2009
+ <div><strong>Each round, every player takes one turn</strong> — twelve rounds total. Passing Treasury Opens is a board circuit; a round is not a full lap around the board.</div>
2010
+ </div>
2011
+ <div class="tour-row">
2012
+ <div class="tour-num">3</div>
2013
+ <div><strong>Three shared tracks</strong> — Public Credit, Public Resistance, Industrial Capacity — change as the Republic plays out.</div>
2014
+ </div>
2015
+ <div class="tour-row">
2016
+ <div class="tour-num">4</div>
2017
+ <div><strong>Your portfolio</strong> appears in a strip beside the action rail. Use the "Panels" button on the right edge to see opponent details and the ledger.</div>
2018
+ </div>
2019
+ <div class="tour-row">
2020
+ <div class="tour-num">5</div>
2021
+ <div><strong>The mandate</strong> — from round 8 onward, a player with 15 Influence and a 5-point lead can claim the mandate and trigger Final Accounting. If no one does, the game ends after round 12.</div>
2022
+ </div>
2023
+ </div>
2024
+
2025
+ <div class="footer-row">
2026
+ <span class="meta" id="welcomeMeta">Step 1 of 3 · "How to play" recalls this any time.</span>
2027
+ <button class="primary" id="welcomeAdvance">Continue</button>
2028
+ </div>
2029
+ </div>
2030
+ </div>
2031
+
516
2032
  <div class="batch-overlay hidden" id="batchOverlay">
517
2033
  <div class="batch-modal">
518
2034
  <div class="bm-head">
519
- <div><div class="ttl">Batch Simulation</div><div class="sub">Scripted profiles only. Local. Deterministic per seed.</div></div>
2035
+ <div><div class="ttl">Balance Evidence Run</div><div class="sub">Many seeds, scripted profiles only — to test balance, not for daily play. Local. Deterministic per seed.</div></div>
520
2036
  <button id="batchClose">Close</button>
521
2037
  </div>
522
2038
  <div class="bm-controls">
@@ -737,9 +2253,9 @@ const MARKET_SHOCK_CARDS = [
737
2253
  { id:2, name:'Foreign Demand Rises', tags:['System'],
738
2254
  effectText:'All <strong>Manufactures</strong> owners collect 40 TN.',
739
2255
  resolve(s){ for(let i=0;i<s.players.length;i++){ if(countOwnedSys(s,i,'manufactures')>0) s = adjustCash(s,40,'Foreign Demand Rises: owns Manufactures',i); } return s; } },
740
- { id:3, name:'Speculation Fever', tags:['Track'], chips:{resist:1},
741
- effectText:'Choose an unowned Rev/State Debt property; auction it. Resistance +1.',
742
- resolve(s, idx){ s = adjustTrack(s,'resistance',1,'Speculation Fever'); /* find an unowned Rev/State Debt property; if any, open auction */ const candidates = [1,3,6,8,9].filter(n => findOwnerIndex(s,n) < 0); if(candidates.length === 0) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Speculation Fever: no unowned Rev/State Debt properties',cls:'card'}); /* pick deterministically: lowest spaceNum */ const target = candidates[0]; s.pendingAuction = startAuction(s, target, idx, 'Speculation Fever card'); s.phase = 'auction'; return logRow(s,{actor:'Card',event:'AUCTION',detail:'Speculation Fever: auctioning ' + SPACES[target].name,cls:'card'}); } },
2256
+ { id:3, name:'Speculation Fever', tags:['Track'], chips:{credit:-1, resist:1},
2257
+ effectText:'Public Credit -1 if Credit ≥ 7, or Public Credit -2 if Credit ≤ 6. Public Resistance +1. Choose an unowned Rev/State Debt property; auction it.',
2258
+ resolve(s, idx){ const credDelta = s.tracks.credit.value >= 7 ? -1 : -2; s = adjustTrack(s,'credit',credDelta,'Speculation Fever'); s = adjustTrack(s,'resistance',1,'Speculation Fever'); /* find an unowned Rev/State Debt property; if any, open auction */ const candidates = [1,3,6,8,9].filter(n => findOwnerIndex(s,n) < 0); if(candidates.length === 0) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Speculation Fever: no unowned Rev/State Debt properties',cls:'card'}); /* pick deterministically: lowest spaceNum */ const target = candidates[0]; s.pendingAuction = startAuction(s, target, idx, 'Speculation Fever card'); s.phase = 'auction'; return logRow(s,{actor:'Card',event:'AUCTION',detail:'Speculation Fever: auctioning ' + SPACES[target].name,cls:'card'}); } },
743
2259
  { id:4, name:'Shipping Disruption', tags:['Suspension'],
744
2260
  effectText:'Commerce / Route payments suspended until your next turn.',
745
2261
  resolve(s, idx){ s.flags.shippingDisruptedUntilTurn = s.turnIndex + 3; return logRow(s,{actor:'Card',event:'EFFECT',detail:'Shipping Disruption: Commerce/Route payments suspended',cls:'card'}); } },
@@ -758,9 +2274,9 @@ const MARKET_SHOCK_CARDS = [
758
2274
  { id:9, name:'Successful Bond Auction', tags:['Multi-system'],
759
2275
  effectText:'Rev Debt and Nat\'l Finance owners collect 30 TN per property.',
760
2276
  resolve(s){ for(let i=0;i<s.players.length;i++){ const n = countOwnedSys(s,i,'revolutionary-debt') + countOwnedSys(s,i,'national-finance'); if(n>0) s = adjustCash(s, 30*n, 'Bond Auction: ' + n + ' × 30 TN', i); } return s; } },
761
- { id:10, name:'Bank Run', tags:['Conditional','Track'], chips:{indust:-1},
762
- effectText:'If Charter passed, Nat\'l Finance owners lose 1 upgrade. Capacity -1.',
763
- resolve(s){ s = adjustTrack(s,'capacity',-1,'Bank Run'); if(!s.flags.bankCharterPassed) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Bank Run: Charter not passed, skipped',cls:'card'}); for(let i=0;i<s.players.length;i++){ const nfs = s.players[i].ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === 'national-finance' && a.tier > 0); if(nfs.length > 0){ const target = nfs.reduce((m,a)=> a.tier > m.tier ? a : m); target.tier -= 1; s = logRow(s,{actor:'Card',event:'EFFECT',detail:'Bank Run: '+s.players[i].name+' loses 1 upgrade on '+SPACES[target.spaceNum].name,cls:'card'}); } } return s; } },
2277
+ { id:10, name:'Bank Run', tags:['Conditional','Track'], chips:{credit:-1, indust:-1},
2278
+ effectText:'If Charter passed, Nat\'l Finance owners lose 1 upgrade. Public Credit -1. Industrial Capacity -1.',
2279
+ resolve(s){ s = adjustTrack(s,'credit',-1,'Bank Run'); s = adjustTrack(s,'capacity',-1,'Bank Run'); if(!s.flags.bankCharterPassed) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Bank Run: Charter not passed, skipped',cls:'card'}); for(let i=0;i<s.players.length;i++){ const nfs = s.players[i].ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === 'national-finance' && a.tier > 0); if(nfs.length > 0){ const target = nfs.reduce((m,a)=> a.tier > m.tier ? a : m); target.tier -= 1; s = logRow(s,{actor:'Card',event:'EFFECT',detail:'Bank Run: '+s.players[i].name+' loses 1 upgrade on '+SPACES[target.spaceNum].name,cls:'card'}); } } return s; } },
764
2280
  { id:11, name:'Cotton Gin Patented', tags:['System','Track'], chips:{indust:1},
765
2281
  effectText:'Textile Works payment doubled. Capacity +1.',
766
2282
  resolve(s){ s.flags.textileDoubleLap = s.lap; return adjustTrack(s,'capacity',1,'Cotton Gin Patented'); } },
@@ -794,9 +2310,9 @@ const REPUBLIC_DEBATE_CARDS = [
794
2310
  { id:7, name:'Federalist Victory', tags:['Conditional','Track'], chips:{credit:1},
795
2311
  effectText:'If you own Nat\'l Finance, collect 100 TN. Credit +1.',
796
2312
  resolve(s, idx){ s = adjustTrack(s,'credit',1,'Federalist Victory'); return countOwnedSys(s,idx,'national-finance') > 0 ? adjustCash(s,100,'Federalist Victory',idx) : logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Federalist Victory: no NF held',cls:'card'}); } },
797
- { id:8, name:'Anti-Federalist Pamphlet', tags:['System','Track'], chips:{resist:1},
798
- effectText:'All Revenue owners pay 30 TN per property. Resistance +1.',
799
- resolve(s){ s = adjustTrack(s,'resistance',1,'Anti-Federalist Pamphlet'); for(let i=0;i<s.players.length;i++){ const n = countOwnedSys(s,i,'revenue-system'); if(n>0) s = adjustCash(s, -30*n, 'Anti-Fed Pamphlet: '+n+' × 30 TN', i); } return s; } },
2313
+ { id:8, name:'Anti-Federalist Pamphlet', tags:['System','Track'], chips:{credit:-1, resist:1},
2314
+ effectText:'Public Credit -1. All Revenue owners pay 30 TN per property. Resistance +1.',
2315
+ resolve(s){ s = adjustTrack(s,'credit',-1,'Anti-Federalist Pamphlet'); s = adjustTrack(s,'resistance',1,'Anti-Federalist Pamphlet'); for(let i=0;i<s.players.length;i++){ const n = countOwnedSys(s,i,'revenue-system'); if(n>0) s = adjustCash(s, -30*n, 'Anti-Fed Pamphlet: '+n+' × 30 TN', i); } return s; } },
800
2316
  { id:9, name:'Funding Plan Advances', tags:['Vote'],
801
2317
  effectText:'Force next Act vote immediately. (Phase 4: noted only.)',
802
2318
  resolve(s){ return logRow(s,{actor:'Card',event:'NOTE',detail:'Funding Plan Advances: force-vote mechanic deferred',cls:'card'}); } },
@@ -941,7 +2457,7 @@ const NARRATION = {
941
2457
  x: '',
942
2458
  },
943
2459
  'endgame · republic-summary': {
944
- h: 'Your Republic, after lap 7',
2460
+ h: 'Your Republic, after twelve rounds',
945
2461
  d: '',
946
2462
  x: '',
947
2463
  },
@@ -1238,6 +2754,7 @@ function adjustTrack(s, key, delta, reason) {
1238
2754
  s = logRow(s, { actor:'Track', event:key.toUpperCase(), detail: reason + ' · ' + before + ' → ' + next + ' (' + (delta>=0?'+':'') + delta + ')', cls:'track' });
1239
2755
  if (key === 'credit' && before > 0 && next === 0) s.pendingDefault = true;
1240
2756
  if (key === 'resistance' && next === 12) s.pendingRebellion = true;
2757
+ if (key === 'credit' && next <= 4 && next > 0 && !s.flags.creditCrisisFired) s.pendingCreditCrisis = true;
1241
2758
  return s;
1242
2759
  }
1243
2760
  function logRow(s, row) {
@@ -1317,7 +2834,7 @@ function initialState(seed) {
1317
2834
  },
1318
2835
  flags: {},
1319
2836
  pendingCard: null, pendingLanding: null, pendingAuction: null, pendingResolveLanding: false,
1320
- pendingDefault: false, pendingRebellion: false,
2837
+ pendingDefault: false, pendingRebellion: false, pendingCreditCrisis: false,
1321
2838
  ledger: [{ actor:'System', event:'INIT', detail:'3-player game · seed ' + seed + ' · You + Hamilton + Morris', cls:'event', lap:1, turn:0 }],
1322
2839
  };
1323
2840
  }
@@ -1361,7 +2878,28 @@ let TURN_SNAPSHOTS = [];
1361
2878
  let NARRATION_LOG = [];
1362
2879
  let NARRATION_ENABLED = true;
1363
2880
  const AUTOSAVE_KEY = 'sovereign.autosave';
1364
- const SAVE_VERSION = 'v0.10';
2881
+ const SAVE_VERSION = 'v0.20-mandate-candidate';
2882
+ const TOTAL_ROUNDS = 12;
2883
+ const LATE_REPUBLIC_START = 8;
2884
+
2885
+ /* Mandate victory model — designer-overridable via URL in ?designer=1 mode */
2886
+ const MANDATE = (function() {
2887
+ const def = { threshold: 15, lead: 5, minRound: 8, hardCap: 12 };
2888
+ try {
2889
+ const params = new URLSearchParams(window.location.search);
2890
+ if (params.get('designer') === '1') {
2891
+ const t = parseInt(params.get('mandate_threshold'), 10);
2892
+ const l = parseInt(params.get('mandate_lead'), 10);
2893
+ const m = parseInt(params.get('mandate_min_round'), 10);
2894
+ const c = parseInt(params.get('mandate_hard_cap'), 10);
2895
+ if (Number.isFinite(t)) def.threshold = t;
2896
+ if (Number.isFinite(l)) def.lead = l;
2897
+ if (Number.isFinite(m)) def.minRound = m;
2898
+ if (Number.isFinite(c)) def.hardCap = c;
2899
+ }
2900
+ } catch (e) {}
2901
+ return def;
2902
+ })();
1365
2903
  let REPLAY = null;
1366
2904
 
1367
2905
  function dispatch(action) {
@@ -1380,12 +2918,13 @@ function dispatch(action) {
1380
2918
  render();
1381
2919
  if (STATE.pendingDefault) queueMicrotask(() => dispatch({ type:'TRIGGER_DEFAULT' }));
1382
2920
  else if (STATE.pendingRebellion) queueMicrotask(() => dispatch({ type:'TRIGGER_REBELLION' }));
2921
+ else if (STATE.pendingCreditCrisis) queueMicrotask(() => dispatch({ type:'TRIGGER_CREDIT_CRISIS' }));
1383
2922
  else if (STATE.phase === 'act-vote' && STATE.acts.current) {
1384
2923
  const nextOpp = findNextUnvotedOpponent();
1385
- if (nextOpp >= 0) setTimeout(() => castOpponentActVote(nextOpp), 250);
2924
+ if (nextOpp >= 0) setTimeout(() => castOpponentActVote(nextOpp), 1500);
1386
2925
  }
1387
2926
  else if (STATE.players[STATE.activePlayerIndex].profile !== 'human' && (STATE.phase === 'awaiting-roll' || STATE.phase === 'act-vote')) {
1388
- setTimeout(() => runOpponent(), 250);
2927
+ setTimeout(() => runOpponent(), 1500);
1389
2928
  }
1390
2929
  }
1391
2930
 
@@ -1454,7 +2993,7 @@ function reduce(s, action) {
1454
2993
  if (act) {
1455
2994
  s.acts.current = { actId: act.id, voting: true, votes: { 0:null, 1:null, 2:null } };
1456
2995
  s.phase = 'act-vote';
1457
- s = logRow(s, { actor:'Acts', event:'REVEAL', detail:'Lap ' + s.lap + ' begins · ' + act.name + ' revealed for vote', cls:'act' });
2996
+ s = logRow(s, { actor:'Acts', event:'REVEAL', detail:'Round ' + s.lap + ' begins · ' + act.name + ' revealed for vote', cls:'act' });
1458
2997
  } else {
1459
2998
  s.phase = 'awaiting-roll';
1460
2999
  }
@@ -1736,6 +3275,14 @@ function reduce(s, action) {
1736
3275
  return s;
1737
3276
  }
1738
3277
 
3278
+ case 'TRIGGER_CREDIT_CRISIS': {
3279
+ s = logRow(s, { actor:'System', event:'CREDIT_CRISIS', detail:'Public Credit collapses to ' + s.tracks.credit.value + ' · financial panic spreads · Public Resistance rises by 1', cls:'event' });
3280
+ s = adjustTrack(s, 'resistance', 1, 'Credit Crisis');
3281
+ s.flags.creditCrisisFired = true;
3282
+ s.pendingCreditCrisis = false;
3283
+ return s;
3284
+ }
3285
+
1739
3286
  case 'TRIGGER_REBELLION': {
1740
3287
  s = logRow(s, { actor:'System', event:'REBELLION', detail:'Resistance = 12 · Revenue upgrades destroyed · Whiskey owner → Crisis · Resistance → 6', cls:'event' });
1741
3288
  for (let i = 0; i < s.players.length; i++) {
@@ -1756,10 +3303,32 @@ function reduce(s, action) {
1756
3303
  s.activePlayerIndex = (s.activePlayerIndex + 1) % s.players.length;
1757
3304
  s.phase = 'awaiting-roll';
1758
3305
  if (s.activePlayerIndex === 0) {
3306
+ /* End-of-round mandate check (rounds 8–11). Mandate fires if a player
3307
+ has Influence ≥ MANDATE.threshold AND a lead of ≥ MANDATE.lead
3308
+ over second place. Sole highest-Influence holder triggers; ties for
3309
+ highest do not trigger. Evaluated before round/lap advances. */
3310
+ if (s.lap >= MANDATE.minRound && s.lap < MANDATE.hardCap) {
3311
+ const scored = s.players.map((p, idx) => ({ idx, name: p.name, total: scorePlayer(s, idx).total }));
3312
+ const sorted = scored.slice().sort((a, b) => b.total - a.total);
3313
+ const top = sorted[0], second = sorted[1];
3314
+ const eligible = top.total >= MANDATE.threshold && (top.total - second.total) >= MANDATE.lead;
3315
+ const sharedTop = scored.filter(x => x.total === top.total).length > 1;
3316
+ if (eligible && !sharedTop) {
3317
+ s.mandateTrigger = {
3318
+ playerIdx: top.idx, playerName: top.name,
3319
+ round: s.lap, influence: top.total, lead: top.total - second.total,
3320
+ threshold: MANDATE.threshold, leadRequired: MANDATE.lead,
3321
+ };
3322
+ s = logRow(s, { actor:'System', event:'MANDATE',
3323
+ detail: top.name + ' claims the mandate · ' + top.total + ' Influence · lead ' + (top.total - second.total) + ' over ' + second.name,
3324
+ cls:'event' });
3325
+ return reduce(s, { type:'END_GAME' });
3326
+ }
3327
+ }
1759
3328
  /* Full round done · advance lap */
1760
3329
  s.lap += 1;
1761
- if (s.lap > 7) {
1762
- s = logRow(s, { actor:'System', event:'GAME OVER', detail:'7 laps complete · scoring', cls:'event' });
3330
+ if (s.lap > TOTAL_ROUNDS) {
3331
+ s = logRow(s, { actor:'System', event:'GAME OVER', detail: TOTAL_ROUNDS + ' rounds complete · scoring', cls:'event' });
1763
3332
  return reduce(s, { type:'END_GAME' });
1764
3333
  }
1765
3334
  s = logRow(s, { actor:'System', event:'LAP', detail:'Begin lap ' + s.lap, cls:'event' });
@@ -1967,6 +3536,17 @@ function renderBoard() {
1967
3536
  cell.dataset.num = space.num;
1968
3537
  cell.style.gridColumn = (c+1);
1969
3538
  cell.style.gridRow = (r+1);
3539
+ /* Tooltip: name, type, cost summary */
3540
+ const _asset = ASSETS[space.num];
3541
+ const _typeLbl = space.kind === 'route' ? 'Route' :
3542
+ space.kind === 'institution' ? 'Institution' :
3543
+ space.kind === 'card-shock' ? 'Market Shock space' :
3544
+ space.kind === 'card-debate' ? 'Republic Debate space' :
3545
+ space.kind === 'tax' ? 'Tax / Scandal' :
3546
+ space.kind && space.kind.startsWith('corner') ? 'Corner' :
3547
+ _asset && _asset.sys ? (SYS_LABEL[_asset.sys] || 'Property') : 'Space';
3548
+ const _costLbl = _asset && _asset.cost ? ' · Cost ' + _asset.cost + ' TN' : '';
3549
+ cell.title = space.name + ' · ' + _typeLbl + _costLbl;
1970
3550
  cell.innerHTML = `<div class="band"></div><div class="num">${String(space.num).padStart(2,'0')}</div><div class="nm">${space.name}</div><div class="tokens"></div>`;
1971
3551
  grid.appendChild(cell);
1972
3552
  }
@@ -1978,7 +3558,7 @@ function renderBoard() {
1978
3558
  <div class="bc-eyebrow">Sovereign · Hamilton System</div>
1979
3559
  <div class="bc-title">Founding Credit</div>
1980
3560
  <div class="bc-sub">Fund the debt. Build the bank. Industrialize the republic.</div>
1981
- <div class="bc-lap" id="bcLap">Lap 1 / 7</div>
3561
+ <div class="bc-lap" id="bcLap">Round 1 of 12</div>
1982
3562
  <div class="bc-tracks">
1983
3563
  <span class="t credit">Credit<span class="v" id="bcCredit">5</span></span>
1984
3564
  <span class="t resist">Resist<span class="v" id="bcResist">2</span></span>
@@ -1992,8 +3572,9 @@ function renderBoard() {
1992
3572
 
1993
3573
  function applyOwnership() {
1994
3574
  document.querySelectorAll('.board-cell').forEach(cell => {
1995
- cell.classList.remove('owned-p0','owned-p1','owned-p2','active-space');
1996
- cell.querySelectorAll('.owner-dot, .tier').forEach(e => e.remove());
3575
+ cell.classList.remove('owned-p0','owned-p1','owned-p2','active-space','you-here','opponent-active');
3576
+ cell.removeAttribute('data-opp-label');
3577
+ cell.querySelectorAll('.owner-dot, .tier, .owner-mark').forEach(e => e.remove());
1997
3578
  cell.querySelectorAll('.tokens').forEach(e => e.innerHTML = '');
1998
3579
  });
1999
3580
  STATE.players.forEach((p, idx) => {
@@ -2001,21 +3582,41 @@ function applyOwnership() {
2001
3582
  const cell = document.querySelector('.board-cell[data-num="' + a.spaceNum + '"]');
2002
3583
  if (!cell) return;
2003
3584
  cell.classList.add('owned-p' + idx);
2004
- cell.appendChild(el('div', 'owner-dot'));
3585
+ /* v0.18 human playability rebuild — replace plain dot with a sigil/initial badge */
3586
+ const mark = el('div', 'owner-mark');
3587
+ const glyph = idx === 0 ? (p.glyph || '§') : (p.name?.[0] || ['Y','H','M'][idx]);
3588
+ mark.innerHTML = '<span>' + glyph + '</span>';
3589
+ cell.appendChild(mark);
2005
3590
  if (a.tier > 0) cell.appendChild(el('div', 'tier', 'I'.repeat(a.tier)));
2006
3591
  });
2007
3592
  });
2008
- /* Tokens */
3593
+ /* Player pawns on the board — bigger, with initial/sigil inside */
2009
3594
  STATE.players.forEach((p, idx) => {
2010
3595
  const cell = document.querySelector('.board-cell[data-num="' + p.position + '"]');
2011
3596
  if (!cell) return;
2012
3597
  const tw = cell.querySelector('.tokens');
2013
- if (tw) { const t = el('div', 'tok p' + idx); tw.appendChild(t); }
3598
+ if (tw) {
3599
+ const t = el('div', 'tok p' + idx + (STATE.activePlayerIndex === idx ? ' active-pawn' : ''));
3600
+ const glyph = idx === 0 ? (p.glyph || '§') : (p.name?.[0] || ['Y','H','M'][idx]);
3601
+ t.innerHTML = '<span>' + glyph + '</span>';
3602
+ tw.appendChild(t);
3603
+ }
2014
3604
  });
2015
- /* Highlight active player's position */
2016
- const cur = document.querySelector('.board-cell[data-num="' + STATE.players[STATE.activePlayerIndex].position + '"]');
2017
- if (cur) cur.classList.add('active-space');
2018
- document.getElementById('bcLap').textContent = 'Lap ' + Math.min(STATE.lap, 7) + ' / 7';
3605
+ /* Highlight YOU and the active opponent distinctly */
3606
+ const youCell = document.querySelector('.board-cell[data-num="' + STATE.players[0].position + '"]');
3607
+ if (youCell) youCell.classList.add('you-here');
3608
+ const activeIdx = STATE.activePlayerIndex;
3609
+ if (activeIdx !== 0) {
3610
+ const oppCell = document.querySelector('.board-cell[data-num="' + STATE.players[activeIdx].position + '"]');
3611
+ if (oppCell) {
3612
+ oppCell.classList.add('opponent-active');
3613
+ oppCell.setAttribute('data-opp-label', STATE.players[activeIdx].name.toUpperCase() + "'S TURN");
3614
+ }
3615
+ } else {
3616
+ /* Your own active space pulse */
3617
+ if (youCell) youCell.classList.add('active-space');
3618
+ }
3619
+ document.getElementById('bcLap').textContent = 'Round ' + Math.min(STATE.lap, TOTAL_ROUNDS) + ' of ' + TOTAL_ROUNDS + (STATE.lap >= LATE_REPUBLIC_START ? ' · Late Republic' : '');
2019
3620
  document.getElementById('bcCredit').textContent = STATE.tracks.credit.value;
2020
3621
  document.getElementById('bcResist').textContent = STATE.tracks.resistance.value;
2021
3622
  document.getElementById('bcIndust').textContent = STATE.tracks.capacity.value;
@@ -2082,12 +3683,22 @@ function renderTracks() {
2082
3683
  ];
2083
3684
  TRACKS.forEach(t => {
2084
3685
  const v = STATE.tracks[t.key].value;
2085
- const row = el('div', 't-row ' + t.cls);
3686
+ /* v0.18 polished danger-zone classes so failure thresholds are visible at a glance */
3687
+ let warnCls = '';
3688
+ if (t.key === 'credit' && v === 0) warnCls = ' in-default';
3689
+ else if (t.key === 'credit' && v <= 4) warnCls = ' in-warning';
3690
+ else if (t.key === 'resistance' && v >= 10) warnCls = ' in-warning';
3691
+ const row = el('div', 't-row ' + t.cls + warnCls);
2086
3692
  let html = '<div class="lbl-t">' + t.label + '</div><div class="val">' + v + '</div>';
2087
3693
  html += '<div class="scale">';
2088
- for (let i = 0; i <= 12; i++) html += '<div class="tk' + (i%3===0?' major':'') + (i===v?' marker':'') + '">' + i + '</div>';
3694
+ for (let i = 0; i <= 12; i++) html += '<div class="tk' + (i%3===0?' major':'') + (i===v?' marker':'') + '" data-pos="' + i + '">' + i + '</div>';
2089
3695
  html += '</div>';
2090
3696
  html += '<div class="reason">Reason: ' + STATE.tracks[t.key].lastReason + '</div>';
3697
+ /* Crisis tag: visible chip when warning band entered or sticky flag set (Credit Crisis already fired) */
3698
+ if (t.key === 'credit' && v <= 4 && v > 0) html += '<span class="crisis-tag warning">⚠ Credit Crisis zone</span> ';
3699
+ if (t.key === 'credit' && v === 0) html += '<span class="crisis-tag warning">⚠ Default</span> ';
3700
+ if (t.key === 'credit' && STATE.flags.creditCrisisFired) html += '<span class="crisis-tag locked">Credit Crisis fired</span> ';
3701
+ if (t.key === 'resistance' && v >= 10) html += '<span class="crisis-tag warning">⚠ Rebellion approaches</span> ';
2091
3702
  t.thresholds.forEach(th => {
2092
3703
  const hit = v >= th.at;
2093
3704
  html += '<span class="' + (hit ? 'threshold-hit' : 'threshold-miss') + '">' + (hit ? '✓ ' : '○ ') + th.msg + '</span> ';
@@ -2221,7 +3832,7 @@ function renderCardDrawer() {
2221
3832
  actions = `<em style="font-family:var(--display);font-size:11px;opacity:.7">Opponent resolving automatically…</em>`;
2222
3833
  }
2223
3834
  root.innerHTML = `
2224
- <div class="drawer-card">
3835
+ <div class="drawer-card" data-deck="${deck}">
2225
3836
  <div class="band" style="background:${bandColor}"><span>${deckLabel}</span><span>№ ${String(card.id).padStart(2,'0')}</span></div>
2226
3837
  <div style="font-family:var(--ui);font-size:8.5px;letter-spacing:.18em;text-transform:uppercase;text-align:center;margin-top:4px;opacity:.7">Drawn by ${drawnBy}</div>
2227
3838
  <div class="nm">${card.name}</div>
@@ -2285,7 +3896,12 @@ function renderLedger() {
2285
3896
  root.innerHTML = '';
2286
3897
  STATE.ledger.slice().reverse().forEach((row, i) => {
2287
3898
  const stamp = 'L' + row.lap + '·T' + row.turn + '·#' + (STATE.ledger.length - i);
2288
- const r = el('div', 'row ' + (row.cls || ''));
3899
+ /* v0.18 polished failure-tier emphasis classes (display-only; event identifiers preserved) */
3900
+ let extraCls = '';
3901
+ if (row.event === 'CREDIT_CRISIS') extraCls = ' row-credit-crisis';
3902
+ else if (row.event === 'DEFAULT') extraCls = ' row-default';
3903
+ else if (row.event === 'REBELLION') extraCls = ' row-rebellion';
3904
+ const r = el('div', 'row ' + (row.cls || '') + extraCls);
2289
3905
  r.innerHTML = `<span class="stamp">${stamp}</span><span class="actor">${row.actor} · ${row.event}</span><span>${row.detail}</span>`;
2290
3906
  root.appendChild(r);
2291
3907
  });
@@ -2296,29 +3912,204 @@ function renderControls() {
2296
3912
  const bar = document.getElementById('controlsBar');
2297
3913
  bar.innerHTML = '';
2298
3914
  const isHumanTurn = STATE.activePlayerIndex === 0;
2299
- if (STATE.phase === 'awaiting-roll' && isHumanTurn) {
2300
- const roll = el('button', 'primary', '🎲 Roll dice');
2301
- roll.onclick = () => dispatch({ type:'ROLL_DICE' });
2302
- bar.appendChild(roll);
2303
- } else if (STATE.phase === 'crisis-choice' && isHumanTurn) {
2304
- const lbl = el('div', '', '<em style="font-family:var(--display);font-size:11px;margin-right:8px">In Crisis. Choose:</em>');
2305
- bar.appendChild(lbl);
2306
- const pay = el('button', 'primary', 'Pay 50 TN');
2307
- pay.onclick = () => dispatch({ type:'ROLL_DICE', crisisChoice: 'pay' });
2308
- bar.appendChild(pay);
2309
- const dbl = el('button', '', 'Roll for doubles');
2310
- dbl.onclick = () => dispatch({ type:'ROLL_DICE', crisisChoice: 'doubles' });
2311
- bar.appendChild(dbl);
2312
- const skp = el('button', '', 'Skip turn');
2313
- skp.onclick = () => dispatch({ type:'ROLL_DICE', crisisChoice: 'skip' });
2314
- bar.appendChild(skp);
2315
- } else if (STATE.phase === 'awaiting-roll' && !isHumanTurn) {
2316
- const hint = el('div', '', `<em style="font-family:var(--display);font-size:11px">${STATE.players[STATE.activePlayerIndex].name} is taking their turn…</em>`);
2317
- bar.appendChild(hint);
2318
- } else if (STATE.phase === 'asset-decision' && !isHumanTurn) {
2319
- const hint = el('div', '', `<em style="font-family:var(--display);font-size:11px">${STATE.players[STATE.activePlayerIndex].name} is deciding buy / decline…</em>`);
2320
- bar.appendChild(hint);
2321
- /* Auto-resolve opponent decision */
3915
+ const phase = STATE.phase;
3916
+ let humanDecisionPending = false;
3917
+ const isFirstLap = STATE.lap === 1;
3918
+
3919
+ /* Positions strip always rendered first so the player can read where everyone is */
3920
+ if (phase !== 'game-over') {
3921
+ const strip = el('div', 'positions-strip');
3922
+ STATE.players.forEach((p, idx) => {
3923
+ const row = el('div', 'pos-row p' + idx + (STATE.activePlayerIndex === idx ? ' is-active' : ''));
3924
+ const glyph = idx === 0 ? (p.glyph || '§') : (p.name?.[0] || ['Y','H','M'][idx]);
3925
+ const where = SPACES[p.position]?.name || ('space ' + p.position);
3926
+ /* Show current Influence (mandate score) inline */
3927
+ const ip = scorePlayer(STATE, idx).total;
3928
+ /* Mandate distance badge — leader only, only from MANDATE.minRound onward */
3929
+ let mandateBadge = '';
3930
+ if (STATE.lap >= MANDATE.minRound && STATE.phase !== 'game-over') {
3931
+ const scored = STATE.players.map((pl, i) => ({ idx: i, total: scorePlayer(STATE, i).total }));
3932
+ const sorted = scored.slice().sort((a, b) => b.total - a.total);
3933
+ const top = sorted[0], second = sorted[1];
3934
+ if (idx === top.idx && top.total > second.total) {
3935
+ const fromThreshold = MANDATE.threshold - top.total;
3936
+ const fromLead = MANDATE.lead - (top.total - second.total);
3937
+ let badgeText;
3938
+ if (fromThreshold > 0 && fromLead > 0) {
3939
+ badgeText = fromThreshold + ' from threshold · +' + fromLead + ' lead needed';
3940
+ } else if (fromThreshold > 0) {
3941
+ badgeText = fromThreshold + ' from threshold';
3942
+ } else if (fromLead > 0) {
3943
+ badgeText = 'lead ' + (top.total - second.total) + ' of ' + MANDATE.lead;
3944
+ } else {
3945
+ badgeText = 'mandate eligible';
3946
+ }
3947
+ mandateBadge = '<div class="pos-mandate">' + badgeText + '</div>';
3948
+ }
3949
+ }
3950
+ row.innerHTML = '<div class="pos-tok"><span>' + glyph + '</span></div>' +
3951
+ '<div>' +
3952
+ '<div class="pos-name">' + p.name + '</div>' +
3953
+ '<div class="pos-space">' + where + '</div>' +
3954
+ '<div class="pos-cash">' + p.cash.toLocaleString('en-US') + ' \u00b7 <span class="pos-ip">' + ip + ' IP</span></div>' +
3955
+ mandateBadge +
3956
+ '</div>';
3957
+ strip.appendChild(row);
3958
+ });
3959
+ bar.appendChild(strip);
3960
+
3961
+ /* Landing outcome band — explains what just happened to YOU since your last roll */
3962
+ if (LAST_LANDING_OUTCOME && LAST_LANDING_OUTCOME.text) {
3963
+ const lb = el('div', 'landing-band ' + (LAST_LANDING_OUTCOME.cls || ''));
3964
+ lb.innerHTML = '<div class="lb-eyebrow">What just happened</div>' +
3965
+ '<div class="lb-text">' + LAST_LANDING_OUTCOME.text + '</div>';
3966
+ bar.appendChild(lb);
3967
+ }
3968
+
3969
+ /* Portfolio strip — compact "your holdings" so the player doesn't need the sidebar */
3970
+ const you = STATE.players[0];
3971
+ const pf = el('div', 'portfolio-strip');
3972
+ const head = el('div', 'pf-head');
3973
+ const count = you.ownedAssets.length;
3974
+ head.innerHTML = '<div class="pf-eyebrow">Your portfolio</div>' +
3975
+ '<div class="pf-line">' + (count === 0 ? 'no holdings yet' : (count + ' holding' + (count === 1 ? '' : 's'))) + '</div>' +
3976
+ '<div class="pf-cash">' + you.cash.toLocaleString('en-US') + '</div>';
3977
+ pf.appendChild(head);
3978
+ const pflist = el('div', 'pf-list');
3979
+ if (count === 0) {
3980
+ pflist.innerHTML = '<span class="pf-empty">Land on an unowned property to buy your first holding.</span>';
3981
+ } else {
3982
+ you.ownedAssets.forEach(a => {
3983
+ const sp = SPACES[a.spaceNum];
3984
+ const asset = ASSETS[a.spaceNum];
3985
+ const kind = (sp.kind === 'route' || sp.kind === 'institution') ? sp.kind : '';
3986
+ const sys = (kind === '' && asset && asset.sys) ? asset.sys : '';
3987
+ const chip = el('span', 'pf-chip');
3988
+ if (sys) chip.setAttribute('data-sys', sys);
3989
+ if (kind) chip.setAttribute('data-kind', kind);
3990
+ chip.innerHTML = sp.name + (a.tier > 0 ? ' <span class="tier-roman">' + 'I'.repeat(a.tier) + '</span>' : '');
3991
+ pflist.appendChild(chip);
3992
+ });
3993
+ }
3994
+ pf.appendChild(pflist);
3995
+ const openBtn = el('button', 'pf-open', count === 0 ? 'Open panels' : 'Open portfolio');
3996
+ openBtn.type = 'button';
3997
+ openBtn.title = 'Show the right-side panels (Treasury, opponents, ledger)';
3998
+ openBtn.onclick = () => {
3999
+ document.body.classList.remove('sidebar-collapsed');
4000
+ try { localStorage.setItem(SIDEBAR_KEY, '0'); } catch (e) {}
4001
+ const lbl = document.getElementById('sidebarToggleLabel');
4002
+ if (lbl) lbl.textContent = 'Hide panels';
4003
+ const t = document.getElementById('panelTreasury');
4004
+ if (t && t.scrollIntoView) t.scrollIntoView({ behavior:'smooth', block:'start' });
4005
+ };
4006
+ pf.appendChild(openBtn);
4007
+ bar.appendChild(pf);
4008
+ }
4009
+
4010
+ /* v0.18 playtest fix — always show an action banner so the user knows what to do next */
4011
+ function addBanner(eyebrow, prompt, hint, actions, opts) {
4012
+ opts = opts || {};
4013
+ const banner = el('div', 'action-banner' + (opts.opponent ? ' opponent-turn' : '') + (opts.crisis ? ' crisis-rail' : '') + (opts.endgame ? ' endgame-rail' : ''));
4014
+ const textBox = el('div', 'ab-text');
4015
+ let inner = '';
4016
+ if (eyebrow) inner += '<div class="ab-eyebrow">' + eyebrow + '</div>';
4017
+ if (prompt) inner += '<div class="ab-prompt">' + prompt + '</div>';
4018
+ if (hint) inner += '<div class="ab-hint">' + hint + '</div>';
4019
+ textBox.innerHTML = inner;
4020
+ banner.appendChild(textBox);
4021
+ if (actions && actions.length) {
4022
+ const btns = el('div', 'ab-actions');
4023
+ actions.forEach(a => {
4024
+ if (a.input) {
4025
+ const wrap = document.createElement('span');
4026
+ wrap.style.display = 'inline-flex';
4027
+ wrap.style.gap = '6px';
4028
+ wrap.style.alignItems = 'center';
4029
+ const inp = document.createElement('input');
4030
+ inp.type = 'number'; inp.id = a.input.id; inp.min = a.input.min || 10;
4031
+ inp.placeholder = a.input.placeholder || '';
4032
+ inp.value = a.input.value || '';
4033
+ wrap.appendChild(inp);
4034
+ const btn = el('button', a.primary ? 'primary' : '', a.label);
4035
+ if (a.disabled) btn.disabled = true;
4036
+ btn.onclick = () => a.onClick(inp.value);
4037
+ wrap.appendChild(btn);
4038
+ btns.appendChild(wrap);
4039
+ return;
4040
+ }
4041
+ const btn = el('button', a.primary ? 'primary' : '', a.label);
4042
+ if (a.disabled) btn.disabled = true;
4043
+ btn.onclick = a.onClick;
4044
+ btns.appendChild(btn);
4045
+ });
4046
+ banner.appendChild(btns);
4047
+ }
4048
+ /* First-lap helping hand — calm guidance for the first time you see each phase */
4049
+ if (opts.firstLapHint) {
4050
+ const hintEl = el('div', 'first-lap-hint');
4051
+ hintEl.innerHTML = opts.firstLapHint;
4052
+ banner.appendChild(hintEl);
4053
+ }
4054
+ bar.appendChild(banner);
4055
+ }
4056
+
4057
+ /* Human's vote outstanding on the current Act */
4058
+ if (phase === 'act-vote' && STATE.acts.current && STATE.acts.current.votes[0] == null) {
4059
+ humanDecisionPending = true;
4060
+ const actId = STATE.acts.current.actId;
4061
+ const act = ACTS.find(a => a.id === actId);
4062
+ addBanner('Congress · Floor Vote',
4063
+ 'The <em>' + act.name + '</em> is on the floor.',
4064
+ 'What it does: ' + (act.summary || act.effect || 'see panel') + '<br /><strong>Vote YES</strong> to pass it. <strong>Vote NO</strong> to block. Pass needs majority.', [
4065
+ { label:'Vote YES', primary:true, onClick: () => humanVote('yes') },
4066
+ { label:'Vote NO', onClick: () => humanVote('no') },
4067
+ ], isFirstLap ? { firstLapHint: 'Acts of Congress change the whole Republic. Read the effect, then choose. Most Acts only come up once — this is your moment to influence them.' } : undefined);
4068
+ }
4069
+ else if (phase === 'act-vote') {
4070
+ addBanner('Congress · Floor Vote', 'Opponents are casting their ballots…', 'The game continues automatically.', [], { opponent:true });
4071
+ }
4072
+ else if (phase === 'awaiting-roll' && isHumanTurn) {
4073
+ humanDecisionPending = true;
4074
+ addBanner('Your turn', 'Roll the dice to move through the Republic.', 'Two six-sided dice. Land where you land.', [
4075
+ { label:'🎲 Roll dice', primary:true, onClick: () => rollWithAnimation() },
4076
+ ], isFirstLap ? { firstLapHint: 'Start your first turn — roll the dice. The game will move your delegate and explain whatever you land on.' } : undefined);
4077
+ }
4078
+ else if (phase === 'crisis-choice' && isHumanTurn) {
4079
+ humanDecisionPending = true;
4080
+ addBanner('Constitutional Crisis', 'You sit in the Crisis chamber.', 'Pay to leave, gamble on doubles, or skip this turn.', [
4081
+ { label:'Pay 50 TN', primary:true, onClick: () => dispatch({ type:'ROLL_DICE', crisisChoice: 'pay' }) },
4082
+ { label:'Roll for doubles', onClick: () => dispatch({ type:'ROLL_DICE', crisisChoice: 'doubles' }) },
4083
+ { label:'Skip turn', onClick: () => dispatch({ type:'ROLL_DICE', crisisChoice: 'skip' }) },
4084
+ ]);
4085
+ }
4086
+ else if (phase === 'awaiting-roll' && !isHumanTurn) {
4087
+ addBanner('Opponent turn', STATE.players[STATE.activePlayerIndex].name + ' is taking their turn…', 'The game will continue automatically.', [], { opponent:true });
4088
+ }
4089
+ else if (phase === 'asset-decision' && isHumanTurn && STATE.pendingLanding) {
4090
+ humanDecisionPending = true;
4091
+ const num = STATE.pendingLanding.spaceNum;
4092
+ const sp = SPACES[num];
4093
+ const asset = ASSETS[num];
4094
+ const cost = asset ? asset.cost : 0;
4095
+ const owned = findOwnerIndex(STATE, num);
4096
+ if (owned >= 0 && owned !== 0) {
4097
+ /* Landed on opponent property — rent owed (already auto-settled by reducer in this branch).
4098
+ This branch shouldn't normally hit asset-decision; defensive log only. */
4099
+ addBanner('Landing', 'You stand on ' + sp.name + ' — owned by ' + STATE.players[owned].name + '.', '', []);
4100
+ } else {
4101
+ const canBuy = STATE.players[0].cash >= cost;
4102
+ addBanner('Landing',
4103
+ 'You landed on <em>' + sp.name + '</em>.',
4104
+ canBuy ? 'Buy it outright for ' + cost + ' TN, or decline and let it go to forced auction.'
4105
+ : 'You cannot afford ' + cost + ' TN — you must decline; it goes to forced auction.', [
4106
+ { label:'Buy ' + cost + ' TN', primary:true, disabled: !canBuy, onClick: () => dispatch({ type:'BUY_ASSET' }) },
4107
+ { label:'Decline → Auction', onClick: () => dispatch({ type:'DECLINE_ASSET' }) },
4108
+ ], isFirstLap ? { firstLapHint: 'Buying gives you income whenever players land on this space and counts toward your final Influence. Declining sends it to auction — sometimes the right call if your cash is low.' } : undefined);
4109
+ }
4110
+ }
4111
+ else if (phase === 'asset-decision' && !isHumanTurn) {
4112
+ addBanner('Opponent turn', STATE.players[STATE.activePlayerIndex].name + ' is deciding buy or decline…', '', [], { opponent:true });
2322
4113
  setTimeout(() => {
2323
4114
  if (STATE.phase !== 'asset-decision' || STATE.activePlayerIndex === 0) return;
2324
4115
  const pIdx = STATE.activePlayerIndex;
@@ -2326,8 +4117,64 @@ function renderControls() {
2326
4117
  const decision = PROFILES[STATE.players[pIdx].profile].decideBuy(STATE, pIdx, num);
2327
4118
  if (decision.buy) dispatch({ type:'BUY_ASSET', reason: decision.reason });
2328
4119
  else dispatch({ type:'DECLINE_ASSET', reason: decision.reason });
2329
- }, 250);
4120
+ }, 1700);
4121
+ }
4122
+ else if (phase === 'card-resolve' && isHumanTurn && STATE.pendingCard) {
4123
+ humanDecisionPending = true;
4124
+ const card = (STATE.pendingCard.deck === 'market' ? MARKET_SHOCK_CARDS : REPUBLIC_DEBATE_CARDS).find(c => c.id === STATE.pendingCard.cardId);
4125
+ const eyebrow = STATE.pendingCard.deck === 'market' ? 'Market Shock' : 'Republic Debate';
4126
+ const effectLine = card && card.effectText ? card.effectText.replace(/<[^>]+>/g, '') : 'See the card on the right for details.';
4127
+ addBanner(eyebrow, 'You drew <em>' + (card ? card.name : 'an event card') + '</em>.', 'Effect: ' + effectLine, [
4128
+ { label:'Resolve effect', primary:true, onClick: () => dispatch({ type:'RESOLVE_CARD' }) },
4129
+ ], isFirstLap ? { firstLapHint: 'Cards are historical shocks and debates. Resolve to apply the effect — most fire instantly, some send a property to auction.' } : undefined);
4130
+ }
4131
+ else if (phase === 'card-resolve' && !isHumanTurn) {
4132
+ addBanner('Opponent turn', STATE.players[STATE.activePlayerIndex].name + ' resolves their card…', '', [], { opponent:true });
4133
+ }
4134
+ else if (phase === 'card-choice' && isHumanTurn && STATE.pendingCard) {
4135
+ humanDecisionPending = true;
4136
+ const card = (STATE.pendingCard.deck === 'market' ? MARKET_SHOCK_CARDS : REPUBLIC_DEBATE_CARDS).find(c => c.id === STATE.pendingCard.cardId);
4137
+ if (card && card.choices) {
4138
+ const effectLine = card.effectText ? card.effectText.replace(/<[^>]+>/g, '') : '';
4139
+ addBanner('Card decision', 'The card requires a choice.', effectLine,
4140
+ card.choices.map((c, i) => ({ label: c.label, primary: i === 0, onClick: () => dispatch({ type:'RESOLVE_CARD_CHOICE', choiceIndex: i }) }))
4141
+ );
4142
+ }
4143
+ }
4144
+ else if (phase === 'auction' && STATE.pendingAuction) {
4145
+ const a = STATE.pendingAuction;
4146
+ const humanUp = a.bidsRemaining[0] === 0;
4147
+ const spaceName = SPACES[a.spaceNum].name;
4148
+ if (humanUp) {
4149
+ humanDecisionPending = true;
4150
+ addBanner('Auction', '<em>' + spaceName + '</em> is up for auction.', 'Current high bid: ' + a.highBid + ' TN. Enter a bid (must exceed the high) or pass.', [
4151
+ { input: { id:'humanBidInput', min: a.highBid + 10, placeholder: 'bid' }, label:'Bid', primary:true, onClick: (v) => {
4152
+ const amount = parseInt(v, 10);
4153
+ if (isNaN(amount) || amount < 10 || amount > STATE.players[0].cash) { alert('Enter a bid between 10 and your cash on hand.'); return; }
4154
+ if (amount <= STATE.pendingAuction.highBid) { alert('Bid must exceed current high bid of ' + STATE.pendingAuction.highBid); return; }
4155
+ dispatch({ type:'AUCTION_BID', playerIndex: 0, amount, reason: 'human bid' });
4156
+ } },
4157
+ { label:'Pass', onClick: () => submitHumanPass() },
4158
+ ]);
4159
+ } else {
4160
+ const bidder = STATE.players[a.bidsRemaining[0]];
4161
+ addBanner('Auction', bidder.name + ' is considering their bid on ' + spaceName + '…', 'High bid: ' + a.highBid + ' TN.', [], { opponent:true });
4162
+ }
4163
+ }
4164
+ else if (phase === 'game-over') {
4165
+ document.body.classList.add('endgame-active');
4166
+ addBanner('Twelve rounds complete', 'The founding struggle has ended. Final tally posted in the report.', '', [
4167
+ { label:'View final report', primary:true, onClick: () => document.getElementById('endgameView')?.scrollIntoView({behavior:'smooth', block:'start'}) },
4168
+ ], { endgame:true });
2330
4169
  }
4170
+ else if (!isHumanTurn) {
4171
+ addBanner('Opponent turn', STATE.players[STATE.activePlayerIndex].name + ' is taking their turn…', '', [], { opponent:true });
4172
+ }
4173
+
4174
+ /* Dim side panels during human decisions */
4175
+ document.body.classList.toggle('decision-active', humanDecisionPending);
4176
+ if (phase !== 'game-over') document.body.classList.remove('endgame-active');
4177
+
2331
4178
  document.getElementById('phasePill').textContent = 'phase: ' + STATE.phase;
2332
4179
  document.getElementById('seedPill').textContent = 'seed: ' + STATE.rngSeed;
2333
4180
  const ap = STATE.players[STATE.activePlayerIndex];
@@ -2346,14 +4193,39 @@ function renderEndgame() {
2346
4193
  const root = document.getElementById('endgameView');
2347
4194
  root.classList.remove('hidden');
2348
4195
  const winner = STATE.players.reduce((m, p, i) => STATE.finalScores[i].total > STATE.finalScores[m.idx].total ? { idx: i, total: STATE.finalScores[i].total } : m, { idx: 0, total: STATE.finalScores[0].total });
4196
+ /* v0.18 polished — failure-tier posture chips (display-only) */
4197
+ const _ledger = STATE.ledger;
4198
+ const _crisisFired = !!STATE.flags.creditCrisisFired || _ledger.some(r => r.event === 'CREDIT_CRISIS');
4199
+ const _defaultFired = _ledger.some(r => r.event === 'DEFAULT');
4200
+ const _rebellionFired = _ledger.some(r => r.event === 'REBELLION');
4201
+ const _finalCred = STATE.tracks.credit.value;
4202
+ let _postureCls = 'stable', _postureLbl = 'Stable credit';
4203
+ if (_finalCred <= 2) { _postureCls = 'collapsed'; _postureLbl = 'Collapsed credit'; }
4204
+ else if (_finalCred <= 5 || _crisisFired || _defaultFired) { _postureCls = 'strained'; _postureLbl = 'Strained credit'; }
4205
+ const _crisisChip = _defaultFired
4206
+ ? '<span class="posture-chip collapsed">Default fired</span>'
4207
+ : _crisisFired ? '<span class="posture-chip fired">Credit Crisis fired</span>' : '<span class="posture-chip avoided">No Credit Crisis</span>';
4208
+ const _rebellionChip = _rebellionFired
4209
+ ? '<span class="posture-chip collapsed">Rebellion fired</span>'
4210
+ : '<span class="posture-chip avoided">No Rebellion</span>';
4211
+ const _m = STATE.mandateTrigger;
4212
+ const _endTitle = _m ? 'Final Accounting' : 'Twelve rounds complete';
4213
+ const _endSub = _m
4214
+ ? _m.playerName + ' claimed the mandate at round ' + _m.round + ' with ' + _m.influence + ' Influence, leading by ' + _m.lead + '.'
4215
+ : 'No founder secured a mandate. The Republic decides by final accounting after twelve rounds.';
2349
4216
  let html = `
2350
- <h2>Endgame Report <span class="surface-id">H</span></h2>
2351
- <div class="sub">After lap 7. Three players. Influence breakdown computed from rules.</div>
4217
+ <h2>${_endTitle} <span class="surface-id">H</span></h2>
4218
+ <div class="sub">${_endSub}</div>
2352
4219
  <div class="winner">
2353
4220
  <div class="lbl">Winner</div>
2354
4221
  <div class="nm">${STATE.players[winner.idx].name} · ${PROFILES[STATE.players[winner.idx].profile]?.label || STATE.players[winner.idx].role}</div>
2355
4222
  <div class="score">Influence ${winner.total} · cash $${STATE.players[winner.idx].cash.toLocaleString('en-US')} · ${STATE.players[winner.idx].ownedAssets.length} assets</div>
2356
4223
  </div>
4224
+ <div class="posture-row">
4225
+ <span class="posture-chip ${_postureCls}">${_postureLbl} · Credit ${_finalCred}</span>
4226
+ ${_crisisChip}
4227
+ ${_rebellionChip}
4228
+ </div>
2357
4229
  <div class="results">`;
2358
4230
  STATE.players.forEach((p, i) => {
2359
4231
  const score = STATE.finalScores[i];
@@ -2393,6 +4265,608 @@ function startNewGame(seed) {
2393
4265
 
2394
4266
  document.getElementById('btnNewSeed').addEventListener('click', () => startNewGame(Math.floor(Math.random() * 1e9)));
2395
4267
  document.getElementById('btnReset').addEventListener('click', () => startNewGame(STATE.rngSeed));
4268
+
4269
+ /* Dice rolling animation — flashes two dice for ~700ms, then lands and dispatches.
4270
+ Determinism preserved: the reducer is called once at the end; the visual is decorative. */
4271
+ const DICE_FACES = ['⚀','⚁','⚂','⚃','⚄','⚅'];
4272
+ function rollWithAnimation() {
4273
+ const overlay = document.getElementById('diceOverlay');
4274
+ const d1 = document.getElementById('die1');
4275
+ const d2 = document.getElementById('die2');
4276
+ const sum = document.getElementById('diceSum');
4277
+ if (!overlay || !d1 || !d2) { dispatch({ type:'ROLL_DICE' }); return; }
4278
+ overlay.classList.remove('hidden', 'settled');
4279
+ sum.textContent = 'rolling…';
4280
+ let ticks = 0;
4281
+ const iv = setInterval(() => {
4282
+ d1.textContent = DICE_FACES[Math.floor(Math.random() * 6)];
4283
+ d2.textContent = DICE_FACES[Math.floor(Math.random() * 6)];
4284
+ ticks++;
4285
+ if (ticks >= 12) {
4286
+ clearInterval(iv);
4287
+ /* Dispatch and read the actual rolled value */
4288
+ dispatch({ type:'ROLL_DICE' });
4289
+ const last = STATE.lastRoll;
4290
+ if (last && last.d1 && last.d2) {
4291
+ d1.textContent = DICE_FACES[last.d1 - 1];
4292
+ d2.textContent = DICE_FACES[last.d2 - 1];
4293
+ sum.textContent = (last.d1 + last.d2) + ' moves';
4294
+ overlay.classList.add('settled');
4295
+ }
4296
+ setTimeout(() => overlay.classList.add('hidden'), 1400);
4297
+ }
4298
+ }, 160);
4299
+ }
4300
+
4301
+ /* Orientation panel — 3-step welcome (Welcome → Delegate → Tour) + recall via "How to play" button */
4302
+ const ORIENT_KEY = 'sovereign.orientation.seen';
4303
+ const PLAYER_NAME_KEY = 'sovereign.playerName';
4304
+ const PLAYER_GLYPH_KEY = 'sovereign.playerGlyph';
4305
+ const PLAYER_ROLE_KEY = 'sovereign.playerRole';
4306
+ let WELCOME_STEP = 1;
4307
+ const WELCOME_LABELS = {
4308
+ 1: { title: 'Sovereign', meta: 'Step 1 of 3 · You can recall this any time from "How to play".', btn: 'Begin' },
4309
+ 2: { title: 'Choose your delegate', meta: 'Step 2 of 3 · Your role, name, and sigil persist across games.', btn: 'Next' },
4310
+ 3: { title: 'How a turn works', meta: 'Step 3 of 3 · No more overlays after this.', btn: 'Start the game' },
4311
+ };
4312
+
4313
+ const ROLE_DEFS = {
4314
+ treasury: { profile:'treasury-finance', label:'Treasury / Finance', defaultName:'You' },
4315
+ merchant: { profile:'merchant-infrastructure', label:'Merchant / Infrastructure', defaultName:'You' },
4316
+ manufacturer: { profile:'manufacturer-industry', label:'Manufacturer / Industry', defaultName:'You' },
4317
+ };
4318
+ const ROLE_OPPS = {
4319
+ treasury: [
4320
+ { profile:'merchant-infrastructure', name:'Morris', role:'Merchant / Infrastructure' },
4321
+ { profile:'manufacturer-industry', name:'Slater', role:'Manufacturer / Industry' },
4322
+ ],
4323
+ merchant: [
4324
+ { profile:'treasury-finance', name:'Hamilton', role:'Treasury / Finance' },
4325
+ { profile:'manufacturer-industry', name:'Slater', role:'Manufacturer / Industry' },
4326
+ ],
4327
+ manufacturer: [
4328
+ { profile:'treasury-finance', name:'Hamilton', role:'Treasury / Finance' },
4329
+ { profile:'merchant-infrastructure', name:'Morris', role:'Merchant / Infrastructure' },
4330
+ ],
4331
+ };
4332
+
4333
+ function getPlayerCustom() {
4334
+ let name = 'You', glyph = '§', role = 'treasury';
4335
+ try {
4336
+ name = localStorage.getItem(PLAYER_NAME_KEY) || 'You';
4337
+ glyph = localStorage.getItem(PLAYER_GLYPH_KEY) || '§';
4338
+ role = localStorage.getItem(PLAYER_ROLE_KEY) || 'treasury';
4339
+ } catch (e) {}
4340
+ if (!ROLE_DEFS[role]) role = 'treasury';
4341
+ return { name: name.trim() || 'You', glyph, role };
4342
+ }
4343
+ function applyPlayerCustom() {
4344
+ const { name, glyph, role } = getPlayerCustom();
4345
+ if (!STATE || !STATE.players || !STATE.players[0]) return;
4346
+ STATE.players[0].name = name;
4347
+ STATE.players[0].glyph = glyph;
4348
+ STATE.players[0].role = ROLE_DEFS[role].label;
4349
+ const opps = ROLE_OPPS[role];
4350
+ if (STATE.players[1]) {
4351
+ STATE.players[1].profile = opps[0].profile;
4352
+ STATE.players[1].name = opps[0].name;
4353
+ STATE.players[1].role = opps[0].role;
4354
+ }
4355
+ if (STATE.players[2]) {
4356
+ STATE.players[2].profile = opps[1].profile;
4357
+ STATE.players[2].name = opps[1].name;
4358
+ STATE.players[2].role = opps[1].role;
4359
+ }
4360
+ }
4361
+ function paintGlyphPicker() {
4362
+ const { name, glyph, role } = getPlayerCustom();
4363
+ const input = document.getElementById('playerNameInput');
4364
+ if (input) input.value = name === 'You' ? '' : name;
4365
+ document.querySelectorAll('#glyphPicker button').forEach(b => {
4366
+ b.classList.toggle('sel', b.dataset.glyph === glyph);
4367
+ b.setAttribute('aria-checked', b.dataset.glyph === glyph ? 'true' : 'false');
4368
+ });
4369
+ document.querySelectorAll('#roleGrid .role-card').forEach(b => {
4370
+ b.classList.toggle('sel', b.dataset.role === role);
4371
+ b.setAttribute('aria-checked', b.dataset.role === role ? 'true' : 'false');
4372
+ });
4373
+ updatePreviewToken();
4374
+ }
4375
+ function updatePreviewToken() {
4376
+ const input = document.getElementById('playerNameInput');
4377
+ const selGlyph = document.querySelector('#glyphPicker button.sel');
4378
+ const selRole = document.querySelector('#roleGrid .role-card.sel');
4379
+ const name = (input?.value || 'You').trim() || 'You';
4380
+ const glyph = selGlyph?.dataset.glyph || '§';
4381
+ const role = selRole?.dataset.role || 'treasury';
4382
+ const previewYou = document.getElementById('previewYouTok');
4383
+ const previewYouName = document.getElementById('previewYouName');
4384
+ if (previewYou) previewYou.innerHTML = '<span>' + glyph + '</span>';
4385
+ if (previewYouName) previewYouName.textContent = name;
4386
+ const opps = ROLE_OPPS[role] || ROLE_OPPS['treasury'];
4387
+ const o1 = document.getElementById('previewOpp1Tok');
4388
+ const o1n = document.getElementById('previewOpp1Name');
4389
+ const o2 = document.getElementById('previewOpp2Tok');
4390
+ const o2n = document.getElementById('previewOpp2Name');
4391
+ if (o1) o1.innerHTML = '<span>' + opps[0].name[0] + '</span>';
4392
+ if (o1n) o1n.textContent = opps[0].name;
4393
+ if (o2) o2.innerHTML = '<span>' + opps[1].name[0] + '</span>';
4394
+ if (o2n) o2n.textContent = opps[1].name;
4395
+ }
4396
+ function savePlayerCustom() {
4397
+ const input = document.getElementById('playerNameInput');
4398
+ const selGlyph = document.querySelector('#glyphPicker button.sel');
4399
+ const selRole = document.querySelector('#roleGrid .role-card.sel');
4400
+ const name = (input?.value || '').trim();
4401
+ const glyph = selGlyph?.dataset.glyph || '§';
4402
+ const role = selRole?.dataset.role || 'treasury';
4403
+ try {
4404
+ if (name) localStorage.setItem(PLAYER_NAME_KEY, name); else localStorage.removeItem(PLAYER_NAME_KEY);
4405
+ localStorage.setItem(PLAYER_GLYPH_KEY, glyph);
4406
+ localStorage.setItem(PLAYER_ROLE_KEY, role);
4407
+ } catch (e) {}
4408
+ applyPlayerCustom();
4409
+ render();
4410
+ renderBoard();
4411
+ applyOwnership();
4412
+ }
4413
+
4414
+ function setWelcomeStep(step) {
4415
+ WELCOME_STEP = step;
4416
+ const panel = document.getElementById('welcomePanel');
4417
+ panel.setAttribute('data-step', String(step));
4418
+ const labels = WELCOME_LABELS[step] || WELCOME_LABELS[1];
4419
+ document.getElementById('orientTitle').textContent = labels.title;
4420
+ document.getElementById('welcomeMeta').textContent = labels.meta;
4421
+ document.getElementById('welcomeAdvance').textContent = labels.btn;
4422
+ document.querySelectorAll('#welcomeProgress .dot').forEach(d => {
4423
+ d.classList.toggle('sel', parseInt(d.dataset.step, 10) === step);
4424
+ });
4425
+ document.body.classList.toggle('tour-step-3', step === 3);
4426
+ if (step === 2) { paintGlyphPicker(); }
4427
+ }
4428
+ function showOrient(startStep) {
4429
+ document.body.classList.add('welcome-active');
4430
+ document.getElementById('orientOverlay').classList.remove('hidden');
4431
+ setWelcomeStep(startStep || 1);
4432
+ }
4433
+ function hideOrient() {
4434
+ document.body.classList.remove('welcome-active');
4435
+ document.body.classList.remove('tour-step-3');
4436
+ /* Always save delegate (in case they edited at step 2) */
4437
+ savePlayerCustom();
4438
+ document.getElementById('orientOverlay').classList.add('hidden');
4439
+ try { localStorage.setItem(ORIENT_KEY, '1'); } catch (e) {}
4440
+ }
4441
+
4442
+ document.getElementById('btnHelp').addEventListener('click', () => showOrient(1));
4443
+ document.getElementById('welcomeAdvance').addEventListener('click', () => {
4444
+ if (WELCOME_STEP === 1) { setWelcomeStep(2); }
4445
+ else if (WELCOME_STEP === 2) { savePlayerCustom(); setWelcomeStep(3); }
4446
+ else { hideOrient(); }
4447
+ });
4448
+ document.getElementById('orientOverlay').addEventListener('click', (e) => {
4449
+ if (e.target.id === 'orientOverlay' && WELCOME_STEP === 3) hideOrient();
4450
+ });
4451
+ document.querySelectorAll('#glyphPicker button').forEach(b => {
4452
+ b.addEventListener('click', () => {
4453
+ document.querySelectorAll('#glyphPicker button').forEach(x => x.classList.remove('sel'));
4454
+ b.classList.add('sel');
4455
+ updatePreviewToken();
4456
+ });
4457
+ });
4458
+ document.querySelectorAll('#roleGrid .role-card').forEach(b => {
4459
+ b.addEventListener('click', () => {
4460
+ document.querySelectorAll('#roleGrid .role-card').forEach(x => x.classList.remove('sel'));
4461
+ b.classList.add('sel');
4462
+ updatePreviewToken();
4463
+ });
4464
+ });
4465
+ document.addEventListener('input', (e) => {
4466
+ if (e.target && e.target.id === 'playerNameInput') updatePreviewToken();
4467
+ });
4468
+ document.addEventListener('keydown', (e) => {
4469
+ if (e.key === 'Escape' && !document.getElementById('orientOverlay').classList.contains('hidden') && WELCOME_STEP === 3) hideOrient();
4470
+ });
4471
+
4472
+ /* Designer gate — Balance Sweep + version pill visible only with ?designer=1 */
4473
+ (function applyDesignerGate() {
4474
+ try {
4475
+ const params = new URLSearchParams(window.location.search);
4476
+ if (params.get('designer') === '1') document.body.classList.add('designer-mode');
4477
+ } catch (e) {}
4478
+ })();
4479
+
4480
+ /* Sidebar collapse — defaults to COLLAPSED so the board takes the stage. Persists to localStorage. */
4481
+ const SIDEBAR_KEY = 'sovereign.sidebarCollapsed';
4482
+ function applySidebarState() {
4483
+ let collapsed = true;
4484
+ try {
4485
+ const raw = localStorage.getItem(SIDEBAR_KEY);
4486
+ if (raw === '0') collapsed = false; else if (raw === '1') collapsed = true;
4487
+ } catch (e) {}
4488
+ document.body.classList.toggle('sidebar-collapsed', collapsed);
4489
+ const lbl = document.getElementById('sidebarToggleLabel');
4490
+ if (lbl) lbl.textContent = collapsed ? 'Show panels' : 'Hide panels';
4491
+ }
4492
+ function toggleSidebar() {
4493
+ const wasCollapsed = document.body.classList.contains('sidebar-collapsed');
4494
+ document.body.classList.toggle('sidebar-collapsed', !wasCollapsed);
4495
+ const lbl = document.getElementById('sidebarToggleLabel');
4496
+ if (lbl) lbl.textContent = !wasCollapsed ? 'Show panels' : 'Hide panels';
4497
+ try { localStorage.setItem(SIDEBAR_KEY, !wasCollapsed ? '1' : '0'); } catch (e) {}
4498
+ }
4499
+ /* =====================================================================
4500
+ LANDING OUTCOME — synthesizes a human-readable summary from the ledger
4501
+ No reducer changes. Display-only.
4502
+ ===================================================================== */
4503
+ let LAST_LANDING_OUTCOME = null;
4504
+ let __LAST_LANDING_LEDGER_LEN = 0;
4505
+
4506
+ function classifyLandingOutcome() {
4507
+ if (!STATE || !STATE.ledger) return;
4508
+ /* Find the most recent MOVE event for player 0 (the human) since we last looked. */
4509
+ const ledger = STATE.ledger;
4510
+ const youName = STATE.players[0]?.name;
4511
+ if (!youName) return;
4512
+ /* Walk backwards from end of ledger looking for a recent MOVE by you. */
4513
+ let moveIdx = -1;
4514
+ for (let i = ledger.length - 1; i >= __LAST_LANDING_LEDGER_LEN; i--) {
4515
+ const r = ledger[i];
4516
+ if (r.actor === youName && r.event === 'MOVE') { moveIdx = i; break; }
4517
+ }
4518
+ if (moveIdx < 0) { __LAST_LANDING_LEDGER_LEN = ledger.length; return; }
4519
+ /* Compose the outcome from rows after the MOVE */
4520
+ const after = ledger.slice(moveIdx + 1);
4521
+ const pos = STATE.players[0].position;
4522
+ const sp = SPACES[pos];
4523
+ const asset = ASSETS[pos];
4524
+ const where = sp?.name || ('space ' + pos);
4525
+ let text = null, cls = 'is-neutral';
4526
+
4527
+ /* Rent paid (CASH row with reason starting "Rent on ...") */
4528
+ const rentRow = after.find(r => r.event === 'CASH' && /^Rent on /.test(r.detail || '') && r.actor === youName);
4529
+ const safeRow = after.find(r => r.event === 'SAFE' && (r.detail || '').includes(youName));
4530
+ const ownRow = after.find(r => r.event === 'OWN' && (r.detail || '').includes(youName));
4531
+ const noRentRow = after.find(r => r.event === 'NO RENT');
4532
+ const crisisRow = after.find(r => r.event === 'CRISIS' && (r.detail || '').includes(youName));
4533
+ const sendRow = after.find(r => r.event === 'SEND' && (r.detail || '').includes(youName));
4534
+ const taxRow = after.find(r => r.event === 'CASH' && / tax\b/.test(r.detail || '') && r.actor === youName);
4535
+ const buyRow = after.find(r => r.event === 'OWN' && (r.detail || '').match(/^Buy /));
4536
+
4537
+ if (rentRow) {
4538
+ const m = rentRow.detail.match(/^Rent on (.+?) to (.+?) · ([+-]?\d+) TN/);
4539
+ if (m) {
4540
+ const space = m[1], ownerName = m[2], amount = Math.abs(parseInt(m[3], 10));
4541
+ const ownerIdx = STATE.players.findIndex(p => p.name === ownerName);
4542
+ const ownerPossessive = ownerName + (ownerName.endsWith('s') ? "'" : "'s");
4543
+ const isRoute = sp?.kind === 'route';
4544
+ const isInst = sp?.kind === 'institution';
4545
+ const kindWord = isRoute ? 'route toll' : (isInst ? 'institution payment' : 'rent');
4546
+ text = 'You landed on <em>' + ownerPossessive + ' ' + space + '</em>. Paid <strong>' + amount + ' TN</strong> ' + kindWord + '.';
4547
+ cls = 'is-paid';
4548
+ }
4549
+ } else if (ownRow) {
4550
+ /* Landed on own property */
4551
+ const owned = STATE.players[0].ownedAssets.find(a => a.spaceNum === pos);
4552
+ const isRoute = sp?.kind === 'route';
4553
+ const isInst = sp?.kind === 'institution';
4554
+ const hasFullSet = asset && asset.sys && ownsFullSet(STATE, 0, asset.sys);
4555
+ const canUpgrade = hasFullSet && owned && owned.tier < 3 && !isRoute && !isInst;
4556
+ let body = 'You landed on <em>your ' + where + '</em>. <strong>No rent due.</strong>';
4557
+ if (canUpgrade) body += ' Upgrade available because you own the full set.';
4558
+ text = body; cls = 'is-own';
4559
+ } else if (noRentRow) {
4560
+ text = 'You landed on <em>' + where + '</em>. <strong>No rent due</strong> (payments suspended or unowned).';
4561
+ cls = 'is-neutral';
4562
+ } else if (taxRow) {
4563
+ const m = taxRow.detail.match(/([+-]?\d+) TN/);
4564
+ const amount = m ? Math.abs(parseInt(m[1], 10)) : '';
4565
+ text = 'You landed on <em>' + where + '</em>. Paid <strong>' + amount + ' TN</strong> in tax.';
4566
+ cls = 'is-paid';
4567
+ } else if (crisisRow) {
4568
+ text = 'You landed on <em>Constitutional Crisis</em>. You will sit out until you pay, roll doubles, or skip.';
4569
+ cls = 'is-paid';
4570
+ } else if (sendRow) {
4571
+ text = 'You were sent to <em>Constitutional Crisis</em>.';
4572
+ cls = 'is-paid';
4573
+ } else if (buyRow) {
4574
+ text = 'You bought <em>' + where + '</em>.';
4575
+ cls = 'is-own';
4576
+ } else if (sp?.kind && sp.kind.startsWith('corner') && sp.kind !== 'corner-crisis' && sp.kind !== 'corner-send') {
4577
+ text = 'You landed on <em>' + where + '</em>.';
4578
+ cls = 'is-neutral';
4579
+ } else if (sp?.kind === 'card-shock' || sp?.kind === 'card-debate') {
4580
+ text = 'You landed on <em>' + where + '</em>. Draw an event card.';
4581
+ cls = 'is-neutral';
4582
+ } else if (safeRow) {
4583
+ text = 'You landed on <em>' + where + '</em>. No action.';
4584
+ cls = 'is-neutral';
4585
+ }
4586
+
4587
+ if (text) {
4588
+ LAST_LANDING_OUTCOME = { text, cls };
4589
+ }
4590
+ __LAST_LANDING_LEDGER_LEN = ledger.length;
4591
+ }
4592
+
4593
+ /* Clear the band when you start your next roll */
4594
+ const __LAND_origRoll = (typeof rollWithAnimation === 'function') ? rollWithAnimation : null;
4595
+ if (__LAND_origRoll) {
4596
+ rollWithAnimation = function() {
4597
+ LAST_LANDING_OUTCOME = null;
4598
+ return __LAND_origRoll.apply(this, arguments);
4599
+ };
4600
+ }
4601
+
4602
+ /* Hook into render after toast scanner */
4603
+ const __LAND_origRender = render;
4604
+ render = function() {
4605
+ __LAND_origRender();
4606
+ classifyLandingOutcome();
4607
+ };
4608
+ /* Reset on new game */
4609
+ const __LAND_origStart = startNewGame;
4610
+ startNewGame = function(seed) {
4611
+ LAST_LANDING_OUTCOME = null;
4612
+ __LAST_LANDING_LEDGER_LEN = 0;
4613
+ __LAND_origStart(seed);
4614
+ };
4615
+
4616
+ applySidebarState();
4617
+ document.getElementById('sidebarToggle')?.addEventListener('click', toggleSidebar);
4618
+
4619
+ /* =====================================================================
4620
+ EVENT TOAST — watches the ledger for key events and pops a toast.
4621
+ Display-only; no reducer effect. The toast auto-dismisses after ~3.7s.
4622
+ ===================================================================== */
4623
+ const TOAST_SEEN_KEY = '__sovereign_toast_last_idx';
4624
+ let __TOAST_LAST_LEDGER_LEN = 0;
4625
+ let __LATE_REPUBLIC_SHOWN = false;
4626
+ const __TOAST_ACT_TITLES = {
4627
+ 1: 'Funding Act', 2: 'Assumption Act', 3: 'Bank Charter',
4628
+ 4: 'Manufactures Subsidy', 5: 'Coinage Act', 6: 'Tariff Act',
4629
+ 7: 'Excise Enforcement', 8: 'Internal Improvements',
4630
+ };
4631
+
4632
+ /* Profile-flavored opponent narration — three voices for three roles */
4633
+ const __PROFILE_FLAVOR = {
4634
+ 'treasury-finance': {
4635
+ role: 'Treasury / Finance',
4636
+ rollVerb: 'considers the chamber',
4637
+ moveTo: (where) => 'arrives at ' + where + ' with a Treasurer\u2019s air',
4638
+ buy: (where, cost) => 'takes ' + where + ' for ' + cost + ' TN \u2014 another rung on the federal-credit ladder',
4639
+ decline: (where) => 'lets ' + where + ' pass to auction \u2014 not worth the Treasury\u2019s purse',
4640
+ vote: (verdict) => 'casts ' + verdict + ' on the floor of Congress',
4641
+ upgrade: (where) => 'raises ' + where + ' a tier \u2014 the bond market notices',
4642
+ crisis: 'is summoned to the Crisis chamber',
4643
+ bid: (amount) => 'bids ' + amount + ' TN at the auction block',
4644
+ },
4645
+ 'merchant-infrastructure': {
4646
+ role: 'Merchant / Infrastructure',
4647
+ rollVerb: 'sets out from the counting-house',
4648
+ moveTo: (where) => 'reaches ' + where + ' by the post road',
4649
+ buy: (where, cost) => 'acquires ' + where + ' for ' + cost + ' TN \u2014 commerce favours the prepared',
4650
+ decline: (where) => 'passes on ' + where + ' \u2014 the trade does not warrant the outlay',
4651
+ vote: (verdict) => 'votes ' + verdict + ' \u2014 the merchant interest weighs in',
4652
+ upgrade: (where) => 'improves ' + where + ' \u2014 the wharf is busier this lap',
4653
+ crisis: 'is sent to the Crisis bench',
4654
+ bid: (amount) => 'bids ' + amount + ' TN \u2014 a merchant\u2019s number, considered',
4655
+ },
4656
+ 'manufacturer-industry': {
4657
+ role: 'Manufacturer / Industry',
4658
+ rollVerb: 'starts the day at the foundry',
4659
+ moveTo: (where) => 'walks to ' + where + ' with workshop\u2019s pace',
4660
+ buy: (where, cost) => 'buys ' + where + ' for ' + cost + ' TN \u2014 industry consolidates',
4661
+ decline: (where) => 'leaves ' + where + ' to others \u2014 the works call elsewhere',
4662
+ vote: (verdict) => 'casts ' + verdict + ' for the manufactures',
4663
+ upgrade: (where) => 'expands ' + where + ' \u2014 capacity rises with the smoke',
4664
+ crisis: 'is forced into Crisis by the politics of the day',
4665
+ bid: (amount) => 'tables ' + amount + ' TN \u2014 industry has cash',
4666
+ },
4667
+ };
4668
+ function flavorFor(playerName) {
4669
+ if (!STATE || !STATE.players) return null;
4670
+ const p = STATE.players.find(pl => pl.name === playerName);
4671
+ if (!p) return null;
4672
+ return __PROFILE_FLAVOR[p.profile] || null;
4673
+ }
4674
+ function pushToast({ cls, eyebrow, title, body }) {
4675
+ const stack = document.getElementById('toastStack');
4676
+ if (!stack) return;
4677
+ const t = document.createElement('div');
4678
+ t.className = 'event-toast' + (cls ? ' ' + cls : '');
4679
+ t.innerHTML = '<div class="toast-eyebrow">' + eyebrow + '</div>' +
4680
+ '<div class="toast-title">' + title + '</div>' +
4681
+ (body ? '<div class="toast-body">' + body + '</div>' : '');
4682
+ stack.appendChild(t);
4683
+ setTimeout(() => t.remove(), 3800);
4684
+ }
4685
+ function scanLedgerForToasts() {
4686
+ if (!STATE || !STATE.ledger) return;
4687
+ const ledger = STATE.ledger;
4688
+ const start = __TOAST_LAST_LEDGER_LEN;
4689
+ for (let i = start; i < ledger.length; i++) {
4690
+ const row = ledger[i];
4691
+ if (!row) continue;
4692
+ const isOpponentActor = row.actor && row.actor !== 'System' && row.actor !== 'Card' && row.actor !== 'Track' && STATE.players.findIndex(p => p.name === row.actor && p.profile !== 'human') >= 0;
4693
+
4694
+ if (row.event === 'PASS' && /Act passes/i.test(row.detail || '')) {
4695
+ const actName = (row.detail || '').split(' passes')[0];
4696
+ pushToast({ cls:'pass', eyebrow:'Congress · Act passed', title: actName, body: 'Congress carries the measure. Its effects take hold immediately.' });
4697
+ } else if (row.event === 'PASS' && /Act fails/i.test(row.detail || '')) {
4698
+ const actName = (row.detail || '').split(' fails')[0];
4699
+ pushToast({ cls:'fail', eyebrow:'Congress · Act failed', title: actName, body: 'The floor vote did not carry. The measure is set aside.' });
4700
+ } else if (row.event === 'CREDIT_CRISIS') {
4701
+ pushToast({ cls:'crisis', eyebrow:'Public Credit', title:'Credit Crisis', body: row.detail || 'Public Credit collapses to 4 or below. Financial panic spreads.' });
4702
+ } else if (row.event === 'DEFAULT') {
4703
+ pushToast({ cls:'default', eyebrow:'Catastrophe', title:'Default', body:'Public Credit reaches zero. Every player loses 50 % cash and one upgrade; Credit resets to 3.' });
4704
+ } else if (row.event === 'REBELLION') {
4705
+ pushToast({ cls:'rebellion', eyebrow:'Catastrophe', title:'Rebellion', body:'Public Resistance reaches twelve. Revenue upgrades destroyed; Resistance resets to 6.' });
4706
+ } else if (row.event === 'AUCTION WIN' || (row.event === 'AUCTION' && /wins/i.test(row.detail || ''))) {
4707
+ pushToast({ eyebrow:'Auction settled', title: row.detail || 'Auction won', body:'' });
4708
+ } else if (row.event === 'MANDATE') {
4709
+ pushToast({ cls:'pass', eyebrow:'The mandate is claimed', title: row.detail, body:'The Republic moves to Final Accounting.' });
4710
+ } else if (row.event === 'GAME OVER') {
4711
+ const hadMandate = STATE.mandateTrigger;
4712
+ if (hadMandate) {
4713
+ pushToast({ eyebrow:'Final Accounting', title: hadMandate.playerName + ' claims the mandate', body: hadMandate.influence + ' Influence at round ' + hadMandate.round + ', lead ' + hadMandate.lead + ' over second place.' });
4714
+ } else {
4715
+ pushToast({ eyebrow:'Twelve rounds complete', title:'No founder secured a mandate', body:'The Republic decides by final accounting. View the report.' });
4716
+ }
4717
+ }
4718
+ /* Opponent narration — verbose play-by-play with profile-flavored voice */
4719
+ else if (isOpponentActor && row.event === 'ROLL') {
4720
+ const f = flavorFor(row.actor);
4721
+ pushToast({ eyebrow: row.actor + ' \u00b7 ' + (f ? f.rollVerb : 'rolls'), title: row.detail, body:'' });
4722
+ } else if (isOpponentActor && row.event === 'MOVE') {
4723
+ const m = (row.detail || '').match(/\(([^)]+)\)\s*$/);
4724
+ const where = m ? m[1] : (row.detail || '').split(' ').slice(-1)[0];
4725
+ const f = flavorFor(row.actor);
4726
+ pushToast({ eyebrow: row.actor + ' \u00b7 moves', title: f ? f.moveTo(where) : ('Lands on ' + where), body:'' });
4727
+ } else if (isOpponentActor && row.event === 'BUY') {
4728
+ const f = flavorFor(row.actor);
4729
+ const m = (row.detail || '').match(/^(.*?)\s+for\s+(\d+)/);
4730
+ const where = m ? m[1] : (row.detail || '').split(' ')[0];
4731
+ const cost = m ? m[2] : '';
4732
+ pushToast({ cls:'pass', eyebrow: row.actor + ' \u00b7 buys', title: f ? f.buy(where, cost) : row.detail, body:'' });
4733
+ } else if (isOpponentActor && row.event === 'DECLINE') {
4734
+ const f = flavorFor(row.actor);
4735
+ const m = (row.detail || '').match(/^Declines\s+(.*?)$/);
4736
+ const where = m ? m[1] : (row.detail || '').split(' ').slice(-1)[0];
4737
+ pushToast({ eyebrow: row.actor + ' \u00b7 declines', title: f ? f.decline(where) : row.detail, body: 'It goes to forced auction.' });
4738
+ } else if (isOpponentActor && row.event === 'BID') {
4739
+ const f = flavorFor(row.actor);
4740
+ const m = (row.detail || '').match(/\b(\d+)\s*TN\b/);
4741
+ const amt = m ? m[1] : '';
4742
+ pushToast({ eyebrow: row.actor + ' \u00b7 bids', title: f && amt ? f.bid(amt) : row.detail, body:'' });
4743
+ } else if (isOpponentActor && row.event === 'VOTE') {
4744
+ const f = flavorFor(row.actor);
4745
+ const m = (row.detail || '').match(/Vote\s+(YES|NO)/i);
4746
+ const verdict = m ? m[1].toLowerCase() : 'their ballot';
4747
+ pushToast({ eyebrow: row.actor + ' \u00b7 votes', title: f ? f.vote(verdict) : row.detail, body:'' });
4748
+ } else if (isOpponentActor && row.event === 'UPGRADE') {
4749
+ const f = flavorFor(row.actor);
4750
+ const m = (row.detail || '').match(/^Upgrade\s+(.+?)(\s+to|\s+\(|$)/);
4751
+ const where = m ? m[1] : '';
4752
+ pushToast({ cls:'pass', eyebrow: row.actor + ' \u00b7 upgrades', title: f && where ? f.upgrade(where) : row.detail, body:'' });
4753
+ } else if (isOpponentActor && row.event === 'CRISIS') {
4754
+ const f = flavorFor(row.actor);
4755
+ pushToast({ cls:'fail', eyebrow: row.actor + ' \u00b7 \u2192 Crisis', title: f ? f.crisis : row.detail, body:'' });
4756
+ }
4757
+ /* Lap-end recap: trigger when a new lap begins (lap > 1) */
4758
+ if (row.event === 'LAP' && /Begin lap (\d+)/.test(row.detail || '')) {
4759
+ const m = row.detail.match(/Begin lap (\d+)/);
4760
+ const lap = parseInt(m[1], 10);
4761
+ if (lap > 1) setTimeout(() => showLapRecap(lap - 1), 600);
4762
+ /* Late Republic one-shot toast at the start of round LATE_REPUBLIC_START */
4763
+ if (lap === LATE_REPUBLIC_START && !__LATE_REPUBLIC_SHOWN) {
4764
+ __LATE_REPUBLIC_SHOWN = true;
4765
+ setTimeout(() => pushToast({
4766
+ cls:'pass', eyebrow:'Late Republic',
4767
+ title:'The Founding Acts are decided.',
4768
+ body:'The Republic now plays out its consequences. Rounds 8\u201312 are the late stretch \u2014 no new Acts will be tabled.',
4769
+ }), 1400);
4770
+ }
4771
+ }
4772
+ }
4773
+ __TOAST_LAST_LEDGER_LEN = ledger.length;
4774
+ }
4775
+ /* Hook toast scan into the render loop */
4776
+ const __TOAST_origRender = render;
4777
+ render = function() {
4778
+ __TOAST_origRender();
4779
+ scanLedgerForToasts();
4780
+ };
4781
+ /* Reset on new game */
4782
+ const __TOAST_origStart = startNewGame;
4783
+ startNewGame = function(seed) {
4784
+ __TOAST_LAST_LEDGER_LEN = 0;
4785
+ __LATE_REPUBLIC_SHOWN = false;
4786
+ __TOAST_origStart(seed);
4787
+ };
4788
+
4789
+ /* =====================================================================
4790
+ LAP RECAP — Federalist-voice paragraph at the close of each lap
4791
+ ===================================================================== */
4792
+ function buildLapRecap(lap) {
4793
+ /* Walk the ledger for events with row.lap === lap */
4794
+ const rows = STATE.ledger.filter(r => r.lap === lap);
4795
+ const buys = rows.filter(r => r.event === 'BUY');
4796
+ const passed = rows.filter(r => r.event === 'PASS' && /passes/i.test(r.detail || ''));
4797
+ const failed = rows.filter(r => r.event === 'PASS' && /fails/i.test(r.detail || ''));
4798
+ const creditMoves = rows.filter(r => r.event === 'CREDIT');
4799
+ const crisisFired = rows.some(r => r.event === 'CREDIT_CRISIS');
4800
+ const defaultFired = rows.some(r => r.event === 'DEFAULT');
4801
+ const rebellionFired = rows.some(r => r.event === 'REBELLION');
4802
+ const c = STATE.tracks.credit.value;
4803
+ const r = STATE.tracks.resistance.value;
4804
+ const ic = STATE.tracks.capacity.value;
4805
+ const youCash = STATE.players[0].cash;
4806
+ const sentences = [];
4807
+ sentences.push('Round ' + lap + ' closes.');
4808
+ /* Mandate status from round MANDATE.minRound onward */
4809
+ if (lap >= MANDATE.minRound) {
4810
+ const scored = STATE.players.map((p, idx) => ({ idx, name: p.name, total: scorePlayer(STATE, idx).total }));
4811
+ const sorted = scored.slice().sort((a, b) => b.total - a.total);
4812
+ const top = sorted[0], second = sorted[1];
4813
+ if (top.total === second.total) {
4814
+ sentences.push(top.name + ' and ' + second.name + ' are tied at ' + top.total + ' Influence — no mandate yet.');
4815
+ } else if (top.total >= MANDATE.threshold && (top.total - second.total) >= MANDATE.lead) {
4816
+ sentences.push(top.name + ' has ' + top.total + ' Influence and leads by ' + (top.total - second.total) + '. Final Accounting begins.');
4817
+ } else {
4818
+ const lead = top.total - second.total;
4819
+ const need = [];
4820
+ if (top.total < MANDATE.threshold) need.push((MANDATE.threshold - top.total) + ' more Influence');
4821
+ if (lead < MANDATE.lead) need.push((MANDATE.lead - lead) + ' more lead');
4822
+ sentences.push(top.name + ' leads with ' + top.total + ' Influence, but ' + need.join(' and ') + ' — no mandate yet.');
4823
+ }
4824
+ }
4825
+ if (passed.length > 0) {
4826
+ sentences.push('Congress carried ' + passed.map(x => (x.detail||'').split(' passes')[0]).join(' and ') + '.');
4827
+ }
4828
+ if (failed.length > 0) {
4829
+ sentences.push('The floor turned aside ' + failed.map(x => (x.detail||'').split(' fails')[0]).join(' and ') + '.');
4830
+ }
4831
+ if (buys.length === 1) sentences.push('A single property changed hands: ' + (buys[0].detail || '').split(' for ')[0] + '.');
4832
+ else if (buys.length > 1) sentences.push(buys.length + ' properties changed hands this lap.');
4833
+ if (crisisFired) sentences.push('Credit Crisis fired — financial panic stirred Public Resistance.');
4834
+ if (defaultFired) sentences.push('The Republic defaulted on its bonds — every player lost half their cash.');
4835
+ if (rebellionFired) sentences.push('Rebellion broke out — Revenue upgrades were destroyed.');
4836
+ if (c >= 9) sentences.push('Public Credit firms at ' + c + ' — the bond market trusts the Treasury.');
4837
+ else if (c <= 4) sentences.push('Public Credit slips to ' + c + ' — the warning band is in sight.');
4838
+ else sentences.push('Public Credit holds at ' + c + '.');
4839
+ if (r >= 9) sentences.push('Resistance has climbed to ' + r + ' — the western counties stir.');
4840
+ if (ic >= 6) sentences.push('Industrial Capacity rises to ' + ic + ' — the workshops favour the manufactures.');
4841
+ return sentences.join(' ');
4842
+ }
4843
+
4844
+ function showLapRecap(lapEnded) {
4845
+ if (!STATE || STATE.status === 'gameOver') return;
4846
+ const overlay = document.getElementById('lapRecapOverlay');
4847
+ if (!overlay) return;
4848
+ const title = 'Round ' + lapEnded + ' of ' + TOTAL_ROUNDS;
4849
+ const sub = 'A federalist accounting';
4850
+ const body = buildLapRecap(lapEnded);
4851
+ document.getElementById('lapRecapTitle').textContent = title;
4852
+ document.getElementById('lapRecapSub').textContent = sub;
4853
+ document.getElementById('lapRecapBody').textContent = body;
4854
+ const m = document.getElementById('lapRecapMeta');
4855
+ m.innerHTML = '<div class="m"><span class="lbl">Credit</span><span class="v">' + STATE.tracks.credit.value + '</span></div>' +
4856
+ '<div class="m"><span class="lbl">Resist</span><span class="v">' + STATE.tracks.resistance.value + '</span></div>' +
4857
+ '<div class="m"><span class="lbl">Capacity</span><span class="v">' + STATE.tracks.capacity.value + '</span></div>' +
4858
+ '<div class="m"><span class="lbl">Your cash</span><span class="v">$' + STATE.players[0].cash.toLocaleString('en-US') + '</span></div>';
4859
+ overlay.classList.remove('hidden');
4860
+ }
4861
+ function hideLapRecap() {
4862
+ document.getElementById('lapRecapOverlay')?.classList.add('hidden');
4863
+ }
4864
+ document.getElementById('lapRecapDismiss')?.addEventListener('click', hideLapRecap);
4865
+ document.getElementById('lapRecapOverlay')?.addEventListener('click', (e) => { if (e.target.id === 'lapRecapOverlay') hideLapRecap(); });
4866
+ document.addEventListener('keydown', (e) => {
4867
+ if (e.key === 'Escape' && !document.getElementById('lapRecapOverlay')?.classList.contains('hidden')) hideLapRecap();
4868
+ if (e.key === 'Enter' && !document.getElementById('lapRecapOverlay')?.classList.contains('hidden')) hideLapRecap();
4869
+ });
2396
4870
  document.addEventListener('keydown', (e) => {
2397
4871
  if (e.key === ' ' && STATE && STATE.phase === 'awaiting-roll' && STATE.activePlayerIndex === 0) { e.preventDefault(); dispatch({ type:'ROLL_DICE' }); }
2398
4872
  });
@@ -2549,7 +5023,7 @@ function buildRepublicSummary(state) {
2549
5023
 
2550
5024
  const paras = [];
2551
5025
  /* Para 1 — verdict + headline numbers */
2552
- let p1 = 'After lap 7 the republic’s books close. ';
5026
+ let p1 = 'After twelve rounds the republic’s books close. ';
2553
5027
  if (won) {
2554
5028
  p1 += 'You hold the largest Influence position, with ' + score.total + ' points against ' + state.finalScores[1].total + ' for Hamilton and ' + state.finalScores[2].total + ' for Morris. ';
2555
5029
  } else {
@@ -2603,6 +5077,19 @@ function buildRepublicSummary(state) {
2603
5077
  if (r >= 8) p4 += 'Resistance at or above eight means the Whiskey Rebellion was live the whole back half. ';
2604
5078
  if (ic >= 8) p4 += 'Capacity at or above eight pays the industrial endgame bonus: two Influence for Manufactures, two more for Strategic Industry. ';
2605
5079
  else if (ic >= 6) p4 += 'Capacity at six or above pays the twenty-five-percent industrial rent bonus, but did not reach the endgame Influence threshold. ';
5080
+ /* v0.18 polished — failure-tier narrative: did Credit Crisis fire? Did Default? Rebellion? */
5081
+ const ledger = state.ledger;
5082
+ const defaultFired = ledger.some(row => row.event === 'DEFAULT');
5083
+ const rebellionFired = ledger.some(row => row.event === 'REBELLION');
5084
+ const crisisFired = !!state.flags.creditCrisisFired || ledger.some(row => row.event === 'CREDIT_CRISIS');
5085
+ if (defaultFired) {
5086
+ p4 += 'The republic suffered a Default this game — Public Credit reached zero and every player lost half their cash and an upgrade. The Treasury reset Credit to three and the game continued. ';
5087
+ } else if (crisisFired) {
5088
+ p4 += 'The republic crossed into Credit Crisis — Public Credit collapsed to four or below at least once, ticking Public Resistance up by one. The warning was absorbed without a Default. ';
5089
+ } else {
5090
+ p4 += 'Neither Credit Crisis nor Default fired this game: Public Credit held above five throughout. ';
5091
+ }
5092
+ if (rebellionFired) p4 += 'Public Resistance reached twelve and Rebellion fired — Revenue upgrades were destroyed and Resistance reset to six. ';
2606
5093
  paras.push(p4);
2607
5094
 
2608
5095
  /* Para 5 — verdict + history */
@@ -2653,7 +5140,15 @@ function showResumePill() {
2653
5140
  function hideResumePill() { document.getElementById('resumePill').classList.add('hidden'); }
2654
5141
 
2655
5142
  function loadFromPayload(payload) {
2656
- if (!payload || payload.version !== SAVE_VERSION) {
5143
+ if (payload && payload.version === 'v0.18-candidate') {
5144
+ showIoPill('This save is from an earlier game length and cannot be loaded. Start a new game to play the 12-round model.', true);
5145
+ return false;
5146
+ }
5147
+ if (payload && payload.version === 'v0.19-pacing-candidate') {
5148
+ showIoPill('This save is from before the mandate model and cannot be loaded. Start a new game to play the mandate victory candidate.', true);
5149
+ return false;
5150
+ }
5151
+ if (!payload || (payload.version !== SAVE_VERSION && payload.version !== 'v0.10' && payload.version !== 'v0.11-candidate' && payload.version !== 'v0.12-candidate' && payload.version !== 'v0.13-candidate' && payload.version !== 'v0.14-candidate' && payload.version !== 'v0.15-candidate' && payload.version !== 'v0.16-candidate' && payload.version !== 'v0.17-candidate')) {
2657
5152
  if (payload && (payload.version === 'phase5' || payload.version === 'v0.3' || payload.version === 'v0.4' || payload.version === 'v0.5' || payload.version === 'v0.6' || payload.version === 'v0.8')) {
2658
5153
  showIoPill('Save file from pre-v0.10 ruleset; cannot replay under current balance. Export to keep as historical artifact.', true);
2659
5154
  } else {
@@ -2661,6 +5156,23 @@ function loadFromPayload(payload) {
2661
5156
  }
2662
5157
  return false;
2663
5158
  }
5159
+ if (payload.version === 'v0.10') {
5160
+ showIoPill('Loaded v0.10 save under v0.18 candidate — Credit Crisis event added at Credit ≤ 4. Multiple pressure layers active. Behavior differs.');
5161
+ } else if (payload.version === 'v0.11-candidate') {
5162
+ showIoPill('Loaded v0.11 save under v0.18 candidate — Credit Crisis event added at Credit ≤ 4. Bank Run v0.11 preserved. Behavior differs.');
5163
+ } else if (payload.version === 'v0.12-candidate') {
5164
+ showIoPill('Loaded v0.12 save under v0.18 candidate — Credit Crisis event added at Credit ≤ 4. Charter scaling not in this branch. Behavior differs.');
5165
+ } else if (payload.version === 'v0.13-candidate') {
5166
+ showIoPill('Loaded v0.13 save under v0.18 candidate — Credit Crisis event added at Credit ≤ 4. Spec Fever + Anti-Fed pressure layers active. Behavior differs.');
5167
+ } else if (payload.version === 'v0.14-candidate') {
5168
+ showIoPill('Loaded v0.14 save under v0.18 candidate — Credit Restored gate NOT in this branch. Credit Crisis event added at Credit ≤ 4.');
5169
+ } else if (payload.version === 'v0.15-candidate') {
5170
+ showIoPill('Loaded v0.15 save under v0.18 candidate — Recovery gates NOT in this branch. Credit Crisis event added at Credit ≤ 4.');
5171
+ } else if (payload.version === 'v0.16-candidate') {
5172
+ showIoPill('Loaded v0.16 save under v0.18 candidate — Spec Fever conditional v0.17 active. Credit Crisis event added at Credit ≤ 4.');
5173
+ } else if (payload.version === 'v0.17-candidate') {
5174
+ showIoPill('Loaded v0.17 save under v0.18 candidate — Credit Crisis event added: triggers once per game when Credit first reaches ≤ 4 (and > 0). Penalty: +1 Resistance to all. Default at Credit 0 unchanged.');
5175
+ }
2664
5176
  if (typeof payload.seed !== 'number') { showIoPill('Load failed · invalid seed', true); return false; }
2665
5177
  if (!Array.isArray(payload.decisionLog)) { showIoPill('Load failed · invalid decisionLog', true); return false; }
2666
5178
  /* Reconstruct */
@@ -2902,7 +5414,7 @@ function renderReplay() {
2902
5414
  tk.style.left = (i / total * 100) + '%';
2903
5415
  tk.style.width = (100 / total) + '%';
2904
5416
  tk.dataset.idx = i;
2905
- tk.title = 'Lap ' + f.lap + ' · Turn ' + f.turn;
5417
+ tk.title = 'Round ' + f.lap + ' · Turn ' + f.turn;
2906
5418
  tk.addEventListener('click', (e) => { e.stopPropagation(); REPLAY.index = i; if (REPLAY.timer) { clearInterval(REPLAY.timer); REPLAY.timer = null; REPLAY.playing = false; updateReplayPlayBtn(); } renderReplay(); });
2907
5419
  ticksEl.appendChild(tk);
2908
5420
  }
@@ -2974,6 +5486,9 @@ const _origStart = startNewGame;
2974
5486
  startNewGame = function(seed) {
2975
5487
  resetNarrationDetector();
2976
5488
  _origStart(seed);
5489
+ applyPlayerCustom();
5490
+ renderBoard();
5491
+ render();
2977
5492
  };
2978
5493
 
2979
5494
  /* =====================================================================
@@ -3510,6 +6025,7 @@ function runBatchGame(seed, profileTriplet, charterEnabled) {
3510
6025
  while (s.status !== 'gameOver' && safety-- > 0) {
3511
6026
  if (s.pendingDefault) { step({ type:'TRIGGER_DEFAULT' }); continue; }
3512
6027
  if (s.pendingRebellion) { step({ type:'TRIGGER_REBELLION' }); continue; }
6028
+ if (s.pendingCreditCrisis) { step({ type:'TRIGGER_CREDIT_CRISIS' }); continue; }
3513
6029
  if (s.phase === 'act-vote' && s.acts.current) {
3514
6030
  let advanced = false;
3515
6031
  for (let i = 0; i < s.players.length; i++) {
@@ -3877,7 +6393,29 @@ reduce = function(s, action) {
3877
6393
 
3878
6394
  /* ---- Initial boot: start the game, then offer autosave resume ---- */
3879
6395
  startNewGame(2026);
6396
+ applyPlayerCustom();
6397
+ render();
3880
6398
  showResumePill();
6399
+
6400
+ /* First-load welcome flow — auto-show three-step welcome when no autosave and not seen */
6401
+ (function maybeShowFirstRunOrient() {
6402
+ let seen = false;
6403
+ try { seen = !!localStorage.getItem(ORIENT_KEY); } catch (e) {}
6404
+ if (!seen && !hasAutosave()) showOrient(1);
6405
+ })();
6406
+
6407
+ /* Ledger expand/collapse toggle — add an in-head button */
6408
+ (function wireLedgerToggle() {
6409
+ const head = document.querySelector('.ledger .head');
6410
+ if (!head || head.querySelector('.ledger-toggle')) return;
6411
+ const btn = document.createElement('button');
6412
+ btn.className = 'ledger-toggle';
6413
+ btn.type = 'button';
6414
+ btn.textContent = 'Ledger';
6415
+ btn.title = 'Expand or collapse the ledger';
6416
+ btn.addEventListener('click', () => document.body.classList.toggle('ledger-expanded'));
6417
+ head.appendChild(btn);
6418
+ })();
3881
6419
  </script>
3882
6420
 
3883
6421
  </body>