/* Bundled variable fonts — no internet dependency. The three files cover the full
   weight + width range the design uses (via font-variation-settings: 'wdth' NNN).
   URLs are quoted because the filenames contain commas. */
@font-face{
  font-family:'Archivo';
  src:url("fonts/Archivo-VariableFont_wdth,wght.ttf") format("truetype-variations"),
      url("fonts/Archivo-VariableFont_wdth,wght.ttf") format("truetype");
  font-weight:100 900; font-stretch:62% 125%; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Archivo';
  src:url("fonts/Archivo-Italic-VariableFont_wdth,wght.ttf") format("truetype-variations"),
      url("fonts/Archivo-Italic-VariableFont_wdth,wght.ttf") format("truetype");
  font-weight:100 900; font-stretch:62% 125%; font-style:italic; font-display:swap;
}
@font-face{
  font-family:'Martian Mono';
  src:url("fonts/MartianMono-VariableFont_wdth,wght.ttf") format("truetype-variations"),
      url("fonts/MartianMono-VariableFont_wdth,wght.ttf") format("truetype");
  font-weight:100 800; font-stretch:75% 112%; font-style:normal; font-display:swap;
}

:root{
  --line-f:#E8402A; --line-r:#3B57C4; --line-c:#00A277; --line-m:#F2A007;
  --ease:cubic-bezier(.16,.84,.34,1);
  --rail:1px;
  --pad:clamp(14px,2.4vw,28px);
}
[data-theme="dark"]{
  --bg:#0A0B0D; --bg-2:#101215; --ink:#F4F6F8; --ink-2:#8B939C; --ink-3:#4A5058;
  --hair:#22262B; --node-fill:#0A0B0D; --panel:rgba(16,18,21,.86); --shadow:0 0 0 1px #22262B;
}
[data-theme="light"]{
  --bg:#E9EBEA; --bg-2:#F4F5F4; --ink:#14171A; --ink-2:#5C6469; --ink-3:#636A6E;
  --hair:#C9CECD; --node-fill:#F4F5F4; --panel:rgba(244,245,244,.9); --shadow:0 0 0 1px #C9CECD;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
html,body{height:100%;overflow:hidden;overscroll-behavior:none}
body{background:var(--bg);color:var(--ink);font-family:'Archivo',system-ui,sans-serif;
  font-variation-settings:'wdth' 100; transition:background .5s var(--ease),color .5s var(--ease)}
button{font:inherit;color:inherit;background:none;border:none;cursor:pointer}
.mono{font-family:'Martian Mono','SFMono-Regular',monospace;font-weight:400}
.eyebrow{font-family:'Martian Mono',monospace;font-size:9px;font-weight:400;letter-spacing:.22em;
  text-transform:uppercase;color:var(--ink-3)}

#app{position:fixed;inset:0;display:flex;flex-direction:column}
.canvas-wrap{position:relative;flex:1;min-height:0}
canvas{display:block;width:100%;height:100%;touch-action:none}

/* ---------- top nav (index only) ---------- */
.topnav{display:flex;gap:0;align-items:stretch;background:var(--panel);box-shadow:var(--shadow)}
.topnav a{padding:9px 14px 8px;font-family:'Martian Mono',monospace;font-size:9px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-3);border-right:1px solid var(--hair);text-decoration:none}
.topnav a:last-child{border-right:none}
.topnav a:hover{color:var(--ink)}

/* ---------- quick nav (shared, auto-hiding) ---------- */
.nav-handle{position:fixed;top:0;left:50%;transform:translateX(-50%);width:52px;height:14px;
  padding:0;border:none;cursor:pointer;background:var(--panel);box-shadow:var(--shadow);
  border-radius:0 0 8px 8px;z-index:95;opacity:.4;transition:opacity .2s}
.nav-handle::before{content:"";position:absolute;left:50%;top:5px;transform:translateX(-50%);
  width:22px;height:3px;border-radius:2px;background:var(--ink-3)}
.nav-handle:hover{opacity:.9}
.quicknav{position:fixed;top:0;left:50%;transform:translateX(-50%) translateY(-110%);z-index:96;
  display:flex;gap:0;align-items:stretch;background:var(--panel);backdrop-filter:blur(12px);
  box-shadow:var(--shadow);border-radius:0 0 6px 6px;opacity:0;pointer-events:none;
  transition:opacity .25s var(--ease),transform .25s var(--ease)}
