THE ORDER WAS GIVEN · v1.3 · COMPLETE HTML FOR AI REVIEW

This TXT contains the entire executable review HTML below, including body, all game text, all conditional mechanics, session/evidence validation, UI handlers, saving/restoring and report exports. Nothing in those sections is omitted. The original file contains large embedded art and font payloads before its body; an HTML text reader may stop there. In this review copy, only the binary font/art are replaced by neutral styling/placeholders. Game content and executable logic after the media declarations are byte-identical to the original player edition.

This is the complete one-file HTML, not a shell template. No external script or code file is required. To run it, use the companion AI_Review.html. Assess the original player edition separately for its full artwork.

===== BEGIN COMPLETE HTML =====
<!doctype html><html lang="en">
<!-- AI REVIEW EDITION: Complete v1.3 game content, body, CSS and JavaScript. Embedded artwork/font replaced with a small neutral placeholder solely to make AI ingestion practical. Player edition retains original artwork. -->
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#173d3b"><meta name="description" content="A political negotiation game. Three rounds of promises, independent choices, and unequal public access in fictional Lydora."><title>The Order Was Given</title><style>*{box-sizing:border-box}html{background:#ece0c8;color:#202923;font-family:Arial,Helvetica,sans-serif;font-size:17px}body{margin:0}button,input,select,textarea{font:inherit}button,a,input,select,textarea{outline-offset:4px}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid #a53a2e}button{cursor:pointer;border:1px solid #173d3b;background:#f9f3e6;color:#173d3b;padding:.7rem 1rem;min-height:46px;font-weight:700;border-radius:2px}button:hover{background:#d7e1d8}button:disabled{cursor:default;opacity:.55}a{color:#164e4a;text-underline-offset:3px}.primary{background:#a2392c;color:#fff5df;border-color:#a2392c}.primary:hover{background:#7f2a23}.quiet{background:transparent;border-color:#a09b89;font-size:.87rem;padding:.45rem .7rem}.danger{color:#a02e23}.tag,.eyebrow{font-weight:800;letter-spacing:.13em;text-transform:uppercase;font-size:.73rem}.eyebrow{color:#964032}.muted,.small{color:#596158;font-size:.87rem;line-height:1.5}h1,h2,h3,p{margin:0 0 1rem}h1,h2{font-family:Poster,'Arial Narrow',Impact,sans-serif;font-weight:700;line-height:.95;text-transform:uppercase;letter-spacing:.015em}h1{font-size:clamp(3.7rem,8vw,7.7rem)}h2{font-size:clamp(2.2rem,4vw,3.8rem)}h3{font-size:1.15rem;margin-top:1.25rem}p{line-height:1.55}label{font-weight:700;display:block}input[type=text],input[type=password],select,textarea{width:100%;border:1px solid #828c7c;border-radius:2px;padding:.7rem;background:#fffaf0;color:#202923}input[type=checkbox],input[type=radio]{accent-color:#a2392c;width:1.1rem;height:1.1rem;flex:none}textarea{resize:vertical;min-height:145px;line-height:1.55}textarea:read-only{background:#eee8da}fieldset{border:0;margin:0;padding:0}legend{font-weight:bold;margin-bottom:.6rem}.topbar{background:#173d3b;color:#f5ead3;display:flex;gap:1rem;align-items:center;justify-content:space-between;padding:.65rem max(1rem,calc((100vw - 1280px)/2));flex-wrap:wrap}.brand{font-family:Poster,'Arial Narrow',sans-serif;font-size:1.4rem;text-transform:uppercase;letter-spacing:.07em}.topbar button{color:#f5ead3;border-color:#9eaa94;background:transparent;min-height:38px;font-size:.8rem;padding:.4rem .6rem}.topbar button:hover{background:#35534b}.tools{display:flex;gap:.4rem;flex-wrap:wrap}.saved{font-size:.73rem;color:#d1d8c0}.shell{max-width:1280px;margin:auto;padding:1.6rem 1.5rem 3rem}.scene{width:100%;height:185px;object-fit:cover;object-position:center 54%;display:block;filter:none}.hero{position:relative;min-height:530px;background:#e9d8b7 url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22600%22%3E%3Crect%20width%3D%221200%22%20height%3D%22600%22%20fill%3D%22%23e4d7bb%22%2F%3E%3C%2Fsvg%3E') center/cover no-repeat;padding:4rem max(1.5rem,calc((100vw - 1200px)/2));display:flex;align-items:flex-start}.hero-copy{max-width:44%;padding:0}.hero-copy h1{font-size:clamp(3.5rem,6vw,6.4rem);margin-top:.75rem}.hero-copy p{max-width:360px;font-size:1.08rem}.landing{max-width:1100px;margin:0 auto;padding:2.3rem 1.5rem}.entry-grid{display:grid;grid-template-columns:1fr 1fr;gap:2rem}.mode-options{display:flex;gap:1rem;margin:1rem 0}.mode-options button{flex:1;text-align:left;font-family:Poster,sans-serif;font-size:1.7rem;text-transform:uppercase}.selected{box-shadow:inset 0 -5px #a2392c;background:#fdf6e4}.identity{display:grid;grid-template-columns:1fr 1fr;gap:.85rem;margin:1rem 0}.identity label:first-child{grid-column:1/-1}.credit{margin:2rem 0 0;font-size:.75rem;color:#60675d}.note{border-left:4px solid #a2392c;padding:.7rem 1rem;background:#f3ead8}.error{color:#8b251e;font-weight:700;background:#fff1e8;border-left:4px solid #a2392c;padding:.7rem 1rem;margin:1rem 0}.progress{display:flex;gap:.65rem;align-items:center;flex-wrap:wrap;margin-bottom:1.6rem;font-size:.8rem;color:#657260}.progress span{padding:.4rem 0;border-bottom:2px solid #a9b09c}.progress .current{border-color:#a2392c;color:#862e25;font-weight:bold}.heading-row{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;margin-bottom:1rem}.heading-row .brief{max-width:740px}.status-stamp{border:2px solid #284c40;transform:rotate(-3deg);padding:.8rem;text-transform:uppercase;font-family:Poster,sans-serif;font-size:1.2rem;min-width:150px;text-align:center}.directive{border-left:6px solid #a2392c;margin:1.4rem 0;padding:1.2rem 1.5rem;background:#dfcbb0}.directive .attribution{color:#922f25}.directive blockquote{margin:.5rem 0;font-weight:bold;font-size:1.12rem;line-height:1.55}.intro-grid{display:grid;grid-template-columns:1.2fr 1fr;gap:2rem}.rules{padding:1rem 0;border-top:1px solid #a2ac99;border-bottom:1px solid #a2ac99;margin:1rem 0}.rules ul{padding-left:1.3rem;line-height:1.6}.rules li{margin:.6rem 0}.workspace{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(340px,.9fr);gap:2rem;align-items:start}.people{display:grid;grid-template-columns:1fr 1fr;gap:.7rem;margin:1rem 0}.person{display:flex;align-items:center;gap:.8rem;text-align:left;padding:.35rem .6rem .35rem .35rem;min-height:95px}.person strong{display:block}.person .small{display:block;font-weight:400;font-size:.72rem}.portrait{width:68px;height:88px;flex:none;background-image:url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22600%22%3E%3Crect%20width%3D%221200%22%20height%3D%22600%22%20fill%3D%22%23e4d7bb%22%2F%3E%3C%2Fsvg%3E');background-repeat:no-repeat;background-size:600% auto;background-position:var(--pos) 0;background-color:#dac7a6}.portrait.large{width:100px;height:135px}.conversation{border-top:3px solid #173d3b;padding-top:1rem}.portrait-heading{display:flex;gap:1rem;align-items:center;margin-bottom:1rem}.quote{font-size:1.02rem;line-height:1.6;border-left:2px solid #779081;margin:1rem 0;padding-left:1rem}.priority{font-weight:700;color:#25483c}.questions details{border-top:1px solid #b0b7a4;padding:.65rem 0}.questions summary{cursor:pointer;font-size:.88rem;font-weight:bold;line-height:1.4}.questions details p{font-size:.9rem;margin:.8rem 0}.appeal{display:flex;align-items:flex-start;gap:.65rem;border-bottom:1px solid #babca9;padding:.8rem 0;cursor:pointer}.appeal input{margin-top:.2rem}.appeal .detail{display:block;font-weight:400;font-size:.85rem;line-height:1.5;margin-top:.3rem}.plan-shelf{border-top:5px solid #a2392c;background:#f7f0df;padding:1.25rem}.policy{display:flex;gap:.6rem;padding:.7rem 0;border-bottom:1px solid #c4c5af;line-height:1.4}.policy small{display:block;font-weight:400;color:#5e665b;font-size:.78rem;margin-top:.25rem}.request{margin:1rem 0}.field{margin:1rem 0}.field label,.field legend{margin-bottom:.4rem}.two-fields{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}.evidence{border-left:3px solid #597b68;padding:.7rem 1rem;margin:1rem 0;font-size:.86rem;background:#e2e6d7}.evidence p{margin:.5rem 0 0}.evidence-check{display:flex;gap:.6rem;align-items:flex-start;padding:.65rem 0;font-weight:400;font-size:.85rem;border-bottom:1px solid #c0c7b5}.count{font-size:.75rem;color:#626e5e;margin-top:.25rem}.count.ready{color:#25573e}.action-row{display:flex;gap:.75rem;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:1.5rem 0}.wide-action{width:100%}.snapshot{border-top:1px solid #8c9a88;padding:1.1rem 0;display:grid;grid-template-columns:80px 1fr 110px;gap:1rem;align-items:start}.choice{font-family:Poster,sans-serif;font-size:2.1rem;line-height:1;text-align:right;color:#21483d}.choice small{font-family:Arial,sans-serif;font-size:.68rem;display:block;line-height:1.4;text-transform:uppercase;letter-spacing:.04em;margin-top:.35rem}.snapshot p{font-size:.9rem;margin:.5rem 0}.reveal-top{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin:1.5rem 0}.forecast-result{border-left:4px solid #a2392c;padding:1rem;background:#e1dac6}.tally{border-collapse:collapse;width:100%;margin:1rem 0;font-size:.9rem}.tally th,.tally td{border-bottom:1px solid #b6bda9;text-align:left;padding:.65rem .5rem;vertical-align:top}.tally th{color:#52654f;font-size:.77rem;text-transform:uppercase;letter-spacing:.04em}.tally .total td{font-weight:bold;border-top:2px solid #173d3b}.opportunities{display:flex;gap:2rem;align-items:center;justify-content:space-around;border-top:3px solid #173d3b;border-bottom:1px solid #7b9075;padding:1rem;margin:1rem 0}.opportunities strong{font-family:Poster,sans-serif;font-size:3.6rem;display:block;color:#173d3b}.opportunities span{font-size:.8rem}.forum-grid{display:grid;grid-template-columns:1fr 1fr;gap:2rem}.forum-side{border-top:5px solid #173d3b;padding:1rem 0}.forum-side.youth{border-color:#a2392c}.forum-side .speech{border-left:1px solid #8a9a83;padding-left:1rem;font-size:1rem;line-height:1.65}.response{margin:1.2rem 0}.response label{margin-bottom:.5rem}.comparison{display:grid;grid-template-columns:1fr 1fr;gap:2rem}.comparison>div{border-top:3px solid #173d3b;padding-top:1rem}.readonly{white-space:pre-wrap;line-height:1.6;padding:1rem;background:#e3dfcf;border-left:3px solid #9c856c;font-size:.9rem}.backfill{border-top:1px solid #bcc3af;padding:1rem 0}.completion{border-top:4px solid #173d3b;padding:1.2rem 0}.completion.incomplete{border-color:#a2392c}.report{max-width:950px;margin:auto}.report h1{font-size:3rem}.report h2{font-size:1.8rem;margin-top:2rem}.report h3{margin-top:1rem}.report pre{white-space:pre-wrap;font-family:inherit;line-height:1.6}.report .answer{white-space:pre-wrap;margin:.7rem 0 1.3rem;padding:.7rem 1rem;border-left:2px solid #597b68}.report .original{color:#62685d;font-size:.87rem}.report section{border-top:1px solid #bac1ae;padding-top:1rem}.report ol{line-height:1.6}dialog{background:#f6efdf;color:#202923;border:2px solid #173d3b;border-radius:2px;width:min(700px,94vw);max-height:90dvh;padding:1.5rem}dialog::backdrop{background:#102a25b8}dialog h2{font-size:2.4rem}dialog .close-row{display:flex;justify-content:space-between;gap:1rem;align-items:start;margin-bottom:1rem}dialog .close-row button{min-width:46px}dialog .scroll{max-height:65dvh;overflow:auto;padding-right:.5rem}details.archive{margin:1rem 0;border-top:1px solid #95a48c;padding-top:.8rem}details.archive summary{cursor:pointer;font-weight:bold}.table-wrap{overflow:auto}.save-warning{background:#843427;color:#fff5e5;padding:.6rem 1rem;font-size:.82rem}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}#printReport{display:none}@media(min-width:1100px){.plan-shelf{position:sticky;top:1rem}}@media(max-width:850px){.workspace,.intro-grid,.entry-grid,.forum-grid{grid-template-columns:1fr}.hero{min-height:460px;background-position:60% center;padding-top:2rem}.hero-copy{max-width:47%}.hero-copy h1{font-size:3.6rem}.hero-copy p{font-size:.95rem}.identity{grid-template-columns:1fr}.snapshot{grid-template-columns:62px 1fr 80px}.snapshot .portrait{width:58px;height:80px}.reveal-top{grid-template-columns:1fr}.status-stamp{display:none}.scene{height:140px}.shell{padding:1.2rem 1rem 2rem}}@media(max-width:520px){html{font-size:16px}.topbar{gap:.5rem}.brand{font-size:1.15rem}.hero{min-height:340px;padding:1.6rem 1rem;background-position:55% center}.hero-copy{max-width:51%}.hero-copy h1{font-size:2.8rem}.hero-copy p{font-size:.83rem;line-height:1.4}.landing{padding:1.5rem 1rem}.people{gap:.4rem}.person{gap:.4rem;font-size:.85rem;padding:.3rem}.person .portrait{width:49px;height:76px}.person .small{font-size:.65rem}.two-fields,.comparison{grid-template-columns:1fr}.snapshot{grid-template-columns:1fr 80px}.snapshot .portrait{display:none}.opportunities{gap:1rem}.opportunities strong{font-size:3rem}.mode-options{gap:.6rem}.mode-options button{font-size:1.35rem;padding:.7rem}.tally{font-size:.78rem}.tally th,.tally td{padding:.55rem .3rem}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}@media print{html,body{background:white!important;color:black!important;font-size:10pt}body>*:not(#printReport){display:none!important}#printReport{display:block!important}.report{max-width:none}.report h1{font-family:Arial,sans-serif;font-size:24pt}.report h2{font-family:Arial,sans-serif;font-size:17pt;break-after:avoid}.report h3{break-after:avoid}.report section{break-inside:auto}.report .answer,.report .original{background:white;color:black}.tally{font-size:9pt}.tally tr{break-inside:avoid}.credit{color:black}a{color:black}button{display:none!important}@page{margin:.6in}}
@media(max-width:850px){.hero{display:block;background:none;min-height:0;padding:0}.hero::before{content:"";display:block;height:200px;background:url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22600%22%3E%3Crect%20width%3D%221200%22%20height%3D%22600%22%20fill%3D%22%23e4d7bb%22%2F%3E%3C%2Fsvg%3E') center 55%/cover no-repeat}.hero-copy{max-width:none;padding:1.5rem;background:#e9d8b7}.hero-copy h1{font-size:clamp(3.4rem,9vw,5rem);margin:.7rem 0 1rem}.hero-copy p{max-width:none;font-size:1rem;line-height:1.5}.hero-copy .eyebrow{font-size:.73rem}}

.political-setting{max-width:980px;border-top:3px solid #8f3529;padding-top:1.2rem;margin:1.4rem 0 2rem}.political-setting h3{font-size:1.5rem;margin-top:0}.political-setting p{max-width:900px}.political-note{border-left:3px solid #8f3529;padding-left:.9rem;font-size:.9rem;line-height:1.5}.progress .setting-cue{color:#8f3529;font-weight:700;border-bottom:0}
.mission{max-width:75ch;margin:1rem 0 1.5rem;border-left:4px solid #a2392c;padding-left:1rem}
.setting-facts{display:grid;grid-template-columns:1fr 1fr;gap:.2rem 2rem}
.setting-facts h4{margin:.4rem 0 .5rem;font-size:.95rem;color:#25483c}
.political-setting p{max-width:70ch}
.round-workflow{border-top:3px solid #a2392c;border-bottom:1px solid #8c9a88;margin:1.4rem 0;padding:1rem 0 .3rem}
.round-workflow ol{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1.4rem;margin:0 0 1rem;padding-left:1.3rem;line-height:1.4}
.round-workflow li{padding-left:.15rem;font-size:.87rem}
.round-workflow strong,.round-workflow span{display:block}
.round-workflow span{font-size:.82rem;margin-top:.3rem}
.request-summary{border-top:2px solid #173d3b;margin-top:1.3rem;padding-top:.3rem}
.request-summary dl{margin:.8rem 0;font-size:.82rem}
.request-summary dl>div{padding:.55rem 0;border-bottom:1px solid #c4c5af}
.request-summary dt{font-weight:bold}
.request-summary dd{margin:.2rem 0 0;line-height:1.4}
@media(max-width:700px){.setting-facts{grid-template-columns:1fr}.round-workflow ol{grid-template-columns:1fr 1fr;gap:1rem 1.5rem}}
.word-help,.model-detail{margin:1rem 0;border-top:1px solid #95a48c;padding-top:.7rem}
.word-help summary,.model-detail summary{cursor:pointer;font-weight:bold;font-size:.87rem;line-height:1.5}
.terms{font-size:.86rem;line-height:1.55}.terms dt{font-weight:bold;margin-top:.7rem}.terms dd{margin:.15rem 0 .5rem}
.decision-help{font-size:.9rem;line-height:1.6;padding-left:1.3rem}.decision-help li{margin:.65rem 0}
.response-checklist{font-size:.87rem;line-height:1.6;padding-left:1.35rem;color:#424f43}.response-checklist li{margin:.4rem 0}
.second-source{margin-top:.9rem}.question-cue{font-weight:bold;margin-bottom:.4rem}
.changes-board{border-top:3px solid #a2392c;border-bottom:1px solid #8c9a88;margin:1.5rem 0;padding:.3rem 0 .7rem}
.changes-board ul{font-size:.9rem;line-height:1.6;padding-left:1.3rem}.changes-board li{margin:.4rem 0}
.history-cue{border-left:4px solid #a2392c;background:#eee0c9;padding:.8rem 1rem;font-size:.9rem;line-height:1.6}
.snapshot .resource-result{color:#173d3b;font-size:.9rem;margin:.6rem 0}
</style>
</head>
<body>
<div id="app"></div><div id="live" class="sr-only" aria-live="polite" aria-atomic="true"></div><dialog id="dialog" aria-labelledby="dialog-title"></dialog><div id="printReport"></div><input type="file" id="restoreFile" accept="application/json,.json" hidden><noscript><p>This game needs JavaScript. It uses no account or external AI service.</p></noscript><script>const ASSETS={"room": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22600%22%3E%3Crect%20width%3D%221200%22%20height%3D%22600%22%20fill%3D%22%23e4d7bb%22%2F%3E%3C%2Fsvg%3E", "cast": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22600%22%3E%3Crect%20width%3D%221200%22%20height%3D%22600%22%20fill%3D%22%23e4d7bb%22%2F%3E%3C%2Fsvg%3E", "forum": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22600%22%3E%3Crect%20width%3D%221200%22%20height%3D%22600%22%20fill%3D%22%23e4d7bb%22%2F%3E%3C%2Fsvg%3E", "transfer": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22600%22%3E%3Crect%20width%3D%221200%22%20height%3D%22600%22%20fill%3D%22%23e4d7bb%22%2F%3E%3C%2Fsvg%3E"};
// Narrative content for the solo ensemble game. AI-authored design copy;
// durations and response targets are estimates, not observed learner results.
// minWords is a completion nudge only. It does not grade correctness.

const CONTENT = {
  ruleMemorySteps: [
    'The organization previously gave Youth at least one unit while Youth promised its priority policy.',
    'Youth later removed that promise. It was still absent in the immediately preceding round.',
    'If Youth brings the promise back now, a reply that would otherwise give Youth both units becomes one unit for each campaign for this return round.',
    'A Government-only, unused, or already-shared reply stays unchanged. This affects the return round, not every later round.'
  ],
  ruleHelp: {
  "transit": {
    "service": [
      "If only Youth promises operator reimbursement to protect routes and drivers’ pay (P4), Dani gives both reach units to Youth. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If only Government promises operator reimbursement (P4), Dani gives both reach units to Government.",
      "If both campaigns promise operator reimbursement (P4), Dani’s confidence in Government’s delivery experience breaks the tie: both reach units go to Government.",
      "If neither campaign promises operator reimbursement (P4), Dani keeps both reach units unused."
    ],
    "shared": [
      "If at least one campaign promises operator reimbursement to protect routes and drivers’ pay (P4), Dani accepts the shared arrangement: one reach unit goes to Government and one to Youth. This includes a tie and a promise from only one campaign.",
      "If neither campaign promises operator reimbursement (P4), Dani declines the shared request and keeps both reach units unused."
    ],
    "pressure": [
      "If Government promises operator reimbursement to protect routes and drivers’ pay (P4), Dani gives both reach units to Government. This includes a tie in which Youth also promises P4, and it applies even after an earlier insistence request.",
      "If only Youth promises operator reimbursement (P4), and your request to Dani in the immediately preceding round was also an insistence request, Dani keeps both reach units unused. The previous request need not have succeeded for this consecutive-request rule to apply.",
      "If only Youth promises operator reimbursement (P4), and you did not make an insistence request to Dani in the immediately preceding round, Dani gives both reach units to Youth. This also covers the first round. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If neither campaign promises operator reimbursement (P4), Dani keeps both reach units unused."
    ]
  },
  "union": {
    "service": [
      "If Youth promises the discounted young-person pass (P1), Mika gives both reach units to Youth. If Government also promises P1, Mika’s preference for Youth’s original pass proposal breaks the tie. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If only Government promises the discounted young-person pass (P1), Mika gives both reach units to Government.",
      "If neither campaign promises the discounted young-person pass (P1), Mika keeps both reach units unused."
    ],
    "shared": [
      "If Youth promises the discounted young-person pass (P1), Mika accepts the shared arrangement: one reach unit goes to Government and one to Youth, whether or not Government also promises P1.",
      "If only Government promises the discounted young-person pass (P1), Mika declines the split and gives both reach units to Government.",
      "If neither campaign promises the discounted young-person pass (P1), Mika keeps both reach units unused."
    ],
    "pressure": [
      "If Youth promises the discounted young-person pass (P1), Mika rejects exclusive Youth backing but gives one reach unit to each campaign. This also applies if both campaigns promise P1.",
      "If only Government promises the discounted young-person pass (P1), Mika gives both reach units to Government.",
      "If neither campaign promises the discounted young-person pass (P1), Mika keeps both reach units unused."
    ]
  },
  "shift": {
    "service": [
      "If only Youth promises more early and late journeys (P3), Alex gives both reach units to Youth. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If only Government promises more early and late journeys (P3), Alex gives both reach units to Government.",
      "If both campaigns promise more early and late journeys (P3), compare their operator reimbursement promises (P4). If only one campaign also promises P4, Alex gives both reach units to that campaign. If that campaign is Youth, the return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If both campaigns promise more early and late journeys (P3) and have the same operator reimbursement position (both include P4 or neither includes P4), Alex gives one reach unit to each campaign.",
      "If neither campaign promises more early and late journeys (P3), Alex keeps both reach units unused, even if a plan includes operator reimbursement (P4)."
    ],
    "shared": [
      "If both campaigns promise more early and late journeys (P3), Alex accepts the shared arrangement and gives one reach unit to each campaign.",
      "If only Youth promises more early and late journeys (P3) and Youth also promises operator reimbursement (P4), Alex accepts the shared arrangement and gives one reach unit to each campaign.",
      "If only Youth promises more early and late journeys (P3), but Youth does not promise operator reimbursement (P4), Alex declines the split and gives both reach units to Youth. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If only Government promises more early and late journeys (P3), Alex declines the split and gives both reach units to Government.",
      "If neither campaign promises more early and late journeys (P3), Alex keeps both reach units unused."
    ],
    "pressure": [
      "If Youth promises both more early and late journeys (P3) and operator reimbursement (P4), Alex gives both reach units to Youth, even if Government also promises P3. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If Youth does not promise the P3 plus P4 package and Government promises more early and late journeys (P3), Alex gives both reach units to Government.",
      "If Government does not promise more early and late journeys (P3) and Youth does not promise both P3 and operator reimbursement (P4), Alex keeps both reach units unused. This includes Youth promising P3 without P4 and neither campaign promising P3."
    ]
  },
  "neighborhood": {
    "service": [
      "If both campaigns promise the discounted low-income pass (P2), Noa gives one reach unit to each campaign.",
      "If only Youth promises the discounted low-income pass (P2), Noa gives both reach units to Youth. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If only Government promises the discounted low-income pass (P2) and the public terminal is Government-only, Noa gives one reach unit to each campaign to preserve an independent opportunity for Youth.",
      "If only Government promises the discounted low-income pass (P2) and the public terminal is shared, Noa gives both reach units to Government.",
      "If neither campaign promises the discounted low-income pass (P2), Noa keeps both reach units unused, even if the terminal excludes Youth."
    ],
    "shared": [
      "If either or both campaigns promise the discounted low-income pass (P2), Noa gives one reach unit to each campaign, regardless of terminal access.",
      "If neither campaign promises the discounted low-income pass (P2), but the public terminal is Government-only, Noa still gives one reach unit to each campaign to preserve independent access.",
      "If neither campaign promises the discounted low-income pass (P2) and the public terminal is already shared, Noa keeps both reach units unused."
    ],
    "pressure": [
      "If Government promises the discounted low-income pass (P2), Noa gives both reach units to Government. This includes a tie in which Youth also promises P2, and it applies even if the terminal excludes Youth.",
      "If only Youth promises the discounted low-income pass (P2), Noa gives both reach units to Youth. The return-after-withdrawal rule below can change this to one unit for each campaign.",
      "If neither campaign promises the discounted low-income pass (P2), Noa keeps both reach units unused."
    ]
  },
  "memory": "Return after a withdrawn promise: this applies only if an organization previously gave Youth at least one reach unit while Youth offered that organization’s priority policy, Youth later withdrew that policy, and the policy was absent in the immediately preceding round. If Youth restores it now and the current request would otherwise earn both units for Youth, the organization instead gives one unit to each campaign for this return round. A decision to give both units to Government, keep both unused, or already share is unchanged. This is a conditional change on return, not a permanent penalty or a rule that automatically turns every reply into sharing."
},
  title: 'The Order Was Given',
  subtitle: 'Three rounds to build a transport campaign in Lydora',

  setting: {
    title: 'An election under authoritarian rule',
    headings: ['Who holds power', 'Why the contest is unequal', 'Why citizens differ', 'Where opposition can still act'],
    paragraphs: [
      'Lydora is an authoritarian state. The Unity Party has held national power for 18 years. Its leaders dominate the executive, the election authority, and the senior courts. State institutions and the ruling party routinely work together to protect its hold on office.',
      'Opposition parties are legal and have won some local elections. They compete for real support, but national television overwhelmingly promotes the government, public resources favor its campaigns, and critical organizers have faced selective inspections, fines, or police questioning. An election can matter even when the contest is heavily tilted.',
      'Many citizens genuinely support the government. They credit it with the common fare card and transport improvements, or trust it more than its challengers. Others want change but worry about the cost of visible opposition.',
      'Opposition networks and some independent organizations remain active. The four organizations at your table have retained practical control of their own spaces, contacts, and vehicles for tonight. Ari can exclude Youth from the public terminal; securing their cooperation is a different problem.'
    ],
    actorNotes: {
      transit: 'Transport funding runs through the ministry. Dani can value the government’s delivery while retaining the co-op’s choice over tonight’s contribution.',
      union: 'The union organizes amid scrutiny of opposition activity. Its campus network is one of the spaces still available for independent campaigning.',
      shift: 'Official coverage gives little space to late-shift journeys. Alex’s workplace contacts can carry an argument that receives little airtime.',
      neighborhood: 'Public venues often favor the ruling party. Noa’s independent hall can provide another place to hear a political case.'
    },
    scope: 'The authoritarian setting is given. Tonight’s decisions explore how public control and independent cooperation work within it; they do not establish why these organizations retain room to act.'
  },

  opening: {
    eyebrow: 'Lydora · Elections under authoritarian rule',
    title: 'You missed the connection. Now you have a campaign to run.',
    paragraphs: [
      'You are Sasha Marin, an apprentice and the organizer of Youth, a legal opposition election campaign. A missed bus connection once cost you an apprenticeship interview. You know what a journey can decide before a person reaches the door.',
      'Fares have risen. Your Youth Campaign offers a discounted young-person pass and more early and late journeys. Ari Velen, transport minister and a campaign leader for the ruling Unity Party, offers a discounted low-income pass and reimbursement to protect operators’ routes and pay. He points to the common fare card his government introduced. Tonight, both campaigns must make their public case.',
      'You have volunteers, a small forum, and a table covered in unanswered invitations. Four organizations have spaces, contacts, or transport resources of their own. Each has members to answer to. You can keep your opening plan or publish a different pair of commitments before asking for their support.',
      'A notice from the terminal office has just arrived. Read it, then make your first plan. Ari and the organizations will make their own decisions when the round closes.'
    ],
    order: 'The central bus terminal will host only the official transport forum. Partner organizations are directed to bring their members to the official forum rather than the Youth Campaign’s event.',
    modeNote: 'Class keeps eight short writing responses, about 300 words in total, in your report. Just for Fun lets you skip writing; the same people, choices, and rules remain.'
  },

  rounds: [
    {
      id: 1,
      title: 'First calls',
      brief: 'The notice lies between your policy sheet and a cold cup of tea. Dani is waiting at the depot, Mika has borrowed a room between classes, Alex is on a break, and Noa is checking the hall bookings. Choose your two commitments and decide how to approach each of them.',
      development: 'These first allocations are provisional. Your published plan and appeals will appear on the decision board; nobody has promised to repeat an allocation in the next round.'
    },
    {
      id: 2,
      title: 'Your first choices are public',
      brief: 'The first replies are in. Read what each owner actually chose before making another offer. You can retain your plan, revise it, or change the way you ask. The people across the table have already heard your first approach.',
      development: 'There is another chance to request shared terminal access. A request changes access only if Ari agrees. Review the current terminal decision and each organization’s explanation before predicting the next reveal.'
    },
    {
      id: 3,
      title: 'Before the doors open',
      brief: 'Noa needs a final entry in the booking book. Dani’s drivers need to know which arrangements are confirmed. Your volunteers want one clear policy sheet to carry tonight. Put forward the plan you are prepared to stand behind, then make your final requests.',
      development: 'This reveal fixes the arrangements for tonight’s forums. There will be no further negotiation after it. An earlier allocation shows what its owner chose then; it does not promise what they will choose now.'
    }
  ],

  characters: {
    coop: {
      name: 'Dani Soren',
      role: 'Elected driver delegate · Transit Co-op',
      priority: 'P4 · Reimburse operators to protect routes and drivers’ pay',
      asset: 'Vehicles, driver network, and booking channel · 2 reach units',
      description: 'Dani has a duty roster folded into a coat pocket. Every new promise has to survive a question from the people driving the first bus tomorrow.',
      questions: [
        {
          q: 'What is the first thing your members will ask?',
          a: '“Who keeps the routes running, and who pays the drivers?” A cheaper pass is welcome. It does not answer that question by itself. P4 is the commitment I look for first.'
        },
        {
          q: 'What could the co-op contribute tonight?',
          a: 'Our vehicles, drivers’ contacts, and booking channel. Together they provide two units in these forum arrangements. The members put those resources in our hands; I have to account for where they go.'
        },
        {
          q: 'Have you already chosen Ari?',
          a: 'He has published something our members care about. That earns a hearing. You can make your own case, and you can ask us to support both forums. Read my reply before calling anything agreed.'
        }
      ],
      appeals: {
        service: { label: 'Make the service case', detail: 'Ask Dani to judge Youth’s published plan against the co-op’s transport priority.' },
        shared: { label: 'Propose support for both forums', detail: 'Ask the co-op to divide its two units, one to each campaign.' },
        pressure: { label: 'Insist the drivers should back Youth', detail: 'Demand exclusive Youth support on the grounds that the co-op owes the challenger its backing.' }
      }
    },

    students: {
      name: 'Mika Iven',
      role: 'Commuter representative · Student Union',
      priority: 'P1 · Discounted young-person pass',
      asset: 'Campus meeting space and member network · 2 reach units',
      description: 'Mika is balancing a timetable against a work schedule. The union’s members agree about the cost of travel more readily than they agree about parties.',
      questions: [
        {
          q: 'What would make the greatest difference to your members?',
          a: 'P1 comes first for us. A pass is a daily cost, even in a week when every connection works. Some students also work nights, so you will hear more than one concern in our meetings.'
        },
        {
          q: 'Can I count the student union as part of Youth?',
          a: 'A student card is not a party card, Sasha. Some members like Ari’s fare card; some want him out. Bring us a plan and a request. Do not put our name on your supporters’ sheet before we answer.'
        },
        {
          q: 'What could you offer a forum?',
          a: 'A campus meeting space and a network of members: two units in the forum arrangements. We could support one campaign, divide them, or keep them unused. I will tell you what we actually decide.'
        }
      ],
      appeals: {
        service: { label: 'Make the commuter case', detail: 'Ask Mika to judge Youth’s published plan against the union’s transport priority.' },
        shared: { label: 'Propose a place for both campaigns', detail: 'Ask the union to divide its two units, one to each campaign.' },
        pressure: { label: 'Insist students belong with Youth', detail: 'Demand exclusive Youth support on the grounds that a student union owes the challenger its backing.' }
      }
    },

    workers: {
      name: 'Alex Kova',
      role: 'Hospital porter · Shift Workers Network',
      priority: 'P3 · More early and late journeys, with no extra night fare',
      asset: 'Community meeting room and workplace contacts · 2 reach units',
      description: 'Alex has a short break before the next hospital shift. The network’s argument begins at the hour when most campaign offices close.',
      questions: [
        {
          q: 'Which journey are the campaigns missing?',
          a: 'The one after the last scheduled bus. I can finish a shift on time and still have no way home. P3 is our first priority: early and late journeys, without an extra night fare.'
        },
        {
          q: 'Would your members support an incumbent?',
          a: 'They might. They might support you. They need a usable journey after the campaign banners come down. Show me what you have published; do not ask me to infer it from the age of your candidates.'
        },
        {
          q: 'What can the network decide tonight?',
          a: 'How to use our community room and workplace contacts. Those are our two units here. We can offer them to one forum, divide them, or withhold them. Nobody has booked them by printing our name on a notice.'
        }
      ],
      appeals: {
        service: { label: 'Make the late-journey case', detail: 'Ask Alex to judge Youth’s published plan against the network’s transport priority.' },
        shared: { label: 'Propose support across both forums', detail: 'Ask the network to divide its two units, one to each campaign.' },
        pressure: { label: 'Insist workers should stand with Youth', detail: 'Demand exclusive Youth support on the grounds that the network owes the challenger its backing.' }
      }
    },

    neighbors: {
      name: 'Noa Dalen',
      role: 'Community hall steward · Neighborhood Association',
      priority: 'P2 · Discounted low-income pass',
      asset: 'Accessible hall and neighborhood contacts · 2 reach units',
      description: 'Noa knows which neighbors need the hall’s ramp and which are counting every fare. A booking is also a promise to people who expect the doors to open.',
      questions: [
        {
          q: 'Whose journeys are you thinking about?',
          a: 'People who cannot absorb another fare rise. P2 is our first priority. Some are young, some are retired, and some work next door. I need a plan that remembers them when it leaves this table.'
        },
        {
          q: 'What is available at your hall?',
          a: 'An accessible hall and neighborhood contacts: two units in these forum arrangements. Our association decides how they are used. If we share, each campaign gets one unit; neither gets to claim both.'
        },
        {
          q: 'Does the terminal notice settle your booking?',
          a: 'It is a serious notice. It tells me what Ari intends. I still have an association meeting to answer to and a booking book to complete. Make your request, and judge our decision when you receive it.'
        }
      ],
      appeals: {
        service: { label: 'Make the affordable-fare case', detail: 'Ask Noa to judge Youth’s published plan against the association’s transport priority.' },
        shared: { label: 'Propose room for both forums', detail: 'Ask the association to divide its two units, one to each campaign.' },
        pressure: { label: 'Insist the neighborhood must back Youth', detail: 'Demand exclusive Youth support on the grounds that the association owes the challenger its backing.' }
      }
    }
  },

  forum: {
    title: 'The arrangements become a public occasion',
    paragraphs: [
      'The booking sheets are closed. The resources on the final decision board now set each campaign’s speaking opportunity. Ari will make his case, and Sasha will make theirs. An organization’s support does not decide how its individual members will vote.',
      'Look at the forum your choices helped make possible. Check which contributions came from exclusive support or sharing, and which resources stayed unused. The terminal decision remains part of these arrangements even where another owner offered an alternative.'
    ],
    closing: 'The election is still ahead. The decision board shows tonight’s speaking opportunities; it does not show how citizens will vote.'
  },

  prompts: [
    {
      id: 'opening', label: 'Your first intention', minWords: 15, targetWords: 30,
      prompt: 'Choose one thing you want Sasha’s campaign to achieve today and explain why it matters. Name something another person could say or do that might change your approach.'
    },
    {
      id: 'forecast1', label: 'Before the first reveal', minWords: 20, targetWords: 30,
      prompt: 'Predict one organization’s resource decision (allocation) in this round and give your reason. Point to your published plan, your appeal, or something that person said; make clear where you are making an assumption.'
    },
    {
      id: 'forecast2', label: 'Before the second reveal', minWords: 20, targetWords: 30,
      prompt: 'Predict one decision that will stay the same or change in this round. Explain how your current approach and one of the earlier decisions support that prediction.'
    },
    {
      id: 'forecast3', label: 'Before the final reveal', minWords: 20, targetWords: 30,
      prompt: 'Predict one final organization decision and explain the rule or evidence behind it. If you remain uncertain, name a missing detail or assumption.'
    },
    {
      id: 'explain', label: 'Explain an actual consequence', minWords: 25, targetWords: 45,
      prompt: 'Compare Ari’s terminal decision with one organization’s resource decision, using two revealed evidence items. Who controlled each choice, and what did each choice mean for a campaign’s speaking opportunity? State one thing this evidence cannot establish about votes or fairness beyond this venue.'
    },
    {
      id: 'compare', label: 'Predict the access-only comparison', minWords: 20, targetWords: 40,
      prompt: 'Only terminal access will switch; the published plans and all four final resource decisions (allocations) will stay fixed. Predict what will change and what this comparison could help you judge about tonight’s contest.'
    },
    {
      id: 'revise', label: 'Reconsider your claim', minWords: 20, targetWords: 40,
      prompt: 'Use the access-only result and one earlier forecast to defend, narrow, or change your explanation. State what remains unknown about votes. Would either side’s eventual victory settle whether tonight’s access was fair?'
    },
    {
      id: 'transfer', label: 'A different town', minWords: 25, targetWords: 55,
      prompt: 'In another town, a mayor closes a municipal hall for a documented repair and orders an independently owned sports club to cancel a rival’s meeting. Name one fact, not already given, that you would investigate. Explain how different answers could change your prediction about what happens next.'
    }
  ],

  readingQuestion: 'What would you need to learn about real institutions before using Lydora’s experience to explain why an opposition group can keep operating?'
};



const Engine=(()=>{
/*
 * The Order Was Given — deterministic, offline model.
 * NPC rules are authored fictional decisions, not empirical political laws.
 * No election result, voter motive, or learning score is generated here.
 */

const OWNER_IDS = ['transit', 'union', 'shift', 'neighborhood'];
const POLICY_IDS = ['P1', 'P2', 'P3', 'P4'];
const APPEAL_IDS = ['service', 'shared', 'pressure'];
const ALLOCATION_IDS = ['G', 'Y', 'S', 'W'];
const SCHEMA_VERSION = 1;

function freeze(value) {
  if (value && typeof value === 'object' && !Object.isFrozen(value)) {
    Object.values(value).forEach(freeze);
    Object.freeze(value);
  }
  return value;
}

function copy(value) {
  return structuredClone(value);
}

function plainObject(value) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function fail(message) {
  throw new TypeError(message);
}

function exactKeys(object, keys, label) {
  if (!plainObject(object)) fail(`${label} must be an object.`);
  const actual = Object.keys(object).sort();
  if (actual.length !== keys.length || actual.some((key, index) => key !== [...keys].sort()[index])) {
    fail(`${label} must contain exactly: ${keys.join(', ')}.`);
  }
}

function normalizePlan(plan, label = 'Plan') {
  if (!Array.isArray(plan) || plan.length !== 2 || new Set(plan).size !== 2 ||
      plan.some(policy => !POLICY_IDS.includes(policy))) {
    fail(`${label} must contain exactly two different policies from P1–P4.`);
  }
  return POLICY_IDS.filter(policy => plan.includes(policy));
}

function normalizeAction(action) {
  exactKeys(action, ['plan', 'appeals', 'requestShared'], 'Round decision');
  const plan = normalizePlan(action.plan, 'Youth plan');
  exactKeys(action.appeals, OWNER_IDS, 'Appeals');
  const appeals = {};
  for (const id of OWNER_IDS) {
    if (!APPEAL_IDS.includes(action.appeals[id])) {
      fail(`The ${id} appeal must be service, shared, or pressure.`);
    }
    appeals[id] = action.appeals[id];
  }
  if (typeof action.requestShared !== 'boolean') fail('requestShared must be true or false.');
  return { plan, appeals, requestShared: action.requestShared };
}

const POLICIES = freeze([
  { id: 'P1', title: 'Discounted young-person pass', detail: 'A discounted pass for young passengers.' },
  { id: 'P2', title: 'Discounted low-income pass', detail: 'A discounted pass for passengers on low incomes.' },
  { id: 'P3', title: 'More early and late journeys', detail: 'More early and late journeys, with no extra night fare.' },
  { id: 'P4', title: 'Protect routes and drivers’ pay', detail: 'Reimburse operators to protect routes and drivers’ pay during fare reductions.' }
]);

const ACTORS = freeze([
  { id: 'transit', name: 'Transit Co-op', person: 'Dani Soren', job: 'Elected driver delegate', priority: 'P4', asset: 'Vehicles, driver network, and booking channel', secondary: 'Delivery confidence; the incumbent wins a P4 tie.' },
  { id: 'union', name: 'Student Union', person: 'Mika Iven', job: 'Commuter student representative', priority: 'P1', asset: 'Campus meeting space and member network', secondary: 'Member independence; a demand does not bind students to a party.' },
  { id: 'shift', name: 'Shift Workers Network', person: 'Alex Kova', job: 'Evening hospital porter', priority: 'P3', asset: 'Community meeting room and workplace contacts', secondary: 'P4 strengthens a P3 delivery case; two equally credible P3 plans can share.' },
  { id: 'neighborhood', name: 'Neighborhood Association', person: 'Noa Dalen', job: 'Community hall steward', priority: 'P2', asset: 'Accessible hall and neighborhood contacts', secondary: 'Affordable journeys and fair public access; a shared hall can preserve both forums.' }
]);

const ACTOR_BY_ID = Object.fromEntries(ACTORS.map(actor => [actor.id, actor]));

const RULE_SUMMARIES = freeze([
  'Dani: service backs the only P4 plan; if both have P4, incumbent delivery experience wins the tie. A shared request is accepted if either plan has P4. A pressure request can earn Youth-only cooperation when Youth alone has P4, but with Youth alone offering P4, a second consecutive pressure request leaves both units unused. If neither campaign has P4, both units remain unused.',
  'Mika: service backs Youth if Youth publishes P1, including a P1 tie; otherwise Government can earn the assets with P1. Sharing is accepted only when Youth also offers P1. A pressure request with Youth P1 earns shared cooperation rather than exclusive endorsement.',
  'Alex: service first compares P3, then P4 as a delivery tie-break; equally credible P3 plans share. A shared request is accepted if both have P3 or Youth combines P3 with P4; otherwise the sole P3 provider receives both units. A pressure request succeeds for Youth with P3 + P4; otherwise Government P3 wins or resources remain unused.',
  'Noa: service compares P2. Both P2 plans share. If only Government has P2 while the terminal excludes Youth, Noa still shares the hall to preserve independent access; with a shared terminal, Government receives both units. A shared request is accepted when P2 or terminal exclusion supplies a reason. Under pressure, Noa prefers Government’s P2 case.',
  'Local memory: if an owner contributed to Youth while Youth published its priority, then Youth withdrew it in a later round, restoring that priority earns shared rather than exclusive Youth cooperation for that round. This is a local authored reservation, not a global trust score.',
  'Ari controls the public terminal. In round 1 the order remains exclusive. Later, already-shared access stays shared. Otherwise Ari accepts a sharing request only if Noa shared the hall in the previous revealed round. The request alone cannot change access.',
  'Each campaign retains one baseline reach. The terminal supplies two units; each independent owner controls two. G = 2/0, Y = 0/2, S = 1/1, W = 0/0 with two unused. Pitch seconds = 20 + 5 × reach. These are speaking opportunities, not votes.'
]);

/** A new run has no clock, random seed, generated ballot, or outside dependency. */
function createState(scenario = 'standard') {
  if (scenario !== 'standard') fail('Unknown scenario. Use standard.');
  return freeze({
    schemaVersion: SCHEMA_VERSION,
    round: 1,
    history: [],
    lastPlan: ['P1', 'P3'],
    scenario
  });
}

function currentGovernment(state) {
  if (state.round === 1) return {
    plan: ['P2', 'P4'],
    reason: 'Ari publishes low-income fares and operator reimbursement, defending the Government’s delivery record.'
  };
  if (state.round === 2) return {
    plan: ['P2', 'P3'],
    reason: 'Before your decision, Ari publicly replaces P4 with P3 to seek the shift workers’ cooperation. The Government still has only two promises.'
  };
  const prior = state.history.at(-1);
  if (prior?.allocations.shift.choice === 'G') return {
    plan: ['P2', 'P3'],
    reason: 'Ari keeps P2 + P3 after the Shift Workers Network gave Government both units in the previous reveal.'
  };
  return {
    plan: ['P2', 'P4'],
    reason: 'Without exclusive Shift Workers cooperation in the previous reveal, Ari restores P4 beside P2 to seek the co-op again.'
  };
}

function accessDecision(state, requested) {
  const prior = state.history.at(-1);
  if (state.round === 1) return {
    terminal: 'exclusive', possible: false,
    reason: requested
      ? 'Ari receives your request but keeps the first-round terminal order. Government controls access to the public terminal; your request cannot allocate it.'
      : 'Ari retains Government-only access to the public terminal under the first-round order.'
  };
  if (prior.terminal === 'shared') return {
    terminal: 'shared', possible: true,
    reason: 'Ari retains the shared access already published. Government owns this access decision; Youth does not revoke or renew it.'
  };
  const supported = prior.allocations.neighborhood.choice === 'S';
  if (requested && supported) return {
    terminal: 'shared', possible: true,
    reason: 'Noa’s previous shared-hall decision supplied a public example of access for both forums. Ari accepts your published request and changes his own terminal rule to shared.'
  };
  return {
    terminal: 'exclusive', possible: supported,
    reason: requested
      ? 'Ari refuses the request: Noa did not share the hall in the previous reveal, the announced condition for this fictional concession. The terminal stays Government-only.'
      : 'No new sharing request was published. Ari retains Government-only terminal access.'
  };
}

function supportedThenWithdrawn(state, actor) {
  let supportedRound = null;
  for (const snapshot of state.history) {
    if (snapshot.youthPlan.includes(actor.priority) && ['Y', 'S'].includes(snapshot.allocations[actor.id].choice)) {
      supportedRound = snapshot.round;
    } else if (supportedRound !== null && !snapshot.youthPlan.includes(actor.priority)) {
      return { supportedRound, withdrawnRound: snapshot.round };
    }
  }
  return null;
}

function historyNotes(state, actor) {
  const prior = state.history.at(-1);
  if (!prior) return ['No previous arrangement has been revealed.'];
  const notes = [`Previous owner decision: ${prior.allocations[actor.id].choice}. Previous Youth promise: ${prior.youthPlan.join(' + ')}.`];
  if (actor.id === 'transit' && prior.action.appeals.transit === 'pressure') {
    notes.push('Dani heard a pressure request last round. A second consecutive demand will block otherwise-exclusive Youth cooperation.');
  }
  const withdrawn = supportedThenWithdrawn(state, actor);
  if (withdrawn && !prior.youthPlan.includes(actor.priority)) {
    notes.push(`${actor.person} contributed while Youth offered ${actor.priority} in round ${withdrawn.supportedRound}; Youth withdrew it in round ${withdrawn.withdrawnRound}. Restoring it now can earn S rather than an otherwise-Y decision for this round.`);
  } else if (prior.youthPlan.includes(actor.priority) && ['Y', 'S'].includes(prior.allocations[actor.id].choice)) {
    notes.push(`${actor.person} contributed with Youth’s ${actor.priority} promise on record. A withdrawal now is visible; a later restoration will first earn shared rather than exclusive Youth cooperation.`);
  }
  return notes;
}

/** Information available before sealing. All consequential conditions are inspectable. */
function getRoundBrief(state) {
  validateState(state);
  if (state.round === 4) {
    const last = state.history.at(-1);
    return freeze({ round: 4, complete: true, title: 'The forums can open', governmentPlan: copy(last.governmentPlan), terminal: last.terminal, rules: copy(RULE_SUMMARIES) });
  }
  const government = currentGovernment(state);
  const access = accessDecision(state, false);
  const terminalRule = state.round === 1
    ? 'The first-round terminal order remains Government-only, including if you request sharing.'
    : state.history.at(-1).terminal === 'shared'
      ? 'Ari will retain the access he already shared. A further request is unnecessary.'
      : access.possible
        ? 'Noa shared the hall last round. If you request shared terminal access, Ari will accept under the announced concession rule; otherwise access stays exclusive.'
        : 'Noa did not share the hall last round. Ari will refuse a sharing request this round. A request has no command authority.';
  return freeze({
    round: state.round,
    complete: false,
    title: ['Provisional arrangements', 'Public commitments', 'Final arrangements'][state.round - 1],
    governmentPlan: copy(government.plan),
    governmentReason: government.reason,
    terminal: access.terminal,
    terminalRule,
    terminalRequestPossible: access.possible,
    historyNotes: Object.fromEntries(ACTORS.map(actor => [actor.id, historyNotes(state, actor)])),
    historyReservations: Object.fromEntries(ACTORS.map(actor => [actor.id, supportedThenWithdrawn(state, actor)])),
    rules: copy(RULE_SUMMARIES)
  });
}

function result(choice, rule, reason, quote) {
  return { choice, rule, reason, quote };
}

function transitDecision(context) {
  const { youth, government, appeal, priorPressure } = context;
  if (appeal === 'shared') {
    return youth || government
      ? result('S', 'P4 is present; accept shared cooperation.', 'At least one published plan protects operators. Dani accepts a shared booking channel while keeping the co-op independent.', 'We can serve both forums. Our network remains ours, and P4 is still the priority.')
      : result('W', 'Neither plan has P4; decline a shared booking.', 'Sharing by itself does not answer the co-op’s reimbursement concern. Neither plan includes P4.', 'A shared invitation does not replace a route-and-pay commitment. We will keep the channel unused.');
  }
  if (government) {
    return result('G', youth ? 'Both plans have P4; incumbent delivery confidence breaks the tie.' : 'Only Government has P4.', youth
      ? 'Both campaigns publish P4. In this authored decision, Dani gives greater weight to the incumbent’s delivery experience; your appeal cannot command the co-op.'
      : 'Government is the only campaign publishing the co-op’s first priority, P4.', youth
      ? 'You both name P4. For this booking, the delivery record keeps us with Ari. This is our choice, not his order.'
      : 'Ari has kept P4 on paper. We will provide both units to that forum by our own decision.');
  }
  if (youth) {
    if (appeal === 'pressure' && priorPressure) return result('W', 'Consecutive pressure blocks exclusive Youth cooperation.', 'Youth alone offers P4, but Dani refuses a second consecutive attempt to claim the co-op’s backing. Both units remain unused for this round.', 'You pressed us last round too. We will not put the booking channel under your unilateral direction.');
    return result('Y', appeal === 'pressure' ? 'Only Youth has P4; its policy case outweighs this first demand.' : 'Only Youth has P4.', appeal === 'pressure'
      ? 'Dani rejects the idea that Youth owns the network but independently chooses Youth because only its published plan includes P4. Pressure is not a transfer of ownership.'
      : 'Only Youth publishes P4, so the co-op chooses to contribute its two units to Youth.', appeal === 'pressure'
      ? 'The demand gives you no authority over us. P4 does give us a reason to work with you this time.'
      : 'Your P4 commitment answers our first concern. We choose to bring the vehicles and booking channel to your forum.');
  }
  return result('W', 'Neither plan has P4.', 'Neither campaign publishes the co-op’s reimbursement priority. Both units stay unused.', 'Neither plan protects the operators here. We will not book either forum.');
}

function unionDecision({ youth, government, appeal }) {
  if (appeal === 'pressure' && youth) return result('S', 'Youth P1 earns cooperation; member independence prevents exclusive endorsement under pressure.', 'Mika values Youth’s P1 commitment but refuses the demand to treat all students as Youth supporters. The union allocates one unit to each forum.', 'P1 earns cooperation. It does not make our members your campaign. We will share the space and contacts.');
  if (appeal === 'shared') {
    if (youth) return result('S', 'Youth has P1; accept a shared arrangement.', 'Youth’s P1 commitment makes a shared invitation acceptable to Mika. The union chooses S.', 'Keep the young-person pass in the plan and we can help both forums.');
    if (government) return result('G', 'Only Government has P1; decline the proposed split.', 'A shared request does not outweigh the only published P1 promise. Mika independently gives both units to Government.', 'You are asking us to divide our support while only Ari offers P1. We choose his forum.');
    return result('W', 'Neither plan has P1; decline the proposed split.', 'Neither plan includes a young-person pass. Mika keeps both union units unused.', 'Neither offer answers the pass issue. The campus network will stay out of this booking.');
  }
  if (youth) return result('Y', government ? 'Both plans have P1; Mika’s stated Youth preference breaks the tie.' : 'Only Youth has P1.', government
    ? 'Both plans include P1. Mika’s authored preference for the Youth campaign’s originating pass proposal breaks the tie; no member’s ballot is bound.'
    : 'Youth includes the union’s first priority, P1. Mika chooses Youth cooperation without binding individual citizens’ ballots.', 'The pass is in your two-promise plan. We choose to give your forum the campus space and network.');
  if (government) return result('G', 'Only Government has P1.', 'Government alone publishes the union’s P1 priority. The union chooses Government by its own decision.', 'Our priority is the pass, not owing a party our support. This time the Government offers it.');
  return result('W', 'Neither plan has P1.', 'Neither published plan includes P1. The union’s two units remain unused.', 'We cannot point members to a young-person pass in either plan. We will withhold our network.');
}

function shiftDecision({ youth, government, appeal, youthPlan, governmentPlan }) {
  const youthDelivery = youthPlan.includes('P4');
  const governmentDelivery = governmentPlan.includes('P4');
  if (appeal === 'pressure') {
    if (youth && youthDelivery) return result('Y', 'Youth P3 + P4 supplies the full service-and-delivery case.', 'Alex dislikes a claim on the network, but Youth’s P3 + P4 plan meets both stated concerns. Alex independently chooses Youth; the demand itself has no force.', 'You cannot claim our members. But early and late journeys with protected routes is the package we need. We choose your forum.');
    if (government) return result('G', 'Government P3 beats a pressured Youth request without P3 + P4.', 'Youth has not supplied the P3 + P4 combination that would persuade Alex under this demand. Government’s P3 commitment earns both units.', 'A demand is not a reliable timetable. Ari has put P3 in his plan, so our room and contacts go there.');
    return result('W', 'No acceptable P3 delivery case under this demand.', 'Government has no P3, and Youth has not paired P3 with P4. Alex declines the exclusive demand and leaves both units unused.', 'We need a usable service case. Telling us which side we belong to does not provide one.');
  }
  if (appeal === 'shared') {
    if ((youth && government) || (youth && youthDelivery)) return result('S', 'Both P3 plans, or Youth P3 + P4, make sharing acceptable.', 'Alex accepts the proposed split because both offer P3 or Youth pairs the journey promise with P4. One unit goes to each forum.', 'There is enough of a service case to keep both forums in the conversation. We choose to share.');
    if (youth) return result('Y', 'Only Youth has P3; decline dilution of the sole service promise.', 'Alex declines the proposed split and voluntarily gives both units to the only P3 provider. An owner can cooperate without accepting the exact proposed deal.', 'Only your plan promises our journeys. We will put both units there rather than split them.');
    if (government) return result('G', 'Only Government has P3; decline dilution of the sole service promise.', 'Alex declines a split and chooses the only campaign publishing P3, Government.', 'Only Ari has P3 in this reveal. We choose his forum for the room and contacts.');
    return result('W', 'Neither plan has P3.', 'Sharing does not supply a missing early-and-late journey commitment. Both units remain unused.', 'Neither plan has the journeys our members need. We will keep the room unused.');
  }
  if (youth && government) {
    if (youthDelivery !== governmentDelivery) return youthDelivery
      ? result('Y', 'Both have P3; Youth P4 supplies the delivery tie-break.', 'Both campaigns offer P3. Youth alone pairs it with P4, so Alex chooses Youth.', 'Both say P3. Your P4 promise gives us the stronger delivery case, so we choose your forum.')
      : result('G', 'Both have P3; Government P4 supplies the delivery tie-break.', 'Both campaigns offer P3. Government alone pairs it with P4, so Alex chooses Government.', 'Both say P3. Ari’s P4 promise supplies the delivery case this time.');
    return result('S', 'Both have equally supported P3 plans.', 'Both campaigns offer P3 and neither has a unique P4 delivery case. Alex shares the room and contacts.', 'Both plans answer the journey issue on these terms. We will divide our cooperation.');
  }
  if (youth) return result('Y', 'Only Youth has P3.', 'Only Youth offers P3. Alex chooses to contribute both units to Youth.', 'The early and late journeys are in your plan. We choose to use our room and contacts for your forum.');
  if (government) return result('G', 'Only Government has P3.', 'Only Government offers P3. Alex chooses Government cooperation without surrendering ownership.', 'Ari has made the journey commitment. Our assets remain ours; we choose to use them with his forum.');
  return result('W', 'Neither plan has P3.', 'Neither plan offers P3. The shift workers retain their two unused units.', 'Neither plan answers the shift journey problem. We will keep the room and contacts unused.');
}

function neighborhoodDecision({ youth, government, appeal, terminal }) {
  if (appeal === 'shared') {
    if (youth || government || terminal === 'exclusive') return result('S', 'P2 or public exclusion supplies a reason to share independent access.', 'Noa accepts a split to keep both forums accessible. This independent hall decision does not change the terminal rule by itself.', 'We will host contacts from both forums. Ari should make public access equally usable too; our hall cannot make that order for him.');
    return result('W', 'No P2 or remaining exclusion reason for this shared booking.', 'Neither plan offers P2 and the terminal is already shared. Noa declines the proposed use of the hall.', 'The shared terminal settles access there. Without P2, neither booking meets our present priority.');
  }
  if (appeal === 'pressure') {
    if (government) return result('G', 'Government P2 outweighs Youth’s demand.', 'Noa refuses Youth’s exclusive claim and chooses Government’s P2 case. Cooperation can coexist with an unequal terminal rule; it does not establish fairness.', 'You cannot claim the neighborhood. Ari has P2 in his plan, and we choose his forum for this booking.');
    if (youth) return result('Y', 'Only Youth has P2; its affordability case outweighs the demand.', 'Noa rejects the claim of ownership but chooses the only P2 plan. The request itself has no force.', 'Your demand does not own our hall. Your P2 commitment is why we choose to help this time.');
    return result('W', 'Neither plan has P2.', 'Noa declines the demand because neither plan offers P2. Both units stay unused.', 'Neither plan answers the low-income fare issue. The hall will remain unused.');
  }
  if (youth && government) return result('S', 'Both plans have P2; share independent access.', 'Both plans include P2. Noa divides the hall’s contacts between the campaigns.', 'Both of you have made the low-income pass commitment. We choose to share our hall and contacts.');
  if (youth) return result('Y', 'Only Youth has P2.', 'Only Youth publishes P2. Noa chooses Youth cooperation.', 'The low-income pass is in your plan. We choose to bring both of our units to your forum.');
  if (government && terminal === 'exclusive') return result('S', 'Government P2 and a need to preserve challenger access yield S.', 'Government alone offers P2, but the terminal excludes Youth. Noa balances affordability with independent access by sharing the hall rather than obeying the order to send everyone to the official forum.', 'Ari’s P2 matters. So does having somewhere else to meet. We will share this hall; that choice is ours.');
  if (government) return result('G', 'Only Government has P2; terminal access is already shared.', 'Government alone offers P2 and the terminal is open to both forums. Noa gives Government both independent units.', 'The terminal is open to both now. For our own booking, Ari’s P2 earns the hall and contacts.');
  return result('W', 'Neither plan has P2.', 'Neither plan offers P2. Noa leaves both neighborhood units unused.', 'Neither offer includes our fare priority. We will keep the hall unused.');
}

function resolveOwner(state, actor, action, governmentPlan, terminal) {
  const context = {
    youth: action.plan.includes(actor.priority),
    government: governmentPlan.includes(actor.priority),
    youthPlan: action.plan,
    governmentPlan,
    terminal,
    appeal: action.appeals[actor.id],
    priorPressure: state.history.at(-1)?.action.appeals[actor.id] === 'pressure'
  };
  const resolver = { transit: transitDecision, union: unionDecision, shift: shiftDecision, neighborhood: neighborhoodDecision }[actor.id];
  const decision = resolver(context);
  const prior = state.history.at(-1);
  const withdrawal = supportedThenWithdrawn(state, actor);
  if (decision.choice === 'Y' && withdrawal && context.youth && !prior?.youthPlan.includes(actor.priority)) {
    return result('S', `${decision.rule} Local restoration reservation changes Y to S.`,
      `${decision.reason} However, ${actor.person} contributed when Youth offered ${actor.priority} in round ${withdrawal.supportedRound}, before Youth withdrew it in round ${withdrawal.withdrawnRound}. Restoring it earns shared, rather than exclusive Youth, cooperation for this round.`,
      `You have restored ${actor.priority} after withdrawing the commitment we worked with. We will share this round; we are not returning to exclusive backing yet.`);
  }
  return decision;
}

function validateAllocationChoices(allocations) {
  exactKeys(allocations, OWNER_IDS, 'Allocations');
  const choices = {};
  for (const id of OWNER_IDS) {
    const choice = typeof allocations[id] === 'string' ? allocations[id] : allocations[id]?.choice;
    if (!ALLOCATION_IDS.includes(choice)) fail(`Invalid allocation for ${id}. Use G, Y, S, or W.`);
    choices[id] = choice;
  }
  return choices;
}

/** Accepts (terminal, allocations) or an object with those two properties. */
function calculateReach(terminalOrSnapshot, allocationsArgument) {
  const terminal = typeof terminalOrSnapshot === 'string' ? terminalOrSnapshot : terminalOrSnapshot?.terminal;
  const allocations = allocationsArgument ?? terminalOrSnapshot?.allocations;
  if (!['exclusive', 'shared'].includes(terminal)) fail('Terminal access must be exclusive or shared.');
  const choices = validateAllocationChoices(allocations);
  let G = terminal === 'exclusive' ? 3 : 2;
  let Y = terminal === 'exclusive' ? 1 : 2;
  let unused = 0;
  for (const id of OWNER_IDS) {
    if (choices[id] === 'G') G += 2;
    if (choices[id] === 'Y') Y += 2;
    if (choices[id] === 'S') { G += 1; Y += 1; }
    if (choices[id] === 'W') unused += 2;
  }
  return freeze({ G, Y, unused });
}

function calculateSeconds(reach) {
  return { G: 20 + 5 * reach.G, Y: 20 + 5 * reach.Y };
}

function forumDescriptions(terminal, allocations, reach, seconds) {
  const forums = {};
  for (const campaign of ['G', 'Y']) {
    const hasTerminal = campaign === 'G' || terminal === 'shared';
    const contributors = OWNER_IDS.filter(id => [campaign, 'S'].includes(allocations[id].choice));
    const names = contributors.map(id => ACTOR_BY_ID[id].name);
    forums[campaign] = {
      title: campaign === 'G' ? 'Official transport forum' : 'Youth transport forum',
      canOperate: true,
      baseline: 1,
      terminal: hasTerminal,
      contributors,
      reach: reach[campaign],
      seconds: seconds[campaign],
      description: `${campaign === 'G' ? 'Party supporters' : 'Youth volunteers'} retain a small forum. ${hasTerminal ? 'The public terminal also supplies speaking opportunity.' : 'The public terminal is unavailable to this forum.'} ${names.length ? `Independent cooperation comes from ${names.join(', ')}.` : 'No independent organization contributes to this forum.'}`
    };
  }
  return forums;
}

function resolveUnchecked(state, action) {
  const government = currentGovernment(state);
  const access = accessDecision(state, action.requestShared);
  const allocations = Object.fromEntries(ACTORS.map(actor => [actor.id, resolveOwner(state, actor, action, government.plan, access.terminal)]));
  const reach = calculateReach(access.terminal, allocations);
  const seconds = calculateSeconds(reach);
  const prior = state.history.at(-1);
  const events = [government.reason, access.reason];
  for (const actor of ACTORS) {
    const choice = allocations[actor.id].choice;
    const previous = prior?.allocations[actor.id].choice;
    events.push(`${actor.name} chooses ${choice}${previous ? ` (previously ${previous})` : ''}. ${allocations[actor.id].reason}`);
  }
  events.push(`Reach: Government ${reach.G}, Youth ${reach.Y}, unused ${reach.unused}. Pitch limits: Government ${seconds.G} seconds; Youth ${seconds.Y} seconds. No ballots are inferred.`);
  const snapshot = {
    round: state.round,
    action: copy(action),
    governmentPlan: copy(government.plan),
    youthPlan: copy(action.plan),
    terminal: access.terminal,
    terminalReason: access.reason,
    allocations,
    reach: copy(reach),
    seconds,
    events,
    npcRules: ACTORS.map(actor => `${actor.person}: ${allocations[actor.id].rule}`),
    forum: forumDescriptions(access.terminal, allocations, reach, seconds)
  };
  return freeze({
    schemaVersion: SCHEMA_VERSION,
    round: state.round + 1,
    history: [...copy(state.history), snapshot],
    lastPlan: copy(action.plan),
    scenario: state.scenario
  });
}

function stableStringify(value) {
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
  if (plainObject(value)) return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
  return JSON.stringify(value);
}

/**
 * Returns true, or throws. Replays every saved action before accepting the state.
 * A caller can validate a whole imported payload before replacing current work.
 */
function validateState(state) {
  exactKeys(state, ['schemaVersion', 'round', 'history', 'lastPlan', 'scenario'], 'Engine state');
  if (state.schemaVersion !== SCHEMA_VERSION || state.scenario !== 'standard') fail('Unsupported engine state version or scenario.');
  if (!Array.isArray(state.history) || state.history.length > 3 || state.round !== state.history.length + 1) fail('Engine round does not match its history.');
  normalizePlan(state.lastPlan, 'Last plan');
  let replay = createState(state.scenario);
  for (const snapshot of state.history) {
    if (!plainObject(snapshot) || !snapshot.action) fail('Every saved reveal must contain its sealed action.');
    replay = resolveUnchecked(replay, normalizeAction(snapshot.action));
  }
  if (stableStringify(replay) !== stableStringify(state)) fail('Saved engine state does not match its deterministic action history.');
  return true;
}

/** Seals one complete decision and returns a new, deeply frozen state. */
function resolveRound(state, decision) {
  validateState(state);
  if (state.round > 3) fail('All three rounds are already sealed. Start a new run to make different decisions.');
  const action = normalizeAction(decision);
  return resolveUnchecked(state, action);
}

/**
 * One controlled access comparison. No organization, plan, appeal, speech, or
 * ballot is replayed. The original snapshot and its history remain untouched.
 */
function compareAccess(snapshot) {
  if (!plainObject(snapshot)) fail('An actual reveal snapshot is required.');
  normalizePlan(snapshot.governmentPlan, 'Government plan');
  normalizePlan(snapshot.youthPlan, 'Youth plan');
  const originalReach = calculateReach(snapshot);
  const originalSeconds = calculateSeconds(originalReach);
  if (snapshot.reach && stableStringify(snapshot.reach) !== stableStringify(originalReach)) fail('Snapshot reach does not match its owned resources.');
  if (snapshot.seconds && stableStringify(snapshot.seconds) !== stableStringify(originalSeconds)) fail('Snapshot speaking time does not match its reach.');
  const compared = copy(snapshot);
  compared.terminal = snapshot.terminal === 'exclusive' ? 'shared' : 'exclusive';
  compared.reach = copy(calculateReach(compared.terminal, compared.allocations));
  compared.seconds = calculateSeconds(compared.reach);
  compared.forum = forumDescriptions(compared.terminal, compared.allocations, compared.reach, compared.seconds);
  compared.counterfactual = true;
  compared.terminalReason = 'Controlled comparison only: terminal access changes; both plans and every independent allocation remain fixed. Ari and the organizations are not asked to decide again.';
  compared.comparison = {
    originalTerminal: snapshot.terminal,
    deltaReach: { G: compared.reach.G - originalReach.G, Y: compared.reach.Y - originalReach.Y },
    deltaSeconds: { G: compared.seconds.G - originalSeconds.G, Y: compared.seconds.Y - originalSeconds.Y },
    heldFixed: ['governmentPlan', 'youthPlan', 'allocations'],
    notReplayed: ['bargaining', 'organization decisions', 'speeches', 'ballots'],
    limit: 'This compares speaking opportunity only. It cannot estimate votes, a winner, learning, or the fairness of a whole political system.'
  };
  return freeze(compared);
}

return {POLICIES,ACTORS,createState,getRoundBrief,resolveRound,calculateReach,compareAccess,validateState};})();
const Core=(()=>{
const SCHEMA='order-given-game/1';
const STAGES=['welcome','intro','plan','reveal','forum','compare','revise','transfer','report'];
const IDS=['transit','union','shift','neighborhood'];
const WORD_LIMIT=16000;
const words=t=>String(t||'').trim().split(/\s+/u).filter(Boolean).length;
const prompt=id=>CONTENT.prompts.find(p=>p.id===id);
const contentKey={transit:'coop',union:'students',shift:'workers',neighborhood:'neighbors'};
const planTitle=ids=>Engine.POLICIES.filter(p=>ids.includes(p.id)).map(p=>p.id+' '+p.title).join(' + ');
function evidencePool(s){
 const list=[{id:'notice',label:'Terminal office notice',text:CONTENT.opening.order},{id:'lydora-context',label:'Political background · authoritarian Lydora',text:CONTENT.setting.paragraphs.join(' ')}];
 for(const a of Engine.ACTORS){const c=CONTENT.characters[contentKey[a.id]];list.push({id:'agenda-'+a.id,label:a.person+' · stated priority',text:c.priority+'. '+c.questions[0].a});}
 let prior=Engine.createState();
 for(const snap of s.engine.history){
  const brief=Engine.getRoundBrief(prior);
  list.push({id:'round-'+snap.round+'-plans',label:'Round '+snap.round+' · campaign plans at commitment',text:'Government: '+planTitle(snap.governmentPlan)+'. Youth: '+planTitle(snap.youthPlan)+'. Terminal: '+brief.terminalRule});
  list.push({id:'round-'+snap.round+'-terminal',label:'Round '+snap.round+' · public terminal',text:snap.terminalReason});
  for(const a of Engine.ACTORS){const x=snap.allocations[a.id];list.push({id:'round-'+snap.round+'-'+a.id,label:'Round '+snap.round+' · '+a.person+' reply',text:({G:'Government',Y:'Youth',S:'Share one each',W:'Keep both unused'}[x.choice])+'. '+x.reason+' '+x.quote});}
  prior=Engine.resolveRound(prior,snap.action);
 }
 if(s.engine.round<=3){const b=Engine.getRoundBrief(s.engine);list.push({id:'round-'+b.round+'-plans',label:'Round '+b.round+' · campaign plans at commitment',text:'Government: '+planTitle(b.governmentPlan)+'. Youth: '+planTitle(s.draft.plan)+'. Terminal: '+b.terminalRule});}
 return list;
}
function newSession(){return {schema:SCHEMA,created:new Date().toISOString(),updated:new Date().toISOString(),mode:'class',identity:{name:'',course:'',code:''},screen:'welcome',engine:Engine.createState(),notes:Object.fromEntries(CONTENT.prompts.map(p=>[p.id,''])),draft:{plan:['P1','P3'],appeals:Object.fromEntries(IDS.map(id=>[id,'service'])),requestShared:false,target:'union',prediction:'Y',confidence:'some',sources:[]},forecasts:[],explainSources:[],citizen:'',comparison:{revealed:false,G:'',Y:'',laterG:'',laterY:'',originalNote:'',at:''},revisionRound:3,makeup:{},funExposure:false};}
const plain=x=>x&&typeof x==='object'&&!Array.isArray(x)&&Object.getPrototypeOf(x)===Object.prototype;
const bad=m=>{throw new Error(m)};
function text(x,label,max=WORD_LIMIT){if(typeof x!=='string'||x.length>max)bad('Invalid '+label);return x;}
function member(x,values,label){if(!values.includes(x))bad('Invalid '+label);return x;}
function sourceArray(x,pool){if(!Array.isArray(x)||x.length>80)bad('Invalid evidence list');if(new Set(x.map(e=>e?.id)).size!==x.length)bad('Duplicate evidence references');for(const e of x){if(!plain(e))bad('Invalid evidence');text(e.id,'evidence ID',200);text(e.label,'evidence label',500);text(e.text,'evidence text',5000);if(e.retrospective!==undefined&&typeof e.retrospective!=='boolean')bad('Invalid evidence timing flag');if(pool){const actual=pool.find(a=>a.id===e.id);if(!actual||actual.label!==e.label||actual.text!==e.text)bad('Evidence does not match an available source.');}}return x;}
function validateSession(input){
 if(!plain(input)||input.schema!==SCHEMA)bad('This is not a compatible Order Was Given backup. Your current game has not changed.');
 if(new TextEncoder().encode(JSON.stringify(input)).length>4*1024*1024)bad('This backup is too large.');
 const s=structuredClone(input);Engine.validateState(s.engine);member(s.mode,['class','fun'],'mode');member(s.screen,STAGES,'screen');
 if(!plain(s.identity)||!plain(s.notes)||!plain(s.draft)||!plain(s.makeup)||!plain(s.comparison))bad('The backup is incomplete.');
 for(const k of ['name','course','code'])text(s.identity[k],k,200);
 for(const k of ['created','updated']){text(s[k],k,80);if(Number.isNaN(Date.parse(s[k])))bad('Invalid save date');}
 for(const p of CONTENT.prompts){text(s.notes[p.id],p.id);if(s.makeup[p.id]!==undefined)text(s.makeup[p.id],p.id+' later response');}
 if(Object.keys(s.makeup).some(k=>!CONTENT.prompts.some(p=>p.id===k)))bad('Unknown response ID');
 if(!Array.isArray(s.draft.plan)||s.draft.plan.length>2||new Set(s.draft.plan).size!==s.draft.plan.length||s.draft.plan.some(p=>!Engine.POLICIES.some(a=>a.id===p)))bad('Invalid policy plan');
 if(!plain(s.draft.appeals))bad('Invalid approaches');for(const id of IDS)member(s.draft.appeals[id],['service','shared','pressure'],'approach');
 if(typeof s.draft.requestShared!=='boolean'||typeof s.funExposure!=='boolean')bad('Invalid choice flag');
 member(s.draft.target,IDS,'forecast target');member(s.draft.prediction,['G','Y','S','W'],'forecast');member(s.draft.confidence,['low','some','high'],'confidence');const available=evidencePool(s);sourceArray(s.draft.sources,available);sourceArray(s.explainSources,available);
 if(!Array.isArray(s.forecasts)||s.forecasts.length!==s.engine.history.length)bad('Forecast and round history do not match.');
 let before=Engine.createState();
 s.forecasts.forEach((f,i)=>{if(!plain(f)||f.round!==i+1)bad('Invalid forecast order');member(f.target,IDS,'forecast target');member(f.prediction,['G','Y','S','W'],'forecast choice');member(f.confidence,['low','some','high'],'confidence');text(f.note,'forecast note');sourceArray(f.sources,evidencePool({engine:before,draft:{plan:s.engine.history[i].youthPlan}}));if(f.laterSources!==undefined)sourceArray(f.laterSources,available);text(f.at,'forecast date',80);if(Number.isNaN(Date.parse(f.at)))bad('Invalid forecast date');if(typeof f.skipped!=='boolean')bad('Invalid forecast status');if(f.note!==s.notes['forecast'+(i+1)])bad('Forecast text does not match its locked response.');before=Engine.resolveRound(before,s.engine.history[i].action);});
 member(s.citizen,['','G','Y','A'],'citizen choice');if(typeof s.comparison.revealed!=='boolean')bad('Invalid comparison');text(s.comparison.originalNote,'comparison note');text(s.comparison.at,'comparison date',80);for(const k of ['G','Y']){text(s.comparison[k],'comparison prediction',5);if(!/^\d{0,2}$/.test(s.comparison[k])||(s.comparison.revealed&&s.comparison[k]!==''&&!/^(?:[0-9]|1[0-2])$/.test(s.comparison[k])))bad('Invalid predicted reach');if(s.comparison['later'+k]===undefined)s.comparison['later'+k]='';text(s.comparison['later'+k],'later comparison prediction',5);if(!/^\d{0,2}$/.test(s.comparison['later'+k]))bad('Invalid later reach prediction');}
 if(s.comparison.revealed&&s.comparison.originalNote!==s.notes.compare)bad('Comparison text does not match the locked prediction.');
 if(s.comparison.revealed&&Number.isNaN(Date.parse(s.comparison.at)))bad('Invalid comparison date');
 if(![1,2,3].includes(s.revisionRound))bad('Invalid revision round');
 const n=s.engine.history.length;if(n<3&&['forum','compare','revise','transfer','report'].includes(s.screen))bad('This screen has no completed final round.');if(['welcome','intro'].includes(s.screen)&&n!==0)bad('Invalid opening state');if(s.screen==='plan'&&n>=3)bad('The negotiation has already ended.');if(s.screen==='reveal'&&n===0)bad('No round to reveal.');if(s.comparison.revealed&&n!==3)bad('No final comparison exists.');if(['revise','transfer','report'].includes(s.screen)&&!s.comparison.revealed)bad('The access comparison has not happened.');
 return s;
}
function responseFor(s,id){return s.makeup[id]||s.notes[id]||'';}
function requirements(s){
 const missing=[];if(!s.identity.name.trim())missing.push({id:'name',label:'Your name'});
 if(s.engine.history.length!==3)missing.push({id:'rounds',label:'All three negotiation rounds'});
 for(const p of CONTENT.prompts){if(words(responseFor(s,p.id))<p.minWords)missing.push({id:p.id,label:p.label+' ('+p.minWords+' words minimum)'});}
 for(let i=0;i<3;i++){const f=s.forecasts[i];if(!f||(!f.sources.length&&!f.laterSources?.length))missing.push({id:'evidence'+(i+1),label:'Round '+(i+1)+' forecast evidence'});}
 if(!s.explainSources.some(e=>/^round-[1-3]-terminal$/.test(e.id))||!s.explainSources.some(e=>/^round-[1-3]-(transit|union|shift|neighborhood)$/.test(e.id)))missing.push({id:'explainEvidence',label:'One revealed terminal decision and one organization’s revealed reply'});
 if(!s.citizen)missing.push({id:'citizen',label:'Your fictional citizen choice (abstaining is allowed)'});
 if(!s.comparison.revealed)missing.push({id:'comparisonReveal',label:'The controlled access comparison'});
 if(['G','Y'].some(k=>!/^(?:[0-9]|1[0-2])$/.test(s.comparison[k])&&!/^(?:[0-9]|1[0-2])$/.test(s.comparison['later'+k]||'')))missing.push({id:'accessNumbers',label:'Both predicted reach totals for the access comparison'});
 return missing;
}
function reportData(s){return {title:CONTENT.title,version:'1.3',generated:new Date().toISOString(),mode:s.mode,name:s.identity.name||'Name not supplied',course:s.identity.course,code:s.identity.code,classComplete:s.mode==='class'&&requirements(s).length===0,missing:requirements(s),earlierExploration:s.funExposure,created:s.created,updated:s.updated,notes:CONTENT.prompts.map(p=>({id:p.id,label:p.label,prompt:p.prompt,original:s.notes[p.id],later:s.makeup[p.id]||'',minimum:p.minWords,target:p.targetWords})),forecasts:structuredClone(s.forecasts),history:structuredClone(s.engine.history),explainSources:structuredClone(s.explainSources),citizen:s.citizen,comparison:structuredClone(s.comparison),comparisonActual:s.engine.history.length===3&&s.comparison.revealed?Engine.compareAccess(s.engine.history[2]):null,revisionRound:s.revisionRound,readingQuestion:CONTENT.readingQuestion};}

return {SCHEMA,STAGES,IDS,WORD_LIMIT,words,prompt,evidencePool,newSession,validateSession,responseFor,requirements,reportData};})();
(()=>{
'use strict';
const $=id=>document.getElementById(id), C=CONTENT, E=Engine, K=Core;
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const slot=(new URL(location.href).searchParams.get('slot')||'main').replace(/[^a-zA-Z0-9_-]/g,'').slice(0,40);
const key='yoav-duman-order-given-v1:'+slot;
const aliases={transit:'coop',union:'students',shift:'workers',neighborhood:'neighbors'};
const ci={transit:2,union:3,shift:4,neighborhood:5};
const names={G:'Government',Y:'Youth',S:'Share one each',W:'Keep both unused'};
let s=K.newSession(),ui={person:'union',error:'',saveError:false,saved:false,rawRecovery:'',dialogFocus:null};
try{const raw=localStorage.getItem(key);if(raw){try{s=K.validateSession(JSON.parse(raw));ui.saved=true;}catch(e){ui.rawRecovery=raw;ui.saveError=true;ui.error='Your saved game could not reopen. Autosave is paused to protect it. Open Save & files to download the unopened data, restore a backup, or explicitly start a new run.';}}}catch(e){ui.saveError=true;}
const actor=id=>E.ACTORS.find(a=>a.id===id), character=id=>C.characters[aliases[id]], policy=id=>E.POLICIES.find(p=>p.id===id);
const titlePlan=ids=>E.POLICIES.filter(p=>ids.includes(p.id)).map(p=>p.id+' '+p.title).join(' + ');
const portrait=(n,large=false)=>`<div class="portrait${large?' large':''}" aria-hidden="true" style="--pos:${n*20}%"></div>`;
function say(t){$('live').textContent=t;}
function save(){s.updated=new Date().toISOString();if(ui.rawRecovery){ui.saveError=true;const status=$('saveStatus');if(status)status.textContent='Saving paused: recover saved data';return;}try{localStorage.setItem(key,JSON.stringify(s));ui.saveError=false;ui.saved=true;}catch(e){ui.saveError=true;}const el=$('saveStatus');if(el)el.textContent=ui.saveError?'Use a backup to keep this game':'Saved on this device';}
function button(label,action,cls='',attrs=''){return `<button type="button" class="${cls}" data-action="${action}" ${attrs}>${label}</button>`;}
function credit(){return '<p class="credit">Created by Yoav Duman / <bdi lang="he" dir="rtl">יואב דומן</bdi></p>';}
function settingHtml(){return `<section class="political-setting"><p class="eyebrow">Lydora’s political order</p><h3>${esc(C.setting.title)}</h3><div class="setting-facts">${C.setting.paragraphs.map((p,i)=>`<section><h4>${esc(C.setting.headings[i])}</h4><p>${esc(p)}</p></section>`).join('')}</div></section>`;}
function hint(){return `<p class="small">${s.mode==='class'?'Class checks that choices, evidence, and short responses are present. Writing is recorded, not graded.':'Just for Fun: writing is optional. Choices and consequences stay the same.'}</p>`;}
function response(id,options={}){const p=K.prompt(id),forecast=id.startsWith('forecast'),who=forecast?actor(s.draft.target):null,val=options.value??s.notes[id],readonly=options.readonly||false;return `<div class="response"><label for="note-${id}">${esc(p.label)}${who?` · ${esc(who.person)} · ${esc(who.name)}`:''}</label>${id==='revise'?`<ol class="response-checklist" id="prompt-${id}"><li>Use the access-only result and one earlier forecast to defend, narrow, or change your explanation.</li><li>State what remains unknown about voters’ choices.</li><li>Explain whether either campaign winning would settle whether tonight’s access was fair.</li></ol>`:`<p class="small" id="prompt-${id}">${esc(p.prompt)}</p>`}<textarea maxlength="${K.WORD_LIMIT}" id="note-${id}" data-note="${id}" ${readonly?'readonly':''} aria-describedby="prompt-${id} count-${id}" placeholder="${id==='transfer'?'If… then… / If instead…':id.startsWith('forecast')?'I expect… because… One uncertainty is…':'Use a concrete choice or consequence.'}">${esc(val)}</textarea><p class="count ${K.words(val)>=p.minWords?'ready':''}" id="count-${id}">${K.words(val)} words. Suggested about ${p.targetWords}; you may write more. Class minimum ${p.minWords}. ${readonly?'Original response kept.':'Recorded, not graded.'}</p></div>`;}
function stageName(){if(s.screen==='plan')return 'Round '+s.engine.round+' of 3';if(s.screen==='reveal')return 'Round '+s.engine.history.length+' replies';return {welcome:'Welcome',intro:'Your first intention',forum:'The forums',compare:'One rule changes',revise:'Reconsider',transfer:'A different town',report:'Your evidence report'}[s.screen];}
function progress(){const groups=[['Arrive',['welcome','intro']],['Negotiate',['plan','reveal']],['Forum',['forum']],['Compare',['compare','revise']],['Transfer',['transfer','report']]];return `<nav class="progress" aria-label="Game progress"><span class="setting-cue">${s.screen==='transfer'?'New town · institutions unknown':'Authoritarian Lydora'}</span>${groups.map(([t,ids])=>`<span class="${ids.includes(s.screen)?'current':''}" ${ids.includes(s.screen)?'aria-current="step"':''}>${t}</span>`).join('<span aria-hidden="true">/</span>')}<span>${s.mode==='class'?'For Class':'Just for Fun'}</span></nav>`;}
function header(){return `<header class="topbar"><div><span class="brand">The Order Was Given</span> <span id="saveStatus" class="saved">${ui.rawRecovery?'Saving paused: recover saved data':ui.saveError?'Use a backup to keep this game':ui.saved?'Saved on this device':'Ready to save on this device'}</span></div><div class="tools">${button('Setting','setting')}${button('Evidence','archive')}${button('Help & terms','help')}${button('Save & files','files')}${button(s.mode==='class'?'Class mode':'Fun mode','mode','','id="mode-control"')}${button('Facilitator','facilitator')}</div></header>${ui.rawRecovery?'<div class="save-warning" role="status">Autosave is paused to protect unopened saved data. Use Save &amp; files to recover it before replacing this run. New edits are only in memory until you choose a new run or restore a backup.</div>':ui.saveError?'<div class="save-warning" role="status">This browser could not save locally. Download a backup before closing or moving devices.</div>':''}`;}
function render(focus=true){const active=document.activeElement,aid=active?.id,selection=active?.selectionStart;let body='';switch(s.screen){case'welcome':body=welcome();break;case'intro':body=intro();break;case'plan':body=plan();break;case'reveal':body=reveal();break;case'forum':body=forum();break;case'compare':body=compare();break;case'revise':body=revise();break;case'transfer':body=transfer();break;case'report':body=report();break;}
 $('app').innerHTML=header()+(s.screen==='welcome'?body:`<main class="shell" id="main" tabindex="-1">${progress()}${ui.error?`<div class="error" role="alert">${esc(ui.error)}</div>`:''}${body}</main>`);
 if(!focus&&aid&&$(aid)){const el=$(aid);el.focus({preventScroll:true});if(typeof selection==='number'&&el.setSelectionRange)try{el.setSelectionRange(selection,selection);}catch(e){}}
 else if(focus){window.scrollTo({top:0,behavior:'instant'});$('main')?.focus({preventScroll:true});}say(stageName());}
function welcome(){return `<section class="hero" aria-label="Campaign room in Lydora"><div class="hero-copy"><p class="eyebrow">Lydora · Elections under authoritarian rule</p><h1>The order<br>was given</h1><p>Four independent organizations.<br>Two public promises.<br>Their choices are still theirs.</p></div></section><main class="landing" id="main" tabindex="-1"><div class="entry-grid"><div><p class="eyebrow">Your seat at the table</p><h2>Make your case.<br>Then hear theirs.</h2><p>Lydora’s ruling party controls major state institutions and heavily tilts elections. Opposition groups still organize and compete for genuine support.</p><p>Play Sasha, organizer of the legal Youth opposition campaign. Build a credible public case under unequal rules. Shape three rounds of offers and see which people cooperate, share, or hold back. Then change one access rule and test your explanation.</p><p class="muted">A thoughtful route is designed for 50–60 minutes. No countdown, speed score, or simulated election winner. All people and events are fictional.</p><p class="small">The game works offline after download. No AI account or service is needed.</p></div><div><div class="mode-options" role="group" aria-label="Choose how to play">${button('For Class','choose-class',s.mode==='class'?'selected':'',`id="route-class" aria-pressed="${s.mode==='class'}"`)}${button('Just for Fun','choose-fun',s.mode==='fun'?'selected':'',`id="route-fun" aria-pressed="${s.mode==='fun'}"`)}</div><p class="small">${esc(C.opening.modeNote)}</p><div class="identity"><label>Your name${s.mode==='class'?' (required)':' (optional)'}<input id="name" data-identity="name" type="text" maxlength="200" autocomplete="name" value="${esc(s.identity.name)}"></label><label>Course / section (optional)<input id="course" data-identity="course" type="text" maxlength="200" value="${esc(s.identity.course)}"></label><label>Class code (optional)<input id="code" data-identity="code" type="text" maxlength="200" value="${esc(s.identity.code)}"></label></div><p class="small">A class code labels your report. It does not send your work anywhere. Your game stays in this browser until you export it.</p>${ui.error?`<p class="error" role="alert">${esc(ui.error)}</p>`:''}${button('Take your seat','start','primary wide-action')}<div class="action-row">${button('Restore a saved game','restore','quiet')}${button('Save & files','files','quiet')}</div></div></div>${credit()}</main>`;}
function intro(){return `<div class="heading-row"><div class="brief"><p class="eyebrow">${esc(C.opening.eyebrow)}</p><h2>Your invitations<br>are unanswered</h2></div><div class="status-stamp">Election ahead<br>Forum tonight</div></div><div class="mission"><p class="eyebrow">Your role and mission</p><p><strong>You are Sasha, organizer of Youth, a legal opposition campaign.</strong> Ari represents the ruling Unity Party. Make your campaign’s case in public under unequal rules: publish two promises, seek cooperation, and explain what your choices actually change.</p></div>${settingHtml()}<div class="intro-grid"><div>${C.opening.paragraphs.map(p=>`<p>${esc(p)}</p>`).join('')}<div class="directive"><p class="tag attribution">Terminal office · Ari Velen</p><blockquote>${esc(C.opening.order)}</blockquote></div></div><div><div class="rules"><h3>Your first tools</h3><p class="small">A <strong>forum</strong> is a public campaign meeting. The <strong>terminal</strong> is the central bus station. A request, also called an appeal, is how you ask an organization to cooperate.</p><ul><li>You control Youth's volunteers and exactly two published policy promises.</li><li>Ari controls the public terminal. A request asks him to change his decision.</li><li>Four organizations decide how to use their own resources. Their support does not supply private votes.</li><li><strong>A reach unit is a simplified resource contribution, not a person or a vote.</strong> Each unit adds five speaking seconds to a 20-second base. Baseline volunteers give each campaign one unit. The model represents different ways of reaching an audience with this common measure of speaking time. For example, three units give 20 + 15 = 35 seconds.</li></ul><details class="word-help"><summary>Quick word help: incumbent, co-op, reimbursement</summary>${wordHelp()}</details></div>${response('opening')}${hint()}${button('Open the negotiation table','begin','primary wide-action')}</div></div>`;}
function availableEvidence(){return K.evidencePool(s);}
function evidenceLabel(e){const match=/^round-(\d+)-(transit|union|shift|neighborhood)$/.exec(e.id),id=e.id.startsWith('agenda-')?e.id.slice(7):match?.[2],a=id?actor(id):null;if(a)return match?`Round ${match[1]} · ${a.person} · ${a.name} reply`:`${a.person} · ${a.name} · ${a.priority} priority`;if(e.id==='round-'+s.engine.round+'-plans'&&s.screen==='plan')return 'Round '+s.engine.round+' · current plan preview (saved when sealed)';return e.label;}
function evidenceOptions(list){const groups=new Map();for(const e of list){const round=/^round-(\d+)-/.exec(e.id);const group=round?'Round '+round[1]:e.id.startsWith('agenda-')?'People and priorities':'Setting and terminal notice';if(!groups.has(group))groups.set(group,[]);groups.get(group).push(e);}return [...groups].map(([label,items])=>`<optgroup label="${esc(label)}">${items.map(e=>`<option value="${esc(e.id)}">${esc(evidenceLabel(e))}</option>`).join('')}</optgroup>`).join('');}
function evidencePicker(selected,scope,minimum=1){const list=availableEvidence(),typed=scope==='explain',terminal=list.filter(e=>/^round-[1-3]-terminal$/.test(e.id)),replies=list.filter(e=>/^round-[1-3]-(transit|union|shift|neighborhood)$/.test(e.id));return `<div class="field"><h3>${typed?'Evidence for your comparison':'Evidence for your forecast'}</h3><p class="small" id="evidence-help-${scope}">${typed?'Add one revealed terminal decision and one organization’s revealed reply. Other background remains available in Evidence.':'Choose a source that helps predict '+actor(s.draft.target).person+'’s reply.'} Choosing adds the item immediately and shows its text below. Use Remove to change it.</p>${typed?`<label for="evidence-explain">1. Revealed terminal decision</label><select id="evidence-explain" data-evidence="explain" aria-describedby="evidence-help-explain"><option value="">Choose a terminal decision…</option>${evidenceOptions(terminal)}</select><label class="second-source" for="evidence-explain-owner">2. Organization’s revealed reply</label><select id="evidence-explain-owner" data-evidence="explain" aria-describedby="evidence-help-explain"><option value="">Choose an organization’s reply…</option>${evidenceOptions(replies)}</select>`:`<label for="evidence-${scope}">Add evidence for your forecast</label><select id="evidence-${scope}" data-evidence="${scope}" aria-describedby="evidence-help-${scope}"><option value="">Choose a visible source…</option>${evidenceOptions(list)}</select>`}${selected.map((e,i)=>`<div class="evidence"><div class="heading-row"><strong>${e.retrospective?'Later evidence · ':''}${esc(evidenceLabel(e))}</strong>${button('Remove','remove-source','quiet',`data-scope="${scope}" data-index="${i}" aria-label="Remove ${esc(evidenceLabel(e))}"`)}</div><p>${esc(e.text)}</p></div>`).join('')}<p class="small">${selected.length} attached. ${typed?`Terminal decision: ${selected.some(e=>/^round-[1-3]-terminal$/.test(e.id))?'added':'needed'}. Organization reply: ${selected.some(e=>/^round-[1-3]-(transit|union|shift|neighborhood)$/.test(e.id))?'added':'needed'}. Class requires one from each category.`:'Class requires '+minimum+' source.'} References are checked for presence and type; your instructor assesses the reasoning.</p></div>`;}
function wordHelp(){return '<dl class="terms"><dt>Incumbent</dt><dd>The party or leader already in office.</dd><dt>Co-op</dt><dd>A cooperative: an organization owned by its members.</dd><dt>Reimbursement</dt><dd>Paying back costs. Here P4 helps bus operators cover fare reductions while protecting routes and pay.</dd><dt>Provisional</dt><dd>Not final; an arrangement can change in a later round.</dd><dt>Executive</dt><dd>The top government leadership, including the leader and ministers.</dd><dt>Election authority</dt><dd>The institution responsible for administering elections.</dd><dt>Tie-break</dt><dd>A condition that decides between two otherwise equal offers.</dd><dt>Resource decision / allocation</dt><dd>How an organization chooses to use its room, contacts, or vehicles.</dd></dl>';}
function decisionHelp(a,b){const request=s.draft.appeals[a.id],all=C.ruleHelp?.[a.id]||{},lines=all[request]||[],reservation=b.historyReservations?.[a.id],prior=s.engine.history.at(-1),returning=reservation&&prior&&!prior.youthPlan.includes(a.priority)&&s.draft.plan.includes(a.priority);const readable=t=>t.replace('The return-after-withdrawal rule below can change this to one unit for each campaign.','Earlier promise changes can make this a shared reply; see the history condition below.').replace('If that campaign is Youth, the return-after-withdrawal rule below can change this to one unit for each campaign.','If that campaign is Youth, apply the history condition below.');return `<details class="archive"><summary>How this fictional person decides</summary><p class="small">These are fictional model conditions. Your current request is <strong>${esc(character(a.id).appeals[request].label)}</strong>.</p>${returning?`<p class="history-cue"><strong>Read this history condition first · ${esc(a.person)}</strong><br>${esc(a.person)} helped Youth with ${esc(a.priority)} in round ${reservation.supportedRound}; you removed that promise in round ${reservation.withdrawnRound} and have now restored it. For this return round, a rule below that would give Youth both units instead gives one unit to each campaign. Government-only, unused, and already-shared replies stay unchanged.</p>`:''}<ul class="decision-help">${lines.map(t=>`<li>${esc(readable(t))}</li>`).join('')}</ul><details class="model-detail"><summary>Other requests and the full history condition</summary><h4>When an earlier promise is restored</h4><ol class="decision-help">${C.ruleMemorySteps.map(t=>`<li>${esc(t)}</li>`).join('')}</ol>${Object.entries(all).filter(([kind])=>kind!==request).map(([kind,items])=>`<h4>${esc(character(a.id).appeals[kind].label)}</h4><ul class="decision-help">${items.map(t=>`<li>${esc(readable(t))}</li>`).join('')}</ul>`).join('')}</details></details>`;}

function terminalOffer(b){if(b.terminal==='shared')return '<p class="small"><strong>Terminal access stays shared.</strong> Ari has already granted it. No new request is needed.</p>';if(b.round>1&&b.terminalRequestPossible)return '<p class="small"><strong>Ari announces this round’s offer:</strong> “Noa has opened her hall to both forums. I am willing to offer shared terminal access too, if Youth requests it this round.”</p><p class="small">This is Ari’s decision in this fictional case. Noa’s hall decision cannot order him to share the terminal.</p>';return `<p class="small">${esc(b.terminalRule)}</p>`;}
function roundChanges(snap){const prev=s.engine.history.at(-2);if(!prev)return '';const rows=[];if(prev.terminal!==snap.terminal)rows.push('Public terminal: '+(prev.terminal==='shared'?'shared':'Government-only')+' → '+(snap.terminal==='shared'?'shared':'Government-only')+'.');for(const a of E.ACTORS)if(prev.allocations[a.id].choice!==snap.allocations[a.id].choice)rows.push(a.name+': '+names[prev.allocations[a.id].choice]+' → '+names[snap.allocations[a.id].choice]+'.');return `<section class="changes-board"><h3>What changed since round ${prev.round}</h3>${rows.length?`<ul>${rows.map(t=>`<li>${esc(t)}</li>`).join('')}</ul>`:'<p>The terminal rule and all four resource decisions stayed the same.</p>'}<p class="small">Read the replies below to explain these choices. The full table keeps every resource row, including unchanged ones.</p></section>`;}

function friendlyHistory(t){return t.replace(/Previous owner decision: ([GYSW])\./,(_,v)=>'Previous resource decision: '+names[v]+'.').replace('earn S rather than an otherwise-Y decision','earn shared support rather than exclusive Youth support');}
function roundWorkflow(r){return `<section class="round-workflow" aria-label="What to do this round"><ol><li><strong>Two promises</strong><span>Keep or change your published plan.</span></li><li><strong>Four requests</strong><span>Review one approach for each person.</span></li><li><strong>One forecast</strong><span>Predict one reply, with evidence and a reason.</span></li><li><strong>All four reply</strong><span>Seal the whole round, then inspect the results.</span></li></ol><p class="small">${r===1?'The “Make the … case” options are policy/service requests, preselected for all four people. Keep them if intended, or choose another request.':'Your previous requests carry forward. Review them; unchanged requests will be sent again.'} Selecting a person opens a conversation; it does not limit whose request is sent. No choice has a time penalty.</p></section>`;}
function requestSummary(){return `<section class="request-summary" aria-label="Review all four requests"><h3>Before you seal: all four requests</h3><dl>${E.ACTORS.map(a=>`<div><dt>${esc(a.person)} · ${esc(a.name)}</dt><dd>${esc(character(a.id).appeals[s.draft.appeals[a.id]].label)}</dd></div>`).join('')}</dl><p class="small">You predict only ${esc(actor(s.draft.target).person)}’s reply. All four requests above will be sent. Your two policy promises apply to everyone.</p></section>`;}

function plan(){const r=s.engine.round,b=E.getRoundBrief(s.engine),round=C.rounds[r-1],a=actor(ui.person),c=character(ui.person);return `<div class="heading-row"><div class="brief"><p class="eyebrow">Round ${r} · ${esc(b.title)}</p><h2>${esc(round.title)}</h2><p>${esc(round.brief)}</p></div><div class="status-stamp">${r===3?'Final arrangements':'Provisional · not final'}<br>Two promises</div></div>${roundWorkflow(r)}<div class="directive"><p class="tag attribution">Government's published plan · before your commitment</p><p><strong>${esc(titlePlan(b.governmentPlan))}</strong></p><p>${r>1?'<strong>Since the last round:</strong> ':''}${esc(b.governmentReason)}</p>${r>1?`<p class="small">Previous Government plan: ${esc(titlePlan(s.engine.history.at(-1).governmentPlan))}. Your previous published plan: ${esc(titlePlan(s.engine.lastPlan))}.</p>`:''}${terminalOffer(b)}</div><div class="workspace"><section aria-label="The people at the table"><p class="small">Choose a person to talk to. Inspecting is free. You may revisit anyone before sealing this round.</p><div class="people">${E.ACTORS.map(x=>`<button type="button" id="person-${x.id}" class="person ${ui.person===x.id?'selected':''}" data-action="person" data-id="${x.id}" aria-pressed="${ui.person===x.id}">${portrait(ci[x.id])}<span><strong>${esc(x.person)}</strong><span class="small">${esc(x.name)}<br>Current ask: ${esc(character(x.id).appeals[s.draft.appeals[x.id]].label)}</span></span></button>`).join('')}</div><div class="conversation"><div class="portrait-heading">${portrait(ci[a.id],true)}<div><p class="tag">${esc(a.name)}</p><h3>${esc(a.person)}</h3><p class="small">${esc(c.description)}</p></div></div><p class="priority">${esc(c.priority)}</p><p class="small">${esc(c.asset)}</p><p class="political-note">${esc(C.setting.actorNotes[a.id])}</p><p class="small question-cue">${esc(c.questions[0].q)}</p><p class="quote">${esc(c.questions[0].a)}</p><div class="questions">${c.questions.slice(1).map(q=>`<details><summary>${esc(q.q)}</summary><p>${esc(q.a)}</p></details>`).join('')}</div>${b.historyNotes[a.id].map(t=>`<p class="evidence">${esc(friendlyHistory(t))}</p>`).join('')}<fieldset><legend>Your request to ${esc(a.person)}</legend>${Object.entries(c.appeals).map(([id,x])=>`<label class="appeal"><input id="appeal-${a.id}-${id}" type="radio" name="appeal-${a.id}" data-appeal="${a.id}" value="${id}" ${s.draft.appeals[a.id]===id?'checked':''}><span>${esc(x.label)}<span class="detail">${esc(x.detail)}</span></span></label>`).join('')}</fieldset>${decisionHelp(a,b)}</div></section><aside class="plan-shelf" aria-label="Your plan and forecast"><p class="eyebrow">Youth's published offer</p><h3>Two promises. Keep or change them.</h3><fieldset><legend class="sr-only">Choose exactly two policies</legend>${E.POLICIES.map(p=>`<label class="policy"><input type="checkbox" id="policy-${p.id}" data-policy="${p.id}" ${s.draft.plan.includes(p.id)?'checked':''}><span>${p.id} · ${esc(p.title)}<small>${esc(p.detail)}</small></span></label>`).join('')}</fieldset><p class="small">Two promises model a limited campaign plan; they are not real budget estimates. To change a promise, remove one before choosing another.</p>${b.terminal==='shared'?'<p class="note">The terminal is already shared. No new request is needed this round.</p>':`<label class="appeal request"><input id="request" type="checkbox" data-request ${s.draft.requestShared?'checked':''}><span>Request shared terminal access<span class="detail">Ari must decide. Your request cannot book the public terminal.</span></span></label>`}<hr><div class="two-fields"><div class="field"><label for="target">Predict whose reply?</label><select id="target" data-draft="target">${E.ACTORS.map(x=>`<option value="${x.id}" ${s.draft.target===x.id?'selected':''}>${esc(x.person)} · ${esc(x.name)}</option>`).join('')}</select></div><div class="field"><label for="prediction">Their resource choice</label><select id="prediction" data-draft="prediction">${Object.entries(names).map(([id,t])=>`<option value="${id}" ${s.draft.prediction===id?'selected':''}>${id} · ${esc(t)}</option>`).join('')}</select></div></div><div class="field"><label for="confidence">How sure are you?</label><select id="confidence" data-draft="confidence"><option value="low" ${s.draft.confidence==='low'?'selected':''}>Unsure</option><option value="some" ${s.draft.confidence==='some'?'selected':''}>Somewhat sure</option><option value="high" ${s.draft.confidence==='high'?'selected':''}>Very sure</option></select><p class="small">A stated model rule can justify a confident forecast. Explain which rule or observed evidence supports your confidence.</p></div>${evidencePicker(s.draft.sources,'forecast')}${response('forecast'+r)}${hint()}<p class="small">${esc(round.development)} Sealing fixes this round's plan and requests before all four owners answer.</p>${requestSummary()}${button('Seal round '+r+' plan','seal','primary wide-action')}</aside></div>`;}
function table(snap){return `<div class="table-wrap"><table class="tally"><caption class="sr-only">Resource choices and reach units</caption><thead><tr><th>Resource</th><th>Government</th><th>Youth</th><th>Unused</th></tr></thead><tbody><tr><td>Baseline volunteers</td><td>1</td><td>1</td><td>0</td></tr><tr><td>Public terminal (${snap.terminal==='shared'?'shared':'Government-only'})</td><td>${snap.terminal==='shared'?1:2}</td><td>${snap.terminal==='shared'?1:0}</td><td>0</td></tr>${E.ACTORS.map(a=>{const code=snap.allocations[a.id].choice,v={G:[2,0,0],Y:[0,2,0],S:[1,1,0],W:[0,0,2]}[code];return `<tr><td>${esc(a.name)} · ${code}</td><td>${v[0]}</td><td>${v[1]}</td><td>${v[2]}</td></tr>`;}).join('')}<tr class="total"><td>Total reach</td><td>${snap.reach.G}</td><td>${snap.reach.Y}</td><td>${snap.reach.unused}</td></tr></tbody></table></div>`;}
function times(snap){return `<div class="opportunities"><div><span>Government opportunity</span><strong>${snap.seconds.G}s</strong><span>${snap.reach.G} reach</span></div><div><span>Youth opportunity</span><strong>${snap.seconds.Y}s</strong><span>${snap.reach.Y} reach</span></div></div><p class="small">20 seconds + five seconds per reach unit. Reach and private votes are different things.</p>`;}
function reveal(){const snap=s.engine.history.at(-1),f=s.forecasts.at(-1),reply=snap.allocations[f.target];return `<p class="eyebrow">Round ${snap.round} · all four replies are in</p><h2>They answered<br>for themselves</h2><div class="reveal-top"><div><p><strong>Your published plan:</strong> ${esc(titlePlan(snap.youthPlan))}</p><p><strong>Government's:</strong> ${esc(titlePlan(snap.governmentPlan))}</p><div class="evidence"><strong>Public terminal · ${snap.terminal==='shared'?'shared access':'Government-only'}</strong><p>${esc(snap.terminalReason)}</p></div></div><div class="forecast-result"><p class="tag">Your forecast · ${esc(actor(f.target).person)}</p><p>You predicted <strong>${esc(names[f.prediction])}</strong>.<br>The organization chose <strong>${esc(names[reply.choice])}</strong>.</p><p class="small">${esc(reply.reason)}</p><p class="small">Your original reason is kept in the report. This checks a model prediction, not whether you chose the correct politics.</p></div></div>${roundChanges(snap)}${E.ACTORS.map(a=>{const x=snap.allocations[a.id];return `<section class="snapshot">${portrait(ci[a.id])}<div><h3>${esc(a.person)} <span class="small">${esc(a.name)}</span></h3><p class="resource-result"><strong>${esc({G:'Gives both resource units to Government.',Y:'Gives both resource units to Youth.',S:'Shares one resource unit with each campaign.',W:'Keeps both resource units unused.'}[x.choice])}</strong></p><p class="quote">${esc(x.quote)}</p><p>${esc(x.reason)}</p></div><div class="choice">${x.choice}<small>${esc(names[x.choice])}</small></div></section>`;}).join('')}<div class="reveal-top"><div>${table(snap)}</div><div><h3>The practical consequence</h3>${times(snap)}<p>In this case, each organization retained practical control over tonight’s contribution. The terminal notice did not itself transfer its room, contacts, or vehicles. Legal ownership alone would not guarantee this freedom to decide in another case.</p></div></div><div class="action-row">${button('Open evidence archive','archive','quiet')}${button(snap.round<3?'Make the next round’s plan':'Open tonight’s forums',snap.round<3?'next-round':'to-forum','primary')}</div>`;}
function forum(){const last=s.engine.history[2];const supporters=party=>E.ACTORS.filter(a=>party==='G'?['G','S'].includes(last.allocations[a.id].choice):['Y','S'].includes(last.allocations[a.id].choice)).map(a=>a.person).join(', ')||'baseline volunteers only';return `<p class="eyebrow">The doors can open</p><h2>The final speaking<br>opportunities.</h2>${C.forum.paragraphs.map(p=>`<p>${esc(p)}</p>`).join('')}<img class="scene" src="${ASSETS.forum}" alt="Woodcut city architecture and ordinary commuters entering a public meeting space"><div class="forum-grid"><section class="forum-side"><div class="portrait-heading">${portrait(0)}<div><p class="tag">Official transport forum</p><h3>Ari Velen</h3></div></div><p class="speech">“We introduced the common fare card. Tonight, our two promises are ${esc(last.governmentPlan.map(id=>policy(id).title.toLowerCase()).join(' and '))}. Judge what we offer and what we have delivered.”</p><p class="small">Unity Party · Resources supplied by: ${esc(supporters('G'))}.</p></section><section class="forum-side youth"><div class="portrait-heading">${portrait(1)}<div><p class="tag">Youth transport forum</p><h3>Sasha Marin</h3></div></div><p class="speech">“A journey can decide whether an opportunity remains open. Our published promises are ${esc(last.youthPlan.map(id=>policy(id).title.toLowerCase()).join(' and '))}. These arrangements let us make that case in public.”</p><p class="small">Legal opposition campaign · Resources supplied by: ${esc(supporters('Y'))}.</p></section></div>${times(last)}<p class="note">The speeches above are short authored excerpts, not real-time playback. The numbers show available pitch time. The game does not turn reach, a speech, or your personal choice into a simulated election result.</p><div class="intro-grid"><div>${evidencePicker(s.explainSources,'explain',2)}<fieldset class="field"><legend>One fictional commuter’s choice</legend><p class="small">Step out of Sasha’s organizer role. Imagine one citizen who has heard both cases. Choose Government, Youth, or abstain for that person.</p>${[['G','Government'],['Y','Youth'],['A','Abstain']].map(([v,t])=>`<label class="appeal"><input type="radio" name="citizen" data-citizen value="${v}" ${s.citizen===v?'checked':''}>${t}</label>`).join('')}</fieldset><p class="small">This is one fictional citizen's decision, not a class ballot or your real political beliefs. It will appear in your own report.</p></div><div>${response('explain')}<p class="small">Use the two revealed sources to compare who controlled each decision. Independent cooperation may favor Government, Youth, both, or neither. Equal totals can come from different resource rows.</p>${hint()}${button('Change one access rule','to-compare','primary wide-action')}</div></div>`;}
function compare(){const last=s.engine.history[2];return `<p class="eyebrow">A controlled comparison</p><h2>Only the terminal<br>rule changes</h2><p>Keep the two policy plans and all four final organization decisions exactly as they are. Switch ${last.terminal==='shared'?'<strong>shared terminal access to Government-only</strong>':'<strong>Government-only terminal access to shared</strong>'}. Nothing else is renegotiated. This isolates the terminal rule’s direct effect; a new negotiation could produce other changes.</p><div class="intro-grid"><div><h3>Starting arrangement: as played</h3>${table(last)}${times(last)}<p class="note">Make your prediction before seeing the comparison. This changes an opportunity; it does not replay citizens' votes.</p></div><div><div class="two-fields"><div class="field"><label for="predictG">Predicted new Government reach</label><input id="predictG" type="text" inputmode="numeric" maxlength="2" data-compare="G" value="${esc(s.comparison.G)}"></div><div class="field"><label for="predictY">Predicted new Youth reach</label><input id="predictY" type="text" inputmode="numeric" maxlength="2" data-compare="Y" value="${esc(s.comparison.Y)}"></div></div>${response('compare')}${hint()}${button('Keep my prediction and reveal','reveal-comparison','primary wide-action')}</div></div>`;}
function revise(){const last=s.engine.history[2],other=E.compareAccess(last),f=s.forecasts[s.revisionRound-1];return `<p class="eyebrow">The same final choices · one changed rule</p><h2>A changed opportunity.<br>An open question.</h2><div class="comparison"><div><h3>As played · ${last.terminal==='shared'?'shared terminal':'Government-only terminal'}</h3>${times(last)}</div><div><h3>Access-only comparison · ${other.terminal==='shared'?'shared terminal':'Government-only terminal'}</h3>${times(other)}</div></div><p class="note">One reach unit and five seconds move from one campaign to the other. The published plans and every organization choice stay fixed. We have not established a new election winner or the reason for anyone's vote.</p><p class="political-note">Even shared access to this terminal leaves the ruling party’s national media and institutional advantages in place. This comparison changes one venue rule, not Lydora’s whole political order.</p><div class="intro-grid"><div><details class="archive"><summary>Your earlier explanation</summary><div class="readonly">${esc(s.notes.explain||'No earlier explanation was supplied.')}</div></details><h3>Your access prediction</h3><p>Government ${esc(s.comparison.G||'not supplied')}; Youth ${esc(s.comparison.Y||'not supplied')}. Actual comparison: Government ${other.reach.G}; Youth ${other.reach.Y}.</p><div class="readonly">${esc(s.comparison.originalNote||'No written prediction was supplied before the reveal.')}</div><div class="field"><label for="revisionRound">Revisit an earlier forecast too</label><select id="revisionRound" data-revision-round>${s.forecasts.map(x=>`<option value="${x.round}" ${x.round===s.revisionRound?'selected':''}>Round ${x.round} · ${esc(actor(x.target).person)}</option>`).join('')}</select></div><p class="small">You expected ${esc(names[f.prediction])}; the organization chose ${esc(names[lastForRound(f.round).allocations[f.target].choice])}.</p><div class="readonly">${esc(f.note||'No written forecast supplied.')}</div></div><div>${response('revise')}<p class="small">Use the three points above in one response. You may keep an explanation when the evidence supports it.</p>${hint()}${button('Try a different institutional setting','to-transfer','primary wide-action')}</div></div>`;}
const lastForRound=r=>s.engine.history[r-1];
function transfer(){return `<p class="eyebrow">New case · institutions and regime unspecified</p><h2>Another order.<br>Different conditions.</h2><div class="intro-grid"><div><img class="scene" src="${ASSETS.transfer}" alt="Stylized transit routes passing a neighborhood hall and a public building"><h3>The booking notice</h3><p>A mayor closes a municipal hall for a documented repair. The mayor also orders an independently owned sports club to cancel a rival's meeting.</p><p>This town may have different rules from Lydora. Private ownership is given. What it lets the club do in practice is not.</p><p class="small">Name a genuinely unknown fact and explain both branches. Your prediction may be conditional rather than certain.</p></div><div>${response('transfer')}${hint()}${button('Review my work and make a report','finish','primary wide-action')}</div></div>`;}
function report(){const missing=K.requirements(s),complete=s.mode==='class'&&!missing.length;return `<div class="completion ${complete?'':'incomplete'}"><p class="eyebrow">Your work is kept</p><h2>${complete?'Your Class report<br>is ready':s.mode==='fun'?'Your exploration<br>report is ready':'Your Class work<br>needs a few items'}</h2><p>${complete?'All required choices, evidence references, and response lengths are present. This is a process check, not a grade.':s.mode==='fun'?'Fun mode does not certify Class completion. You can export everything you explored.':'Use the unfinished-work controls below. Exports remain available and will state what is missing.'}</p>${s.funExposure?'<p class="note">Earlier Fun exploration is disclosed in this report. Later responses are labeled retrospective; they do not replace original forecasts.</p>':''}<p><strong>Submit your work:</strong> download a report or print to PDF, then submit it through your course’s normal system. <strong>Keep playing later:</strong> save a JSON backup. Nothing submits automatically.</p><div class="action-row">${button('Print / Save as PDF','print','primary')}${button('Download text report','text')}${button('Download HTML report','html')}${button('Save game backup','backup')}</div></div>${s.mode==='class'&&missing.length?`<section><h3>Unfinished Class work</h3><ul>${missing.map(x=>`<li>${esc(x.label)}</li>`).join('')}</ul>${button('Complete missing responses','makeup','primary')}</section>`:''}<details class="archive" open><summary>Report preview</summary>${renderReport(K.reportData(s))}</details><div class="rules"><h3>Optional question for further reading</h3><p class="small">Carry this question into discussion or Levitsky and Way’s <em>The New Competitive Authoritarianism</em>. Your required report contains the eight responses above.</p><p>${esc(C.readingQuestion)}</p><p class="small">Lydora’s authoritarian setting is given. One facility rule, one cooperation pattern, or one election result alone would not establish that classification in a real country. Ask what makes practical independence possible outside this fictional model.</p></div><div class="action-row">${button('Open evidence archive','archive','quiet')}${button('Start a new run','reset','quiet danger')}</div>`;}
function renderReport(d){return `<article class="report"><h1>The Order Was Given</h1><p><strong>${esc(d.name)}</strong> · ${d.mode==='class'?'For Class':'Just for Fun'}${d.course?' · '+esc(d.course):''}${d.code?' · code '+esc(d.code):''}</p><p>${d.classComplete?'Class process requirements complete.':'Not certified complete for Class.'} Writing has not been graded.</p>${d.earlierExploration?'<p>Earlier Fun exploration occurred. Retrospective responses are marked separately.</p>':''}${d.missing.length?`<p>Missing: ${d.missing.map(x=>esc(x.label)).join('; ')}.</p>`:''}<p class="small">Created ${esc(d.created)}. Report generated ${esc(d.generated)}. Game version ${esc(d.version)}.</p><h2>The political setting</h2><p>${esc(C.setting.scope)}</p><h2>Initial intention and written reasoning</h2>${d.notes.map(n=>`<section><h3>${esc(n.label)}</h3><p>${esc(n.prompt)}</p><div class="answer original"><strong>Original response</strong><br>${esc(n.original||'Not supplied.')}</div>${n.later?`<div class="answer"><strong>Later retrospective response</strong><br>${esc(n.later)}</div>`:''}</section>`).join('')}<h2>Committed forecasts and actual replies</h2>${d.forecasts.map(f=>`<section><h3>Round ${f.round} · ${esc(actor(f.target).person)}</h3><p>Predicted ${esc(names[f.prediction])}; actual ${esc(names[d.history[f.round-1].allocations[f.target].choice])}. Confidence: ${esc(f.confidence)}. Committed ${esc(f.at)}${f.skipped?' · Some Fun requirements were omitted.':''}</p>${f.sources.map(e=>`<p><strong>${esc(e.label)}</strong><br>${esc(e.text)}</p>`).join('')}${!f.sources.length?'<p>No original forecast evidence selected.</p>':''}${f.laterSources?.length?f.laterSources.map(e=>`<p><strong>Later evidence: ${esc(e.label)}</strong><br>${esc(e.text)}</p>`).join(''):''}</section>`).join('')}<h2>The three rounds</h2>${d.history.map(x=>`<section><h3>Round ${x.round}</h3><p>Government plan: ${esc(titlePlan(x.governmentPlan))}. Youth plan: ${esc(titlePlan(x.youthPlan))}.</p><p>Shared terminal access requested: ${x.action.requestShared?'yes':'no'}.</p><p>Terminal: ${esc(x.terminal)}. ${esc(x.terminalReason)}</p>${table(x)}${E.ACTORS.map(a=>`<p><strong>${esc(a.person)}: ${esc(names[x.allocations[a.id].choice])}</strong><br>Youth's approach: ${esc(character(a.id).appeals[x.action.appeals[a.id]].label)}.<br>${esc(x.allocations[a.id].reason)}</p>`).join('')}<p>Pitch opportunities: Government ${x.seconds.G}s; Youth ${x.seconds.Y}s. No vote result is generated.</p></section>`).join('')}<h2>Explanation evidence</h2>${d.explainSources.map(e=>`<p><strong>${e.retrospective?'Later retrospective evidence · ':''}${esc(e.label)}</strong><br>${esc(e.text)}</p>`).join('')||'<p>No explanation evidence selected.</p>'}<p>Fictional citizen choice: ${esc(d.citizen?({G:'Government',Y:'Youth',A:'Abstain'}[d.citizen]):'Not supplied')}. This is not an election tally.</p><h2>Controlled comparison</h2>${d.comparison.laterG||d.comparison.laterY?`<p>Retrospective estimates: Government ${esc(d.comparison.laterG||'not supplied')}; Youth ${esc(d.comparison.laterY||'not supplied')}. These do not replace the initial predictions.</p>`:''}${d.comparisonActual?`<p>Predicted new reach G ${esc(d.comparison.G||'not supplied')} / Y ${esc(d.comparison.Y||'not supplied')}. Actual comparison G ${d.comparisonActual.reach.G} / Y ${d.comparisonActual.reach.Y}. Only terminal access changed. Plans and organization choices stayed fixed.</p>`:'<p>Not revealed.</p>'}<h2>Model limits</h2><p>The fictional characters’ responses follow authored rules, not real political laws. Reach represents speaking opportunity, not votes. Stated ownership is not proof of practical independence in a different case. Lydora’s authoritarian setting is supplied in the background. These forum decisions alone would not classify a real regime, and the game supplies no measured learning score.</p>${credit()}</article>`;}
function reportText(d){let t=`THE ORDER WAS GIVEN\n${d.name}\nMode: ${d.mode}\nCourse: ${d.course}\nCode: ${d.code}\nClass process complete: ${d.classComplete}\nEarlier Fun exploration: ${d.earlierExploration}\nGenerated: ${d.generated}\nMissing: ${d.missing.map(x=>x.label).join('; ')||'none'}\n\n`;for(const n of d.notes)t+=`${n.label}\n${n.prompt}\nORIGINAL: ${n.original||'Not supplied'}\n${n.later?'LATER RETROSPECTIVE: '+n.later+'\n':''}\n`;for(const f of d.forecasts){t+=`ROUND ${f.round} FORECAST: ${actor(f.target).person}, ${names[f.prediction]}, confidence ${f.confidence}\n`;for(const e of f.sources)t+=`${e.label}: ${e.text}\n`;if(f.laterSources)for(const e of f.laterSources)t+=`LATER EVIDENCE ${e.label}: ${e.text}\n`;}
 for(const x of d.history){t+=`\nROUND ${x.round}\nGovernment ${titlePlan(x.governmentPlan)}; Youth ${titlePlan(x.youthPlan)}\nShared terminal access requested: ${x.action.requestShared?'yes':'no'}\nTerminal ${x.terminal}: ${x.terminalReason}\n`;for(const a of E.ACTORS)t+=`${a.person}: ${names[x.allocations[a.id].choice]}. Youth request: ${C.characters[aliases[a.id]].appeals[x.action.appeals[a.id]].label}. ${x.allocations[a.id].reason}\n`;t+=`Reach G${x.reach.G} Y${x.reach.Y} unused${x.reach.unused}; pitch seconds G${x.seconds.G} Y${x.seconds.Y}\n`;}
 t+='\nEXPLANATION EVIDENCE\n'+d.explainSources.map(e=>(e.retrospective?'LATER RETROSPECTIVE EVIDENCE · ':'')+e.label+': '+e.text).join('\n')+`\nFictional citizen choice: ${d.citizen||'not supplied'}\n`;if(d.comparisonActual)t+=`\nACCESS ONLY: predicted G${d.comparison.G} Y${d.comparison.Y}; actual G${d.comparisonActual.reach.G} Y${d.comparisonActual.reach.Y}. Policies and organization choices unchanged.\n`;if(d.comparison.laterG||d.comparison.laterY)t+=`\nRETROSPECTIVE ACCESS ESTIMATES: G${d.comparison.laterG} Y${d.comparison.laterY}.\n`;return t+'\nPOLITICAL SETTING\n'+C.setting.scope+'\n\nNo election result or learning score is generated. The fictional characters’ decisions follow authored rules.\nCreated by Yoav Duman / יואב דומן\n';}
function download(name,data,type){const blob=new Blob([data],{type});const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),15000);say('Prepared '+name);}
function backup(){save();download('The_Order_Was_Given_backup.json',JSON.stringify(s,null,2),'application/json');}
function openDialog(title,body){const d=$('dialog');if(!d.open)ui.dialogFocus=document.activeElement;d.innerHTML=`<div class="close-row"><h2 id="dialog-title">${esc(title)}</h2>${button('Close','close','quiet')}</div><div class="scroll">${body}</div>`;d.showModal();d.querySelector('button,input,select,textarea')?.focus();}
function closeDialog(){const d=$('dialog');d.close();const target=ui.dialogFocus?.isConnected?ui.dialogFocus:$('main');target?.focus?.({preventScroll:true});}
function showFiles(){openDialog('Save & files',`<p>${ui.rawRecovery?'Autosave is paused. Your unopened saved data is protected. Download that data below before you explicitly replace it with a new run or restore a backup. New edits are currently only in memory.':ui.saveError?'Local saving is unavailable. Download a backup before closing.':'Edits and commitments are saved in this browser when local storage is available.'} Moving the HTML file to another browser or device does not move the saved game.</p><div class="action-row">${button('Download backup','backup','primary')}${button('Restore backup','restore')}</div><p class="small">Restore accepts this game's JSON backups and checks the model history before replacing current work. Backups and reports contain your name and written answers; keep them where you intend.</p>${ui.rawRecovery?button('Download unopened saved data','recovery','quiet'):''}<h3>Export your work so far</h3><div class="action-row">${button('Print / Save as PDF','print')}${button('Text report','text')}${button('HTML report','html')}</div>${button('Start a new run','reset','quiet danger')}`);}
function archive(){const list=availableEvidence();openDialog('Your evidence archive',`<p>These are the sources currently available. Future responses stay hidden until you seal their round.</p>${list.map(e=>`<section class="evidence"><strong>${esc(evidenceLabel(e))}</strong><p>${esc(e.text)}</p></section>`).join('')}`);}
function help(){openDialog('How the table works',`<p class="note">${esc(C.setting.scope)} Use Setting to reread the political background.</p><p>You are Sasha. Select exactly two policies, make one request to each organization, and commit a forecast before sealing a round. All four organizations then answer independently.</p><h3>Quick word help</h3>${wordHelp()}<h3>Terms at the table</h3><p><strong>Forum:</strong> a public campaign meeting. <strong>Terminal:</strong> the central bus station. <strong>Appeal:</strong> the request you make. <strong>Allocation:</strong> a decision about where resources go. <strong>Asset:</strong> a useful resource, such as a room or contact network. <strong>Baseline:</strong> the contribution each campaign starts with.</p><p><strong>Reach unit:</strong> a simplified resource contribution that adds five speaking seconds. It is not a count of people, social-media views, or voters. Three units give 20 + 15 = 35 seconds.</p><h3>Four resource choices</h3><p>G: two units to Government. Y: two to Youth. S: one to each. W: both unused.</p><p>Each campaign keeps one baseline unit. The public terminal supplies two units: Government-only 2/0, shared 1/1. Four organizations control two units each. Used plus unused always totals 12.</p><h3>Speaking opportunity</h3><p>Each campaign has 20 seconds plus 5 seconds per reach unit. Reach does not count votes. Cooperation with a forum does not bind a citizen's private choice.</p><h3>Your written work</h3><p>Eight short responses target about 300 words. Class checks required choices, evidence references, and minimum lengths. It does not understand or grade your explanation. Your first forecasts stay in the report when you reconsider.</p><p>Inspecting a person or asking a question is free. Under 'How this fictional person decides', you can inspect authored conditions. A demand is an appeal, not legal power over that person.</p><h3>Saving</h3><p>Autosave is local to this browser. Use Save & files for a portable backup. There is no countdown. Take the time needed to reason.</p>`);}
function noteReady(id){return s.mode==='fun'||K.words(s.notes[id])>=K.prompt(id).minWords;}
function fail(msg,focusId){ui.error=msg;render(false);if(focusId)$(focusId)?.focus();say(msg);}
function move(screen){ui.error='';s.screen=screen;save();render(true);}
function makeup(){const missing=K.requirements(s);openDialog('Complete missing Class work',`<p>Earlier choices and first answers stay unchanged. A response added after you saw consequences is labeled retrospective in the report.</p>${missing.some(x=>x.id==='name')?`<label>Your name<input type="text" id="makeup-name" data-identity="name" maxlength="200" value="${esc(s.identity.name)}"></label>`:''}${C.prompts.filter(p=>missing.some(x=>x.id===p.id)).map(p=>`<section class="backfill"><label for="later-${p.id}">${esc(p.label)} · later response</label><p class="small" id="later-prompt-${p.id}">${esc(p.prompt)}</p><textarea maxlength="${K.WORD_LIMIT}" aria-describedby="later-prompt-${p.id}" id="later-${p.id}" data-makeup="${p.id}">${esc(s.makeup[p.id]||'')}</textarea><p class="count">Original: ${esc(s.notes[p.id]||'not supplied')}. Minimum ${p.minWords} words for the later response.</p></section>`).join('')}${missing.some(x=>x.id.startsWith('evidence'))?`<h3>Missing forecast evidence</h3>${s.forecasts.filter(f=>!f.sources.length&&!f.laterSources?.length).map(f=>`<label>Round ${f.round} · later source<select data-later-source="${f.round}"><option value="">Choose evidence already seen…</option>${availableEvidence().map(e=>`<option value="${esc(e.id)}">${esc(e.label)}</option>`).join('')}</select></label>`).join('')}`:''}${missing.some(x=>x.id==='explainEvidence')?evidencePicker(s.explainSources,'explain',2):''}${missing.some(x=>x.id==='accessNumbers')?`<h3>Later access estimates</h3><p class="small">The original estimates stay unchanged. These later estimates are retrospective.</p><div class="two-fields"><label>Government reach<input type="text" inputmode="numeric" maxlength="2" data-compare-later="G" value="${esc(s.comparison.laterG||'')}"></label><label>Youth reach<input type="text" inputmode="numeric" maxlength="2" data-compare-later="Y" value="${esc(s.comparison.laterY||'')}"></label></div>`:''}${missing.some(x=>x.id==='citizen')?`<label>Fictional citizen choice<select data-later-citizen><option value="">Choose…</option><option value="G">Government</option><option value="Y">Youth</option><option value="A">Abstain</option></select></label>`:''}<p class="small">If any round or comparison is still missing, continue the game itself rather than filling a response here.</p>${button('Keep later responses and review','finish-makeup','primary wide-action')}`);}
const doAction={
 'choose-class':()=>{s.mode='class';save();render(false);},'choose-fun':()=>{s.mode='fun';save();render(false);},
 start:()=>{if(s.mode==='class'&&!s.identity.name.trim())return fail('Add your name for the Class report.','name');move('intro');},
 begin:()=>{if(!noteReady('opening'))return fail('Write at least 15 words for your first intention before opening the table. This is a presence check, not a grade.','note-opening');move('plan');},
 person:b=>{ui.person=b.dataset.id;render(false);},
 seal:()=>{if(s.engine.round>3)return;const id='forecast'+s.engine.round;if(!noteReady(id))return fail('Record at least 20 words for your forecast before seeing the replies. Your instructor assesses the reasoning.','note-'+id);if(s.mode==='class'&&!s.draft.sources.length)return fail('Choose one visible evidence source for your forecast.','evidence-forecast');const r=s.engine.round;s.draft.sources=s.draft.sources.map(e=>availableEvidence().find(a=>a.id===e.id)).filter(Boolean);try{const next=E.resolveRound(s.engine,{plan:s.draft.plan,appeals:s.draft.appeals,requestShared:s.draft.requestShared});s.forecasts.push({round:r,target:s.draft.target,prediction:s.draft.prediction,confidence:s.draft.confidence,note:s.notes[id],sources:structuredClone(s.draft.sources),at:new Date().toISOString(),skipped:s.mode==='fun'&&(!s.notes[id]||!s.draft.sources.length)});s.engine=next;if(s.mode==='fun')s.funExposure=true;move('reveal');}catch(e){fail(e.message);}},
 'next-round':()=>{s.draft={...s.draft,plan:[...s.engine.lastPlan],appeals:{...s.draft.appeals},requestShared:false,sources:[]};move('plan');},
 'to-forum':()=>move('forum'),
 'to-compare':()=>{if(!noteReady('explain'))return fail('Write at least 25 words explaining an observed action and its effect before changing access.','note-explain');if(s.mode==='class'&&K.requirements(s).some(x=>x.id==='explainEvidence')){const terminalMissing=!s.explainSources.some(e=>/^round-[1-3]-terminal$/.test(e.id));return fail(terminalMissing?'Add a revealed terminal decision for your explanation.':'Add an organization’s revealed reply for your explanation.',terminalMissing?'evidence-explain':'evidence-explain-owner');}if(s.mode==='class'&&!s.citizen)return fail('Choose Government, Youth, or abstain for the fictional citizen.');move('compare');},
 'reveal-comparison':()=>{if(!noteReady('compare'))return fail('Record at least 20 words for your prediction before revealing the comparison.','note-compare');const invalid=['G','Y'].find(k=>!/^(?:[0-9]|1[0-2])$/.test(s.comparison[k]));if(invalid)return fail('Predict both new reach totals using whole numbers from 0 to 12.','predict'+invalid);s.comparison.revealed=true;s.comparison.originalNote=s.notes.compare;s.comparison.at=new Date().toISOString();move('revise');},
 'to-transfer':()=>{if(!noteReady('revise'))return fail('Write at least 20 words defending, narrowing, or changing your explanation before applying it elsewhere.','note-revise');move('transfer');},
 finish:()=>{if(!noteReady('transfer'))return fail('Write at least 25 words naming an unknown fact and explaining how different answers change your prediction.','note-transfer');move('report');},
 setting:()=>openDialog('Lydora’s political setting',settingHtml()),
 archive,help,files:showFiles,backup,restore:()=>{$('restoreFile').click();},
 close:closeDialog,
 'remove-source':b=>{const target=b.dataset.scope==='explain'?s.explainSources:s.draft.sources;target.splice(Number(b.dataset.index),1);save();if($('dialog').open)makeupRefresh();else render(false);},
 mode:()=>openDialog('Choose your route',`<p>Switching keeps your work. Class completion is checked again from the actual requirements. If you have already seen outcomes in Fun mode, that earlier exploration will be disclosed.</p><div class="mode-options">${button('For Class','mode-class')}${button('Just for Fun','mode-fun')}</div>`),
 'mode-class':()=>{if(s.mode==='fun'&&s.engine.history.length)s.funExposure=true;s.mode='class';closeDialog();save();render(false);},'mode-fun':()=>{s.mode='fun';closeDialog();save();render(false);},
 print:()=>{closeDialogIfOpen();$('printReport').innerHTML=renderReport(K.reportData(s));window.print();},
 text:()=>download('The_Order_Was_Given_report.txt',reportText(K.reportData(s)),'text/plain;charset=utf-8'),
 html:()=>{const text='<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>The Order Was Given · evidence report</title><style>body{font:16px/1.6 Arial,sans-serif;max-width:960px;margin:2rem auto;padding:1rem;color:#182b25}h1,h2,h3{line-height:1.2}table{border-collapse:collapse;width:100%}th,td{padding:.5rem;border-bottom:1px solid #aaa;text-align:left}.answer{white-space:pre-wrap;border-left:2px solid #567;padding:.6rem 1rem}.small,.original{font-size:.85rem}.credit{margin-top:2rem}@media print{body{font-size:10pt;margin:0}h2,h3{break-after:avoid}tr{break-inside:avoid}}</style>'+renderReport(K.reportData(s))+'</html>';download('The_Order_Was_Given_report.html',text,'text/html;charset=utf-8');},
 recovery:()=>download('The_Order_Was_Given_unopened_save.json',ui.rawRecovery,'application/json'),
 reset:()=>openDialog('Start a new run?',`<p>${ui.rawRecovery?'This will replace your unopened saved data and your current attempt. Download the unopened data first if you need it.':'Your current game will be replaced on this browser. Download a backup first if you want to return to it.'}</p>${ui.rawRecovery?button('Download unopened saved data','recovery','primary'):''}<div class="action-row">${button('Download backup','backup')}${button('Keep this game','close','quiet')}${button('Replace with a new run','confirm-reset','primary')}</div>`),
 'confirm-reset':()=>{closeDialog();s=K.newSession();ui.person='union';ui.rawRecovery='';save();render(true);},
 facilitator:()=>openDialog('Facilitator view',`<p>Enter your usual facilitator code to open the guidance. This convenience view is not a secure assessment key.</p><label for="facilitator-code">Code<input id="facilitator-code" type="password" inputmode="numeric"></label><div class="action-row">${button('Open guide','open-guide','primary')}</div>`),
 'open-guide':()=>{if($('facilitator-code').value!=='5907'){say('Code not recognized.');$('facilitator-code').setAttribute('aria-invalid','true');return;}openDialog('Facilitator guidance',`<p>Designed for a thoughtful 55-minute route: opening 7, first round 12, second 7, final 7, forum 6, comparison prediction 3, reconsideration 4, transfer 6, report/debrief 3. These are design estimates, not measured completion times.</p><p>Begin with the supplied authoritarian setting: meaningful elections coexist with entrenched party control and selective pressure. Do not ask learners to infer the entire regime from one venue rule.</p><p>Let students commit forecasts before explaining responses. Ask what the public terminal directly controls, what independent owners chose, and what the evidence cannot establish about voters.</p><p>Eight responses target 300 words. Presence checks do not grade reasoning. Look for a concrete action→resource/time effect, a distinction between opportunity and votes, and conditional predictions in the new town.</p><p>Do not reward recruiting everyone, sharing everything, or maximizing reach as the correct politics. Keep named explanations from Levitsky and Way for after the assigned reading.</p><p>For Class switching after Fun, require missing work and preserve the retrospective disclosure. Exports are local downloads; students submit through your normal course system.</p><p>For printable instructions, use the separate instructor guide supplied with the game.</p>`);},
 makeup,
 'finish-makeup':()=>{closeDialog();save();render(false);}
};
function closeDialogIfOpen(){if($('dialog').open)closeDialog();}
function makeupRefresh(){closeDialog();makeup();}
document.addEventListener('click',e=>{const b=e.target.closest('button[data-action]');if(!b)return;const fn=doAction[b.dataset.action];if(fn)fn(b);});
document.addEventListener('input',e=>{const x=e.target;if(x.dataset.identity){s.identity[x.dataset.identity]=x.value;save();}else if(x.dataset.note){x.value=x.value.slice(0,K.WORD_LIMIT);s.notes[x.dataset.note]=x.value;const p=K.prompt(x.dataset.note),c=$('count-'+p.id);if(c){c.textContent=K.words(x.value)+' words. Suggested about '+p.targetWords+'; you may write more. Class minimum '+p.minWords+'. Recorded, not graded.';c.classList.toggle('ready',K.words(x.value)>=p.minWords);}save();}else if(x.dataset.makeup){x.value=x.value.slice(0,K.WORD_LIMIT);s.makeup[x.dataset.makeup]=x.value;save();}else if(x.dataset.compare){x.value=x.value.replace(/[^0-9]/g,'').slice(0,2);s.comparison[x.dataset.compare]=x.value;save();}else if(x.dataset.compareLater){x.value=x.value.replace(/[^0-9]/g,'').slice(0,2);s.comparison['later'+x.dataset.compareLater]=x.value;save();}});
document.addEventListener('change',e=>{const x=e.target;if(x.dataset.policy){const p=x.dataset.policy;if(x.checked&&!s.draft.plan.includes(p)){if(s.draft.plan.length>=2){x.checked=false;say('Keep exactly two promises. Remove one before choosing another.');return;}s.draft.plan.push(p);}else if(!x.checked)s.draft.plan=s.draft.plan.filter(id=>id!==p);s.draft.sources=s.draft.sources.map(e=>availableEvidence().find(a=>a.id===e.id)).filter(Boolean);save();render(false);}else if(x.dataset.appeal){s.draft.appeals[x.dataset.appeal]=x.value;save();render(false);}else if(x.hasAttribute('data-request')){s.draft.requestShared=x.checked;save();}else if(x.dataset.draft){s.draft[x.dataset.draft]=x.value;save();if(x.dataset.draft==='target')render(false);}else if(x.dataset.evidence){const item=availableEvidence().find(e=>e.id===x.value);const list=x.dataset.evidence==='explain'?s.explainSources:s.draft.sources;if(item&&!list.some(e=>e.id===item.id)){const added=structuredClone(item);if(x.dataset.evidence==='explain'&&s.screen==='report')added.retrospective=true;list.push(added);}save();if($('dialog').open)makeupRefresh();else render(false);say(list.length+' evidence item'+(list.length===1?'':'s')+' selected.');}else if(x.hasAttribute('data-citizen')||x.hasAttribute('data-later-citizen')){s.citizen=x.value;save();}else if(x.hasAttribute('data-revision-round')){s.revisionRound=Number(x.value);save();render(false);}else if(x.dataset.laterSource){const f=s.forecasts[Number(x.dataset.laterSource)-1],item=availableEvidence().find(e=>e.id===x.value);if(item)f.laterSources=[structuredClone(item)];save();}});
$('restoreFile').addEventListener('change',async e=>{const file=e.target.files[0];e.target.value='';if(!file)return;if(file.size>4*1024*1024){openDialog('Backup not restored','<p>This file is too large for a game backup. Your current work is unchanged.</p>');return;}try{const candidate=K.validateSession(JSON.parse(await file.text()));ui.restoreCandidate=candidate;openDialog('Replace current work?',`<p>Restore ${esc(candidate.identity.name||'unnamed player')}, with ${candidate.engine.history.length} completed rounds? Your current work will be replaced. Download a backup first if you need it.</p><div class="action-row">${button('Back up current game','backup')}${button('Keep current game','close','quiet')}${button('Restore this backup','confirm-restore','primary')}</div>`);}catch(err){openDialog('Backup not restored',`<p>${esc(err.message)}</p><p>Your current game has not changed.</p>`);}});
doAction['confirm-restore']=()=>{if(!ui.restoreCandidate)return;const next=ui.restoreCandidate;delete ui.restoreCandidate;closeDialog();s=next;ui.error='';ui.person='union';ui.rawRecovery='';save();render(true);say('Backup restored. '+stageName());};
$('dialog').addEventListener('close',()=>{ui.restoreCandidate=undefined;});
render(false);
})();
</script></body></html>

===== END COMPLETE HTML =====
