"""deck - the ops deck: every role station live in one tmux-style grid,
with the guided wizard rail.
The deck is the crew law made physical: six panes, one per role, all
live at the same time - because in real life the roles act in parallel,
they do not take turns existing. Each pane is an independent viewport
(iframe) onto that role's station in chrome-stripped "pane mode"; panes
reload and zoom independently, tmux-style.
On the right rides the WIZARD: a guided first watch that takes a
newcomer by the hand through every role's real actions on the live DEMO
wallet. Each step card is camouflaged in the color of the role being
lived, and the matching pane glows - instruction and instrument are
bound by hue, so the learner always knows where to act.
Pure presentation: this module renders strings from the station model;
all live evidence stays inside the panes, which are ordinary cockpit
routes and therefore inherit the read-only guarantee wholesale. The
~60 lines of vanilla JS here do layout only (zoom, reload, step
navigation, glow) - they never touch wallet data.
"""
from __future__ import annotations
from .stations import STATIONS
from .uikit import STYLE, esc
_DECK_EXTRA_STYLE = """
body.deckbody{max-width:none;margin:0;padding:0;height:100vh;display:flex;
flex-direction:column;overflow:hidden}
.deckbar{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;
padding:.45rem .9rem;background:#1c2430;color:#e8ecf2;font-size:.82rem}
.deckbar a{color:#aebcf0;text-decoration:none}
.deckbar .mono{opacity:.75}
.deckgrid{flex:1;display:grid;grid-template-columns:minmax(0,1fr) 21.5rem;
min-height:0}
.deckmain{display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr;
gap:6px;padding:6px;min-height:0;overflow:auto}
@media(min-width:1500px){.deckmain{grid-template-columns:1fr 1fr 1fr}}
.pane{display:flex;flex-direction:column;border:1px solid var(--line);
border-left:4px solid var(--role);border-radius:6px;background:#fff;
min-height:13rem;min-width:0}
.pane header{display:flex;align-items:center;gap:.45rem;padding:.22rem .5rem;
font-size:.78rem;background:var(--roletint);border-radius:0 5px 0 0}
.pane header .q{color:var(--ink2);font-size:.7rem;font-style:italic;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.pane header .monogram{min-width:1.5rem;height:1.5rem;line-height:1.5rem;
font-size:.68rem}
.paneacts{display:flex;gap:.25rem}
.paneacts button,.paneacts a{border:1px solid var(--line);background:#fff;
color:var(--ink);border-radius:4px;font-size:.72rem;line-height:1.25;
padding:.05rem .4rem;cursor:pointer;text-decoration:none}
.pane iframe{flex:1;border:0;width:100%;min-height:0;border-radius:0 0 5px 5px}
.pane.focus{outline:3px solid var(--role);outline-offset:-1px}
.deckmain.zoom .pane{display:none}
.deckmain.zoom .pane.zoomed{display:flex;grid-column:1/-1;grid-row:1/-1}
.wizard{border-left:1px solid var(--line);background:#fff;overflow-y:auto;
padding:.8rem .9rem;min-height:0}
.wizard h2{margin:.1rem 0 .3rem;font-size:1rem}
.wizlegend{display:flex;gap:.3rem;flex-wrap:wrap;margin:.4rem 0 .7rem}
.wizlegend span{font-size:.66rem;font-weight:700;border-radius:5px;
padding:.1rem .4rem;background:var(--roletint);color:var(--role)}
.wizstep{display:none;border-left:4px solid var(--role);
background:var(--roletint);border-radius:6px;padding:.65rem .8rem;
font-size:.86rem}
.wizstep.on{display:block}
.wizstep .rolechip{display:inline-block;font-weight:800;font-size:.7rem;
letter-spacing:.04em;color:var(--role);margin-bottom:.25rem}
.wizstep h3{margin:.1rem 0 .35rem;font-size:.92rem}
.wizstep .succ{margin:.45rem 0 0;font-size:.8rem}
.wizstep .succ b{color:var(--ok)}
.wizstep .learn{margin:.35rem 0 0;font-size:.8rem;color:var(--ink2);
font-style:italic}
.wizctl{display:flex;align-items:center;gap:.6rem;margin:.7rem 0 0}
.wizctl button{padding:.35rem .8rem;font-size:.84rem}
.wizctl button:disabled{opacity:.4;cursor:default}
#wizprog{font-size:.78rem;color:var(--ink2)}
@media(max-width:999px){
body.deckbody{overflow:auto;height:auto}
.deckgrid{display:flex;flex-direction:column-reverse}
.deckmain{grid-template-columns:1fr}
.pane{min-height:22rem}
.wizard{border-left:0;border-bottom:2px solid var(--line)}
}
"""
# The guided first watch. Each step: (role_id, title, do_html, success_html,
# learn_html). The step card wears the role's colors; the matching pane glows.
WIZARD_STEPS: list[tuple[str, str, str, str, str]] = [
("newcomer", "Welcome to the deck",
"Six panes, six roles, all live at the same time — a real crew works in "
"parallel, and so does this deck. The colors ARE the roles; this wizard "
"wears the color of whoever you are acting as, and that role's pane "
"glows. Nothing you do here can sign or change anything — every pane is "
"read-only evidence.",
"You can point at each pane and say what its role is for (the italic "
"line in each pane header helps).",
"One system, six distinct jobs — never one blurred job."),
("operator", "Morning watch: probe everything",
"Act as the Operator (green pane): press "
"«Probe now» on the liveness board. Watch the rows fill "
"with live facts. Find the log head row and read its "
"tree_size — that number is the public transparency log "
"answering you, right now.",
"Every service row says alive with observed facts and latency; "
"the repo rows show HEAD commits.",
"Liveness is checked on demand, never assumed — and never silently in "
"the background."),
("operator", "Read the wallet's own pulse",
"Still in green: scroll to Custody latch — it says "
"unlatched, and the panel explains what the latch would do. "
"Then Recorded history: this demo wallet carries "
"1 incident and 1 refusal receipt on file. You will "
"meet both in the next steps.",
"You found the latch state and the two counters without leaving the "
"green pane.",
"The operator reads state from evidence panels, not from memory."),
("proposer", "Live the proposer: find your request",
"Switch hats — amber pane. One signing request sits in "
"the queue, awaiting device. Read its payload "
"fingerprint: that is the SHA-256 of the exact bytes the "
"offline signer would sign — and nothing else.",
"You can quote the first characters of the fingerprint of what would be "
"signed.",
"A proposal is precise, or it is nothing."),
("proposer", "Read a refusal like a to-do list",
"Still amber: open Incidents (refusals) from the "
"station's instrument links. The demo refusal says "
"POLICY_DENIED, missing allowlisted destination, "
"with a remediation naming exactly what would make the "
"same request succeed.",
"You can say, in one sentence, what the proposer would fix before "
"retrying.",
"warden never says a bare «no» — every refusal is machine-readable "
"instructions."),
("quorum", "Sit on the bench: find the dissenter",
"Now the indigo pane. Count the seats: four, each "
"built from a different verified codebase. The demo incident (you saw "
"its counter in step 3) records that risc0 answered "
"INVALID while the other three said OK — in a real wallet, that single "
"dissent freezes custody on the spot.",
"You found all four seats and can name the dissenting member.",
"One honest dissenter beats three comfortable agreements — that is the "
"whole bench."),
("cryptographer", "Recompute — never trust",
"The purple pane: click «load the sample "
"evidence», then Verify (read-only). The "
"deployed verifier re-checks the signatures and the log inclusion in "
"front of you. Then delete one character from the receipt box and "
"verify again — watch it refuse, loudly.",
"First run: ACCEPTED with a diagnostics list. Broken run: "
"REJECTED, and the diagnostics name the exact failing check.",
"You never trusted this page — you recomputed it. That is the "
"cryptographer's entire posture."),
("architect", "Zoom out: map versus territory",
"The slate pane: read the drift "
"tripwire — it just compared the two renderings of the estate "
"map, name by name, and they agree. Then open the Estate "
"map link and find this wallet's place among the repos, "
"services, and the two self-referential loops.",
"You can say where warden sits on the map, and what the tripwire would "
"catch.",
"The map is recomputed, never remembered — or it lies."),
("operator", "The handoff lap — the team in one incident",
"Watch the whole crew move once, in your head, across the panes: the "
"proposer escalates a refusal → the operator "
"investigates and probes → the bench defends its dissent → "
"the cryptographer re-verifies the evidence → the "
"architect records what changed. Five roles touched one "
"incident — and not one of them did another's job.",
"You can retell the lap naming who hands what to whom.",
"Teamwork here is handoffs between distinct roles — never a blur."),
("newcomer", "Graduation",
"Pick the station that felt most like you and open it full-size (the "
"↗ in its pane header). Read its Mission, its Duties — every one a "
"real command — and its «This station never…» list; the never-list is "
"the fastest way to understand a role. When you are ready for a real "
"wallet: pacta wallet init.",
"You have a station — and you know what it never does.",
"If any page confused you on this watch, that is a bug in the page, "
"not in you. Report it — that is the newcomer's superpower."),
]
def render_deck(wallet_dir: str) -> str:
"""The full deck document: bar, pane grid, wizard rail, layout JS."""
demo_badge = (' DEMO WALLET — custody-inert'
if "DEMO" in wallet_dir else "")
panes = "".join(
f'
Success looks like: {success}
' f'{learn}
' f'