.quicknav.on{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}
.quicknav a,.quicknav button{padding:10px 15px 9px;font-family:'Martian Mono',monospace;font-size:10px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--ink-2);border-right:1px solid var(--hair);
  text-decoration:none;white-space:nowrap;background:none}
.quicknav > *:last-child{border-right:none}
.quicknav a:hover,.quicknav button:hover{color:var(--ink)}
.quicknav a.current{color:var(--ink);background:rgba(127,127,127,.1)}
.quicknav .nav-div{width:1px;background:var(--hair);margin:6px 2px;padding:0}

/* ---------- projection ---------- */
/* Title left, live-sync read-out right. The wall has no free corner otherwise:
   the stats sit centred on the right edge, the legend on the left, and the
   bottom third is the bulletin bar. */
.proj-head{position:absolute;top:0;left:0;right:0;z-index:5;padding:var(--pad);pointer-events:none;
  display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
.proj-title{display:inline-block;font-size:clamp(11px,1.1vw,15px);font-weight:700;letter-spacing:.02em;
  font-variation-settings:'wdth' 118;text-transform:uppercase;line-height:1.1}
.proj-title small{display:block;font-weight:400;font-size:.72em;color:var(--ink-3);letter-spacing:.18em;
  font-variation-settings:'wdth' 100;margin-top:4px}
.proj-stats-side{position:absolute;right:var(--pad);top:50%;transform:translateY(-50%);z-index:5;
  text-align:right;pointer-events:none}
.proj-stats-side .stat{margin-bottom:16px}
.proj-stats-side .stat:last-child{margin-bottom:0}
.proj-stats-side b{display:block;font-size:22px;line-height:1.2;font-weight:700;color:var(--ink);
  font-variation-settings:'wdth' 110}
.proj-stats-side span{display:block;font-family:'Martian Mono',monospace;font-size:9px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-2);margin-top:2px}
.bulletin-bar{position:relative;flex:none;height:clamp(148px,20vh,206px);padding:var(--pad);border-top:var(--rail) solid var(--hair);
  display:flex;flex-direction:column;justify-content:center;gap:9px;overflow:hidden;
  transition:height .35s var(--ease), padding .35s var(--ease), border-top-width .35s var(--ease), opacity .3s var(--ease)}
.bulletin-bar.collapsed{height:0!important;padding-top:0!important;padding-bottom:0!important;
  border-top-width:0!important;opacity:0}
.bulletin-bar .bar{display:flex;align-items:flex-start;gap:14px}
.bulletin-bar .tag{flex:none;margin-top:3px;font-family:'Martian Mono',monospace;font-size:9px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--bg);background:var(--ink);padding:4px 7px 3px}
.bulletin-bar .line{font-size:clamp(19px,3.1vw,40px);line-height:1.15;font-weight:800;
  font-variation-settings:'wdth' 122;letter-spacing:-.012em;text-transform:uppercase;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;
  /* text-wrap: balance splits the two lines evenly so a single trailing word
     doesn't dangle on the second line. Modern browsers (Chrome 114+, Safari
     17.4+, Firefox 121+); older ones fall back to normal word wrap. */
  text-wrap:balance}
