Draw the board as track, split the site into three pages, and fix curve geometry

Adds SVG board rendering shared by the game and the replay, a splash page with a
shareable replay directory, hover tooltips for reference detail, and makes curves
two-port arcs so sidings and run-arounds can finally be built.
This commit is contained in:
Jesse
2026-08-01 13:35:53 -04:00
parent f00255ce31
commit f52ff0e9ac
21 changed files with 5359 additions and 291 deletions
+59 -114
View File
@@ -3,130 +3,75 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Station Master — solitaire</title>
<title>Station Master</title>
<style>
:root{--bg:#12151a;--fg:#e6e9ee;--dim:#8b94a3;--line:#2c333d;--panel:#1a1f26;--run:#3a4250}
:root{--bg:#12151a;--fg:#e6e9ee;--dim:#8b94a3;--line:#2c333d;--panel:#1a1f26}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);
font:14px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
h1{font-size:17px;margin:0}
h2{font-size:12px;text-transform:uppercase;letter-spacing:.09em;color:var(--dim);
margin:0 0 6px;font-weight:600}
h3{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--dim);margin:9px 0 4px}
.dim{color:var(--dim)}
header{position:sticky;top:0;z-index:5;background:var(--panel);border-bottom:1px solid var(--line);
padding:9px 14px;display:flex;gap:20px;align-items:baseline;flex-wrap:wrap}
header b{font-size:16px}
.build{margin-left:auto;font-size:11px;opacity:.7}
.pace{font-size:12px;padding:1px 7px;border-radius:10px}
.pace.good{background:rgba(40,140,60,.32)}
.pace.behind{background:rgba(190,120,40,.28)}
.handcard{border-top:1px solid var(--line);padding:4px 0}
.handcard:first-child{border-top:0}
.handcard .what{font-size:11px;line-height:1.35}
.handcard.unplayable{opacity:.55}
.tag{font-size:10px;background:rgba(190,120,40,.3);border-radius:3px;padding:0 5px}
main{display:grid;grid-template-columns:minmax(0,1fr) 400px;gap:14px;padding:14px;align-items:start}
@media(max-width:1100px){main{grid-template-columns:1fr}}
section{background:var(--panel);border:1px solid var(--line);border-radius:7px;
padding:10px 12px;margin-bottom:12px}
/* division strip */
#division{display:flex;gap:7px;overflow-x:auto;padding-bottom:4px}
.node{border:1px solid var(--line);border-radius:6px;padding:6px 9px;min-width:112px;flex:0 0 auto}
.node.office{border-color:#4d6fa8;background:rgba(60,100,170,.13)}
.node .nm{font-weight:600;font-size:12px}
.chip{display:inline-block;background:#2f6b3d;border-radius:4px;padding:1px 6px;margin:3px 3px 0 0;font-size:11px}
.consist{font-size:10px;color:var(--dim)}
.grade{font-size:10px;background:rgba(200,90,60,.35);border-radius:3px;padding:1px 4px;font-weight:400}
.mlmods{font-size:10px;background:rgba(200,150,60,.28);border-radius:3px;padding:1px 4px;margin-top:2px}
/* board */
#grid{display:grid;gap:5px;overflow-x:auto}
.cell{border:1px solid var(--line);border-radius:5px;padding:5px 6px;min-height:64px;background:#161b21}
.cell.run{background:var(--run)}
.cell.office{border-color:#4d6fa8}
.cell.modifier{background:#2b2333;border-style:dashed}
.cell.legal{outline:2px solid #5aa9e6;outline-offset:-2px;cursor:pointer;
box-shadow:0 0 0 3px rgba(90,169,230,.18) inset}
.cell.legal:hover{background:#233246}
.cell.ghost{border-style:dashed;background:rgba(90,169,230,.07);
display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center}
.cell.ghost .nm{font-weight:400;color:#5aa9e6;font-size:11px}
.cell .nm{font-size:12px;font-weight:600}
.cell .what{font-size:10px;line-height:1.25;margin-top:2px}
.cell{cursor:help}
.coord{font-size:10px}
.enh{font-size:10px;background:rgba(90,140,220,.30);border-radius:3px;padding:1px 4px;margin-top:2px}
.crew{font-size:11px;margin-top:3px;color:#8fd6a0}
.cars{font-size:10px;color:var(--dim)}
/* facilities */
.fac{border-top:1px solid var(--line);padding:7px 0}
.fac:first-child{border-top:0}
.boxes{display:flex;gap:3px;align-items:center;margin-top:3px;flex-wrap:wrap}
.box{display:inline-block;min-width:22px;text-align:center;border-radius:3px;padding:1px 4px;font-size:10px}
.box.empty{background:#242a32;color:#5a6472}
.box.f{background:#2f6b3d}
.box.m{background:#8a6d1f}
.fstat{margin-top:4px;font-size:11px;padding:2px 5px;border-radius:3px;display:inline-block}
.fstat.good{background:rgba(40,140,60,.28)}
.fstat.bad{background:rgba(190,50,50,.38);font-weight:700}
.fstat.idle{opacity:.6}
/* actions */
#actions{max-height:none}
.grp{margin-bottom:6px}
button{background:#2a3038;color:var(--fg);border:1px solid var(--line);border-radius:5px;
padding:5px 9px;margin:2px 3px 2px 0;cursor:pointer;font:inherit;font-size:12px;text-align:left}
button:hover{background:#39424e;border-color:#4d6fa8}
button.act{display:inline-block}
.over{padding:9px;border-radius:5px;font-weight:700;margin-bottom:8px}
.over.win{background:rgba(40,140,60,.35)}
.over.loss{background:rgba(160,60,60,.3)}
/* cards, log, blocked */
.card.gone{opacity:.35;text-decoration:line-through}
.subj{display:block;width:100%;margin:2px 0}
.subj.open{border-color:#4d6fa8;background:#39424e}
.spots{margin:0 0 6px 12px;padding-left:8px;border-left:2px solid #4d6fa8}
.card{display:inline-block;background:#242c36;border:1px solid var(--line);border-radius:4px;
padding:2px 6px;margin:2px 2px 0 0;font-size:11px}
#log{max-height:230px;overflow:auto;font-size:12px}
.line{padding:1px 0}
.t-good{color:#8fd6a0}.t-bad{color:#e58080}.t-clock{color:#9fb6d8;font-weight:600}.t-quiet{color:var(--dim)}
ul.blocked{list-style:none;margin:0;padding:0;font-size:12px}
ul.blocked li{padding:2px 0}
.sev-warn{color:#e0b060}.sev-stop{color:#e58080}
font:15px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:28px}
main{width:100%;max-width:760px}
h1{font-size:32px;margin:0 0 2px;letter-spacing:.02em}
.tag{color:var(--dim);margin:0;font-size:14px}
.blurb{color:#c6ccd6;margin:16px 0 26px;max-width:66ch}
.rule{height:1px;background:var(--line);margin:22px 0}
.doors{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:640px){.doors{grid-template-columns:1fr}}
a.door{display:block;text-decoration:none;color:inherit;background:var(--panel);
border:1px solid var(--line);border-radius:9px;padding:18px 18px 16px;transition:.12s}
a.door:hover{border-color:#4d6fa8;background:#1f2733;transform:translateY(-1px)}
a.door h2{font-size:17px;margin:0 0 5px;color:#9fb6d8}
a.door p{margin:0;color:var(--dim);font-size:13px;line-height:1.5}
a.door .go{display:inline-block;margin-top:11px;font-size:12px;color:#5aa9e6}
/* the rule between title and blurb is drawn as rail, not decorated */
svg.trackline{width:100%;height:24px;display:block;margin:10px 0 0}
.rail{stroke:#4a5361;stroke-width:1.6}
.tie{stroke:#39424e;stroke-width:1.1}
footer{margin-top:26px;color:var(--dim);font-size:11px;display:flex;gap:18px;flex-wrap:wrap}
</style>
</head>
<body>
<header>
<b>Station Master</b>
<span id="clock">—</span>
<span>phase: <b id="phase">—</b></span>
<span>Revenue <b id="revenue">0</b></span>
<span id="objective" class="pace">—</span>
<span class="dim">seed <span id="seed">—</span></span>
<span class="dim">saved in this browser · add ?seed=1234 to the address for a set deal</span>
<span class="dim build" title="what is actually deployed">__BUILD__</span>
</header>
<main>
<div>
<section><h2>The Division — west to east</h2><div id="division"></div></section>
<section><h2>Your Office Area <span class="dim" style="text-transform:none;letter-spacing:0">— hover any card for the full explanation</span></h2><div id="grid"></div></section>
<section><h2>History</h2><div id="log"></div></section>
<h1>Station Master</h1>
<p class="tag">Timetable-and-train-order railroading, 1840&ndash;1950.</p>
<svg class="trackline" viewBox="0 0 700 24" preserveAspectRatio="none" aria-hidden="true">
<line class="rail" x1="0" y1="10" x2="700" y2="10"/>
<line class="rail" x1="0" y1="15" x2="700" y2="15"/>
<g id="ties"></g>
</svg>
<p class="blurb">
You run a division: a Running Track between your Limits, a district of industries hanging beneath
it, and trains that arrive whether or not you are ready for them. Spot the right cars, work the
loads, and get every train away again &mdash; a train with nowhere to stand is a collision, and a
collision costs more than the freight was worth.
</p>
<div class="doors">
<a class="door" href="./play.html">
<h2>Play solitaire</h2>
<p>Five Days, twelve Stages each, and a target of 20 Revenue. Your game saves in this browser,
so you can close the tab and come back to it.</p>
<span class="go">Start a game &rarr;</span>
</a>
<a class="door" href="./replays.html">
<h2>Replays</h2>
<p>Watch a finished game back, Stage by Stage. Open one saved on this site, or a save file
somebody has sent you.</p>
<span class="go">Browse replays &rarr;</span>
</a>
</div>
<div>
<section><h2>Your move</h2><div id="actions"></div></section>
<section><h2>Cards</h2>
<div><span class="dim">hand:</span> <span id="hand">—</span></div>
<div style="margin-top:5px"><span class="dim">face-up Department slots:</span> <span id="depts">—</span></div>
</section>
<section><h2>Your track supply</h2><div id="supply"></div></section>
<section><h2>Blocked — why nothing is moving</h2><ul class="blocked" id="blocked"></ul></section>
<section><h2>Facilities</h2><div id="facs"></div></section>
</div>
<div class="rule"></div>
<footer>
<span>build <span id="build">__BUILD__</span></span>
<span>runs entirely in your browser &mdash; nothing is sent anywhere</span>
</footer>
</main>
<script type="module" src="./web/main.js"></script>
<script type="module" src="./web/splash.js"></script>
</body>
</html>