@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.
- package/CHANGELOG.md +43 -0
- package/README.es.md +14 -9
- package/README.fr.md +15 -10
- package/README.hi.md +14 -9
- package/README.it.md +15 -10
- package/README.ja.md +15 -10
- package/README.md +14 -9
- package/README.pt-BR.md +15 -10
- package/README.zh.md +14 -9
- package/package.json +2 -2
- package/release/CHANGELOG.md +43 -0
- package/release/digital-mode/sovereign-solo.html +2615 -77
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<title>Sovereign
|
|
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">
|
|
422
|
-
<div class="title">
|
|
423
|
-
<div class="sub">
|
|
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="
|
|
430
|
-
<button id="
|
|
431
|
-
<button id="
|
|
432
|
-
<button id="
|
|
433
|
-
<button id="
|
|
434
|
-
<button id="
|
|
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">
|
|
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.
|
|
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
|
|
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.
|
|
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),
|
|
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(),
|
|
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:'
|
|
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 >
|
|
1762
|
-
s = logRow(s, { actor:'System', event:'GAME OVER', detail:'
|
|
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">
|
|
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.
|
|
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
|
-
|
|
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
|
-
/*
|
|
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) {
|
|
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
|
|
2016
|
-
const
|
|
2017
|
-
if (
|
|
2018
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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
|
-
},
|
|
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
|
|
2351
|
-
<div class="sub"
|
|
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
|
|
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 (
|
|
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 = '
|
|
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>
|