.bulletin-bar .line .hi{color:var(--accent,var(--ink))}
.bulletin-bar .sub{font-family:'Martian Mono',monospace;font-size:10px;letter-spacing:.1em;color:var(--ink-3);
  text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* The incoming-cast loader. Six seconds of visible "something is arriving"
   before the path draws — enough for whoever just cast it to look up, read the
   wall, and still catch their own route being drawn. Without it the drawing
   started while they were still turning round, and the moment they came for
   had already happened. The bar is the clock: it says how long, so the wait
   reads as anticipation rather than as the wall having stalled. */
.cast-loader{position:absolute;left:0;right:0;bottom:0;height:4px;background:var(--hair)}
.cast-loader span{display:block;height:100%;width:0;
  background:var(--accent,var(--ink))}
.cast-loader.run span{animation:cast-fill var(--castdur,6000ms) cubic-bezier(.4,0,.5,1) forwards}
@keyframes cast-fill{from{width:0}to{width:100%}}
/* The stations tick up as the loader runs — a countable thing on screen, so the
   wait has visible progress in words as well as in a bar. */
.bulletin-bar .sub b{color:var(--accent,var(--ink));font-weight:700}
@media (prefers-reduced-motion:reduce){
  .cast-loader.run span{animation:none;width:100%}
}

/* Cast-celebration flair — fires once the moment a new path lands. A brief
   accent-tinted flash across the bar (matching the anchor's line colour via
   the same --accent variable the .hi span already uses), and a punch on the
   tag pill so it reads instantly as a distinct beat, not just another bulletin. */
.bulletin-bar.celebrate .tag{background:var(--accent,var(--ink));color:var(--bg)}
.bulletin-bar.celebrate-flash{
  animation: cast-flash 1.6s cubic-bezier(.16,.84,.34,1);
}
.bulletin-bar.celebrate-flash .tag{
  animation: cast-tag-punch .55s cubic-bezier(.16,.84,.34,1);
}
@keyframes cast-flash{
  0%   { background: color-mix(in srgb, var(--accent, var(--ink)) 22%, transparent); }
  60%  { background: color-mix(in srgb, var(--accent, var(--ink)) 8%,  transparent); }
  100% { background: transparent; }
}
@keyframes cast-tag-punch{
  0%   { transform: scale(1.28); letter-spacing: .34em; }
  60%  { transform: scale(1.05); letter-spacing: .22em; }
  100% { transform: scale(1);    letter-spacing: .2em;  }
}
.legend{position:absolute;left:var(--pad);bottom:var(--pad);z-index:5;
  display:flex;flex-direction:column;gap:5px;pointer-events:none}
.legend div{display:flex;align-items:center;gap:7px;font-family:'Martian Mono',monospace;font-size:9px;
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-2)}
.legend i{width:16px;height:3px;display:block}
.legend .legend-title{color:var(--ink-3);font-size:9px;letter-spacing:.24em;
  margin-bottom:2px;padding-bottom:5px;border-bottom:1px solid var(--hair);
  display:block;font-weight:700}

/* ---------- tablet / kiosk ---------- */
.kiosk{display:flex;flex-direction:column;height:100%}
/* Header is centered above the map so the prompt and instructions catch the eye
   the moment someone steps up. Old layout had them left-aligned in a narrow
   column — participants were reading the map and never noticing the ask. */
.kiosk-head{padding:14px var(--pad) 8px;text-align:center;flex:0 0 auto}
/* Both phase variants of the prompt/instructions render stacked in the same grid cell.
   The cell sizes itself to the TALLER of the two, permanently — so switching between them
   never changes the header's height, which would otherwise resize the canvas beneath it
   and visibly shift every station on screen the moment a phase changes. */
.prompt-slot, .instructions-slot{display:grid}
.prompt-slot > *, .instructions-slot > *{grid-area:1/1}
/* The kiosk header is deliberately restrained: every pixel it takes is a pixel
   the station map loses, and the map is the thing people actually touch. Sizes
   are clamped against viewport HEIGHT as well as width so a short landscape
   tablet gets a compact header without needing its own breakpoint. */
.prompt{font-size:clamp(17px,min(2.1vw,3.4vh),26px);font-weight:800;font-variation-settings:'wdth' 116;
  letter-spacing:-.01em;line-height:1.1}
.prompt em{font-style:normal;color:var(--ink-3)}
.instructions{margin:7px auto 0;font-family:'Martian Mono',monospace;
  font-size:clamp(10px,min(1.05vw,1.6vh),12.5px);letter-spacing:.03em;
  color:var(--ink-2);line-height:1.5;max-width:70ch}
.instructions b{color:var(--ink);font-weight:600}
.phase-hidden{visibility:hidden}
.progress-track{display:flex;gap:4px;margin-top:10px}
.progress-track i{flex:1;height:3px;background:var(--hair);display:block}
.progress-track i.on{background:var(--ink)}
.undo-row{justify-content:center}
.kiosk-foot{padding:9px var(--pad);display:flex;gap:10px;align-items:center;
  border-top:var(--rail) solid var(--hair);flex:0 0 auto}
/* Short landscape screens (the Android kiosk tablet): squeeze the chrome
   further so the map gets essentially the whole viewport. */
@media (orientation:landscape) and (max-height:820px){
  .kiosk-head{padding:9px var(--pad) 5px}
  .instructions{margin-top:5px}
  .progress-track{margin-top:7px}
  .kiosk-head .undo-row{margin-top:7px!important}
  .kiosk-foot{padding:7px var(--pad)}
  .btn{padding:10px 18px}
  .btn.small{padding:7px 12px}
}
.route-strip{flex:1;min-width:0;display:flex;align-items:center;gap:6px;overflow:hidden;
  font-family:'Martian Mono',monospace;font-size:10px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-2);white-space:nowrap}
.route-strip .stop{display:inline-flex;align-items:center;gap:6px}
.route-strip .dot{width:7px;height:7px;border-radius:50%;flex:none}
.route-strip .arw{color:var(--ink-3)}
.route-strip .empty{color:var(--ink-3)}
.btn{flex:none;padding:13px 22px;font-family:'Martian Mono',monospace;font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;background:var(--ink);color:var(--bg);transition:opacity .2s,transform .12s;
  display:inline-flex;align-items:center;justify-content:center;gap:8px}
.btn .icon{width:14px;height:14px}
.btn:active{transform:translateY(1px)}
.btn[disabled]{opacity:.22;pointer-events:none}
.btn.ghost{background:none;color:var(--ink-2);box-shadow:inset 0 0 0 1px var(--hair)}
.btn.small{padding:9px 14px;font-size:10px}

/* ---------- preview / constellation modal ---------- */
.modal-veil{position:fixed;inset:0;z-index:50;background:rgba(0,0,0,.5);opacity:0;pointer-events:none;
  transition:opacity .3s var(--ease);display:flex;align-items:center;justify-content:center;padding:20px 16px}
.modal-veil.on{opacity:1;pointer-events:auto}
/* The modal is a flex column with a hard viewport-relative height cap. The
   print-area (constellation + list + keepsake + credits) scrolls internally;
   the foot with the buttons stays pinned at the bottom so the participant
   can always reach Print/Done without hunting. dvh is used to handle mobile
   Safari's URL bar collapsing correctly — vh includes it, dvh doesn't. */
/* A FIXED height, not just a cap. The modal is used twice in a row for the
   same person: preview, then the cast receipt with the QR and path code added.
   Sized to the content, that second state was 235px taller — and because the
   veil centres the modal, the constellation someone was looking at jumped 118px
   up the screen the instant they committed. Pinning the height makes the two
   states the same box; the constellation below flexes to absorb the difference,
   so the QR block slides in underneath and nothing else moves at all. */
.modal{width:min(560px,94vw);position:relative;
  height:min(760px, calc(100vh - 40px));
  height:min(760px, calc(100dvh - 40px));
  max-height:calc(100vh - 40px);
  max-height:calc(100dvh - 40px);
  display:flex;flex-direction:column;
  background:var(--bg);box-shadow:0 0 0 1px var(--hair),0 30px 80px rgba(0,0,0,.5);
  transform:translateY(10px) scale(.98);transition:transform .3s var(--ease)}
.modal-veil.on .modal{transform:translateY(0) scale(1)}
/* Flex column so the constellation can be the part that gives. Still scrolls
   when even the minimum heights don't fit (short landscape viewports). */
.print-area{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;display:flex;flex-direction:column}
.modal-foot{flex:0 0 auto;border-top:1px solid var(--hair)}
/* Close affordance for the cast receipt — the modal no longer times out, so a
   deliberate dismiss control is the only way back to the kiosk. Sized for a
   fingertip, not a mouse pointer. */
.modal-close{position:absolute;top:10px;right:10px;z-index:4;width:40px;height:40px;
  display:inline-flex;align-items:center;justify-content:center;background:none;
  color:var(--ink-3);box-shadow:inset 0 0 0 1px var(--hair)}
.modal-close:hover, .modal-close:active{color:var(--ink)}
.modal-head{padding:20px 56px 0 22px}
.modal-head h3{font-size:20px;font-weight:800;font-variation-settings:'wdth' 118;text-transform:uppercase;line-height:1.15}
.modal-head p{margin-top:6px;font-family:'Martian Mono',monospace;font-size:10.5px;color:var(--ink-2);letter-spacing:.04em}
/* Grows into whatever the rest of the modal isn't using, down to a floor that
   still reads as a drawing. This is what absorbs the keepsake block. */
.constellation-wrap{position:relative;margin:16px 22px;flex:1 1 auto;
  min-height:180px;background:#05060700 ;
  box-shadow:inset 0 0 0 1px var(--hair)}
/* Landscape (horizontal iPad, wide-and-short viewports) — widen the modal so
   the constellation stays legible AND leaves room for the QR keepsake below
   without demanding a scroll. Constellation gets a smaller vh cap in landscape
   so the QR block is always visible in the viewport. */
@media (orientation: landscape) and (min-width: 900px){
  .modal{width:min(820px,94vw)}
}
/* Narrow screens: the keepsake block is the same height but there is far less
   to give, so the drawing's floor has to come down or the receipt starts
   scrolling — which reintroduces exactly the shift this is here to prevent. */
@media (max-width: 460px){
  .constellation-wrap{min-height:150px}
  /* One line for "Your constellation" too. At 20px it wrapped and the heading
     for the cast state ("Your path") did not, so committing shortened the head
     by a line and pulled the drawing up with it. */
  .modal-head h3{font-size:18px}
  /* Same reasoning as the short-landscape block: a 140px QR beside its text is
     most of a phone's width, and every pixel it gives back is a pixel the
     drawing keeps. Still comfortably scannable. */
  .keepsake{padding:10px}
  .keepsake canvas{width:108px!important;height:108px!important}
}
@media (orientation: landscape) and (max-height: 720px){
  .constellation-wrap{min-height:130px}
  /* Right padding must stay clear of the close button, which sits in the
     same corner — hence 56px here too, not the flat 22px. */
  .modal-head{padding:14px 56px 0 22px}
  .modal-head h3{font-size:17px}
  .keepsake{padding:10px}
  .keepsake canvas{width:100px!important;height:100px!important}
}
.modal-list{margin:0 22px 4px;display:flex;flex-wrap:wrap;gap:6px}
.modal-list span{display:inline-flex;align-items:center;gap:6px;font-family:'Martian Mono',monospace;
  font-size:10px;letter-spacing:.08em;text-transform:uppercase;padding:5px 9px;box-shadow:inset 0 0 0 1px var(--hair)}
.modal-list i{width:7px;height:7px;border-radius:50%}
.modal-foot{padding:16px 22px 22px;display:flex;gap:8px;flex-wrap:wrap}
.modal-foot .btn{flex:1;min-width:120px;text-align:center}

/* receipt state (modal reused after posting) */
.stub-meta{margin:0 22px 14px;display:flex;justify-content:space-between;font-family:'Martian Mono',monospace;
  font-size:9px;color:var(--ink-3);letter-spacing:.1em;border-top:1px dashed var(--hair);padding-top:12px}

/* Keepsake block — the call to action, only shown after casting.
   A two-column grid (QR | text) with the heading spanning both. Print collapses
   it to a single column, so the three children stack in DOM order without any
   reordering: heading, QR, code. Note tablet.html sets display:grid inline when
   the QR is ready, so print must never force display here. */
.keepsake{margin:0 22px 18px;display:grid;grid-template-columns:auto minmax(0,1fr);
  gap:8px 14px;align-items:center;padding:12px;box-shadow:inset 0 0 0 1px var(--hair)}
.keepsake-cta{grid-column:1/-1;font-family:'Martian Mono',monospace;font-size:9px;
  font-weight:400;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-3)}
/* The kiosk shows no QR. The receipt in the participant's hand carries the same
   one, and the operator hands that over — a second copy on the glass only made
   the modal taller and invited people to scan a tablet they were about to give
   back. It is still printed; this hides it on screen only. The path code stays
   visible, because if the printer jams that box is the only thing left. */
.keepsake canvas{display:none}
@media print{ .keepsake canvas{flex:none;background:#fff;padding:6px;display:block} }
.keepsake{grid-template-columns:minmax(0,1fr)}
@media print{ .keepsake{grid-template-columns:auto minmax(0,1fr)} }
.keepsake-text{min-width:0}
.keepsake-text p{margin-top:6px;font-family:'Martian Mono',monospace;font-size:10.5px;
  line-height:1.55;color:var(--ink-2)}
.keepsake-url{display:block;margin-top:8px;font-family:'Martian Mono',monospace;font-size:9.5px;
  color:var(--ink-3);letter-spacing:.02em;word-break:break-all;text-decoration:none}
.keepsake-url:hover{color:var(--ink)}

.keepsake-text .print-only,
.keepsake-cta .print-only{display:none}

/* On screen the keepsake is one centred column: the QR that used to sit beside
   this text is printed only, so a left-aligned block now hangs off the side of
   an empty grid cell. */
.keepsake{justify-items:center;text-align:center}
.keepsake-text{text-align:center}
.keepsake .pathcode{align-items:center}
@media print{
  .keepsake{justify-items:stretch;text-align:left}
  .keepsake-text{text-align:center}
}

/* The path code — the half of the receipt that gets typed rather than scanned.
   Boxed and letter-spaced because it is read off thermal paper, often in a dark
   room, and then keyed into a phone. */
.pathcode{margin-top:10px;display:inline-flex;flex-direction:column;gap:3px;
  padding:7px 11px;box-shadow:inset 0 0 0 1px var(--hair)}
.pathcode-label{font-family:'Martian Mono',monospace;font-size:8px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--ink-3)}
.pathcode b{font-family:'Martian Mono',monospace;font-size:15px;font-weight:700;
  letter-spacing:.14em;color:var(--ink);line-height:1.1}
.codehint{margin-top:7px;font-family:'Martian Mono',monospace;font-size:9.5px;
  line-height:1.5;color:var(--ink-3)}

/* credits — shared by view.html and the tablet's print block. On screen the block
   is compact and low-contrast; in print it's slightly stronger. */
.credits{font-family:'Martian Mono',monospace;font-size:10px;line-height:1.7;
  letter-spacing:.03em;color:var(--ink-3)}
.credits .row{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
.credits b{color:var(--ink-2);font-weight:600}
.credits a{color:var(--ink-2);text-decoration:none}
.credits a:hover{color:var(--ink)}
/* When the whole row is an anchor, give it a mobile-safe tap target — a stroked
   underline plus enough vertical padding that a fingertip actually lands. The
   old markup nested a div inside an anchor which some phones treated as a
   zero-height link, hence "not tappable". */
.credits a.row-link{padding:8px 0;border-bottom:1px dashed var(--hair);
  border-top:1px dashed var(--hair);margin-top:4px}
.credits a.row-link:active{color:var(--ink);background:rgba(127,127,127,.06)}
.print-credits{display:none}       /* only shown in print via @media print */
@media print{ .credits{color:#333} .credits b{color:#111} .credits a{color:#111} }

/* -------- print --------
   Tuned for 80mm POS thermal receipt paper (~72mm printable width). The @page
   size declaration is what makes the browser lay out for the narrow strip
   instead of trying to fit A4/Letter into 72mm and either scaling or clipping.
   All font sizes are in pt so the browser doesn't reflow relative to viewport.
   The keepsake block (QR + URL) is stacked vertically because 72mm is too
   narrow for the side-by-side layout used on screen.
   ---
   ALSO — the modal has a CSS transform on it (translateY+scale entrance
   animation). A transform makes the modal a *containing block* for any
   fixed-position descendants, so the old print rules that used position:fixed
   on .print-area were fixed to the (centered) modal, not the page. Result:
   half the page was blank. The fix: neutralize the transform + centering flex
   chain in print, and let .print-area flow as normal block content. */
@media print{
  @page{ size: 72mm auto; margin: 3mm; }
  html,body{background:#fff;color:#000;overflow:visible;height:auto;margin:0;padding:0}
  /* Force pure black on every printable element — thermal heads can't reliably
     print grays; every non-black colour ends up as faint dithered noise. The
     one exception is the constellation stroke, which renderConstellation draws
     as pure #000 in print mode. */
  .print-area *{color:#000!important}
  body *{visibility:hidden}
  .modal-veil{position:static!important;display:block!important;padding:0!important;
    background:none!important;opacity:1!important;pointer-events:auto!important}
  .modal{position:static!important;transform:none!important;box-shadow:none!important;
    width:auto!important;max-width:100%!important;max-height:none!important;
    background:none!important;display:block!important}
  .modal-foot, .modal-list, .stub-meta{display:none!important}
    /* Back to normal block flow — the on-screen flex column exists only to keep
     the modal from jumping, and stretching it across a paper roll of unknown
     length is not something flex should be deciding. */
  .print-area{position:static!important;padding:0!important;background:#fff;
    visibility:hidden;overflow:visible!important;display:block!important}
  .print-area *{visibility:visible}
  .print-area canvas, .print-area svg{filter:none}

  /* Header — title only. Subtitle ("N stations, in order…") hidden for the
     minimal receipt the user asked for. */
  .modal-head{padding:0!important;text-align:center}
  /* Clearly dominant over the station labels inside the drawing (~5pt). At the
     old 14pt against 7.5pt labels the two read as the same level and the
     receipt had no visible hierarchy. */
  .modal-head h3{font-size:15pt;margin:0 0 2.5mm 0;line-height:1.05;letter-spacing:.01em;
    font-weight:800;text-transform:uppercase}
  .modal-head p{display:none!important}
  /* The close affordance is screen-only. */
  .modal-close{display:none!important}

  /* Constellation — no fixed height. renderConstellation in print mode sizes
     the canvas bitmap so its aspect ratio matches the drawing (via inline
     style.width + style.height), and CSS lets the wrap grow to hug it. This
     kills the whitespace that showed up under wide/short paths on receipts.
     The 100%-forcing width/height rules from screen would defeat this — hence
     the width:auto/height:auto here (canvas has explicit inline sizes in print). */
  .constellation-wrap{height:auto!important;min-height:0!important;max-height:none!important;
    margin:2mm 0 3mm 0!important;
    box-shadow:none!important;border:none!important;background:#fff!important;
    line-height:0}
  .constellation-wrap canvas{width:100%!important;height:auto!important;display:block!important}

  /* Keepsake — the call to action. A heading, the QR to the results page, the
     path code in a box, and one line telling them what to do with it. Stacked
     and centred: 66mm is far too narrow for the side-by-side screen layout.
     ---
     Deliberately DO NOT force display on .keepsake itself. tablet.html sets
     inline display:none in openPreview() (no QR generated yet) and display:flex
     in castNow() (QR ready). Forcing display:block in print used to override the
     display:none, printing an empty QR placeholder as a ~40mm blank rectangle
     between the constellation and the credits. */
  .keepsake{margin:3.5mm 0 0!important;padding:0!important;box-shadow:none!important;
    border:none!important;text-align:center;
    grid-template-columns:1fr!important;justify-items:center!important;gap:0!important;
    border-top:1px dashed #000;padding-top:2.5mm!important}
  .keepsake-cta{display:block!important;font-size:8pt!important;font-weight:700!important;
    letter-spacing:.14em!important;color:#000!important;text-transform:uppercase}
  .keepsake canvas{background:#fff!important;display:block!important;
    width:30mm!important;height:30mm!important;margin:1.8mm auto 0!important;padding:0!important}
  .keepsake-text{display:block!important;text-align:center;width:100%}
  .keepsake-text .screen-only{display:none!important}
  .keepsake-text .print-only{display:block}
  .keepsake-cta .screen-only{display:none!important}
  .keepsake-cta .print-only{display:inline}
  .pathcode{display:block!important;margin:3mm auto 0!important;padding:1.5mm 2mm!important;
    box-shadow:none!important;border:1px solid #000!important;width:fit-content}
  .pathcode-label{display:block!important;font-size:6pt!important;letter-spacing:.16em!important;
    color:#000!important}
  .pathcode b{display:block!important;font-size:13pt!important;letter-spacing:.16em!important;
    margin-top:0.6mm}
  .codehint{display:block!important;margin-top:1.6mm!important;font-size:6.5pt!important;
    line-height:1.35!important;color:#000!important}

  /* Credits — one line per row, no IG URL. Small dashed separator above. */
  .print-credits{display:block!important;margin-top:3mm!important;padding-top:2mm!important;
    border-top:1px dashed #000!important;text-align:center}
  .print-credits .row{display:block!important;margin-bottom:0.6mm!important;
    font-size:7pt;line-height:1.4;overflow-wrap:anywhere}
  .print-credits b{font-weight:700}
}

/* ---------- VJ ---------- */
/* Two columns on wide screens: controls on the left (settings, wall view, theme,
   data), read-outs on the right (Tonight in numbers, Bulletin queue). The bulletin
   queue can grow tall — putting it in its own column stops it from pushing every
   control off the top of the viewport. Collapses to a single column below 960px. */
.vj{padding:var(--pad);overflow-y:auto;height:100%;display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:28px;
  max-width:1280px;margin:0 auto;width:100%;align-items:start}
.vj-col{display:flex;flex-direction:column;gap:22px;min-width:0}
@media (max-width:960px){
  .vj{grid-template-columns:1fr;max-width:760px;gap:22px}
}
.vj h2{font-size:13px;font-weight:700;letter-spacing:.02em;font-variation-settings:'wdth' 118;
  text-transform:uppercase;margin-bottom:10px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:7px}
.chip{padding:11px 12px;text-align:left;box-shadow:inset 0 0 0 1px var(--hair);font-family:'Martian Mono',monospace;
  font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-2);transition:.18s}
.chip:hover{color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--ink);color:var(--bg);box-shadow:none}
.chip b{display:block;font-size:9px;color:var(--ink-3);font-weight:400;margin-top:4px;letter-spacing:.06em}
.chip[aria-pressed="true"] b{color:var(--bg);opacity:.6}
.chip.danger{box-shadow:inset 0 0 0 1px var(--line-f)}
.chip.danger:hover{color:var(--line-f)}
.readout{font-family:'Martian Mono',monospace;font-size:10px;line-height:1.9;color:var(--ink-2)}
.readout b{color:var(--ink);font-weight:600}
.row{display:flex;gap:7px;flex-wrap:wrap}
.status-pill{display:inline-flex;align-items:center;gap:6px;font-family:'Martian Mono',monospace;font-size:9px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--ink-2)}
.status-pill i{width:6px;height:6px;border-radius:50%;background:var(--ink-3)}
.status-pill.remote i{background:var(--line-c)}

/* ---------- live sync pill (assets/syncpill.js) ----------
   Deliberately low-contrast when healthy — it sits on the wall and on the
   kiosk all night and must not compete with either. It only earns attention
   when it has something to report. */
.sync-pill{position:fixed;z-index:60;display:inline-flex;align-items:center;gap:7px;
  padding:6px 10px;background:var(--panel);backdrop-filter:blur(8px);
  box-shadow:inset 0 0 0 1px var(--hair);
  font-family:'Martian Mono',monospace;font-size:9px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);pointer-events:none;
  transition:color .3s var(--ease),box-shadow .3s var(--ease),background .3s var(--ease)}
.sync-pill.inline{position:static;align-items:center}
.sync-pill.bl{left:var(--pad);bottom:var(--pad)}
.sync-pill.br{right:var(--pad);bottom:var(--pad)}
.sync-pill.tl{left:var(--pad);top:var(--pad)}
.sync-pill i{width:6px;height:6px;border-radius:50%;background:var(--ink-3);flex:none}
.sync-pill[data-state="live"] i{background:var(--line-c)}
.sync-pill[data-state="waiting"] i{background:var(--line-m);animation:sync-blink 1.5s ease-in-out infinite}
.sync-pill[data-state="holding"]{color:var(--ink)}
.sync-pill[data-state="holding"] i{background:var(--line-f);animation:sync-blink .9s ease-in-out infinite}
/* The kiosk variant. Held casts there mean paper is going out for journeys the
   wall will never show, so this one is meant to be noticed from across a room. */
.sync-pill.loud{background:var(--line-f);color:#fff;box-shadow:none}
.sync-pill.loud i{background:#fff}
/* Under ~560px the kiosk's centred question runs the full width and there is
   no corner left to stand in. Shrink to the dot alone — the colour still says
   everything — but let it speak again the moment it has something wrong to
   report, because at that point being legible beats being tidy. */
@media (max-width:560px){
  .sync-pill.tl .sync-pill-text{display:none}
  .sync-pill.tl{padding:6px}
  .sync-pill.tl.loud{padding:6px 10px}
  .sync-pill.tl.loud .sync-pill-text{display:inline}
}
@keyframes sync-blink{0%,100%{opacity:1}50%{opacity:.25}}

/* ---------- misc ---------- */
.corner-btn{position:absolute;z-index:8;padding:7px 9px;font-family:'Martian Mono',monospace;font-size:9px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);box-shadow:inset 0 0 0 1px var(--hair);
  background:var(--panel);backdrop-filter:blur(8px);display:inline-flex;align-items:center;gap:6px}
.corner-btn:hover{color:var(--ink)}
.corner-btn.icon-only{padding:7px;width:32px;height:32px;justify-content:center}

/* Shared icon styling — every SVG rendered by icons.js sits inside a button
   and inherits currentColor, so nothing needs per-button colour rules. */
.icon{display:inline-block;vertical-align:middle;flex:none}
.quicknav .nav-action{display:inline-flex;align-items:center;justify-content:center;padding:9px 12px}
.quicknav .nav-action .icon{width:16px;height:16px}
.tr{top:var(--pad);right:var(--pad)}
.br{bottom:var(--pad);right:var(--pad)}
.toast{position:absolute;left:50%;bottom:88px;transform:translateX(-50%) translateY(8px);z-index:30;
  padding:8px 13px;background:var(--ink);color:var(--bg);font-family:'Martian Mono',monospace;font-size:10px;
  letter-spacing:.12em;text-transform:uppercase;opacity:0;pointer-events:none;transition:.25s var(--ease)}
.toast.on{opacity:1;transform:translateX(-50%) translateY(0)}

@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;transition-duration:.01ms!important}}
