:root {
  --bg: #14171c;
  --panel: #1d2128;
  --panel-2: #262b34;
  --line: #333a45;
  --text: #e8ebf0;
  --muted: #8b93a1;
  --accent: #e47507;
  --accent-ink: #1a1200;
  --good: #3ecf6e;
  --bad: #ef5350;
  --sel: #ffffff;
  --src: rgba(255, 255, 255, .9);  /* a territory's ring that can be chosen (--sel: chosen); a light map's are dark (pages.game_page) */
  --tgt: #ff8a00;
  --turn: var(--accent);  /* your turn: card border and status bar, the game's clock and step bar */
  --turn-ink: var(--accent-ink);
  --playing: #f5c518;  /* playing their turn now: the outline on their name, a freshness dot fetching */
  --wait: #5aa9ff;  /* waiting on others: status bar, ENTER when it isn't your turn */
  --radius: 10px;
  color-scheme: dark;
}

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text);
  font: 15px/1.35 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
body { -webkit-tap-highlight-color: transparent; overscroll-behavior: none; }
[hidden] { display: none !important; }
h1 { font-size: 20px; margin: 0; }
.muted { color: var(--muted); }
.small { font-size: 12px; }
.error { color: var(--bad); margin: 0; }
.error:empty, #begin:empty { display: none; }  /* filled by script when there's something to say */
a { color: var(--text); }  /* sitewide; buttons and usernames (.btn, .ulink) style their own */
.error a, .empty a, .msg a { white-space: nowrap; }
.spacer { flex: 1; }

.btn { font: inherit; color: var(--text); background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 8px; padding: 9px 14px; cursor: pointer; touch-action: manipulation; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .4; }
.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 600; }
.btn.danger { background: #4a2326; border-color: #6b2e33; }
.btn.confirm { background: var(--bad); color: #fff; border-color: transparent; }
.btn.ghost { background: transparent; }
.btn.icon { padding: 6px 10px; font-size: 18px; line-height: 1; min-width: 38px; }
/* In the top bar, ☰ and fullscreen are square tiles flush with its right edge, top and bottom:
   the button is the bar's height, so nothing pads it. */
.topbar .btn.icon { width: 37px; height: 37px; min-width: 0; padding: 0; display: grid; place-items: center;
  border: 0; border-left: 1px solid var(--line); border-radius: 0; background: none; }
.topbar .btn.icon + .menu { margin-left: -10px; }  /* the two tiles side by side */
.btn.icon svg { display: block; width: 18px; height: 18px; margin: auto; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }

/* ---- login ---- */
.view.center { min-height: 100%; display: flex; flex-direction: column; }
/* Admin's random-map preview: the map with names and circles laid over it, as in a game. */
.map-preview { padding: 16px; display: grid; gap: 8px; }
.map-frame { position: relative; }
.map-frame > :is(svg, img) { display: block; width: 100%; height: auto; }
.map-frame > .ov { position: absolute; inset: 0; height: 100%; }
.ov-terr { fill: var(--panel-2); stroke: rgba(0, 0, 0, .65); stroke-width: 1.5; }
/* A map's preview: troops on it as a game has them (.terr .badge-c, .badge-e, .terr text) */
.ov-num { font: 700 17px system-ui, sans-serif; text-anchor: middle; dominant-baseline: central; paint-order: stroke; stroke-width: 2.5; stroke-linejoin: round; }
.view.center > .card { margin: auto; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); }
.login { width: min(360px, 100% - 32px); padding: 24px; display: grid; gap: 12px; }
.login p { margin: 0; }
input { font: inherit; color: var(--text); background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; padding: 10px 12px; width: 100%; }

/* ---- games ---- */
.topbar { display: flex; align-items: center; gap: 10px; padding: 0 0 0 6px; border-bottom: 1px solid var(--line);  /* 6px: the logo (25px) centred in the 37px bar */
  position: sticky; top: 0; background: var(--bg); z-index: 1; }
/* Where the page runs under the phone's status bar (viewport-fit=cover; on iOS, from the Home
   Screen), the browser reports how far as the safe-area insets: the bar's colour runs up under
   it, its contents below. Zero everywhere else. */
.topbar { padding-top: env(safe-area-inset-top, 0px); }
.games { display: grid; gap: 12px; padding: 16px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.game-card { padding: 12px; display: grid; grid-template-columns: 64px 1fr; gap: 6px 12px; cursor: pointer; text-align: left; font: inherit; color: inherit; }
/* Your turn: a 2px border; 1px less padding keeps the inside the same size. */
.game-card { border: 2px solid var(--tone); padding: 11px; }  /* in its status colour */
.game-card .thumb { background: var(--panel-2); }
.plist { display: flex; flex-wrap: wrap; gap: 0 14px; font-size: 13px; }  /* no extra space when names wrap */
.plist > span { display: inline-flex; align-items: center; height: 30px; }
.game-card .plist { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, max-content)); }  /* two columns, each as wide as its longest name allows: the dots line up */
.game-card .plist > span { min-width: 0; }
.game-card .plist :is(.win, .place) { flex: none; }
.game-card .plist .ulink { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }  /* a long name: cut short in its half */
.badge { font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 99px; background: var(--good); color: #062; }
.empty { color: var(--muted); padding: 24px; text-align: center; }

/* ---- game ---- */
.game-view { position: fixed; inset: 0; overflow: hidden; }
#game { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; height: 100dvh; display: flex; flex-direction: column;
  padding: 0 env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px); }  /* clear of a notch and the home bar */

/* Under the top bar: the map, then the side panel. */
.gbody { flex: 1; min-height: 0; display: flex; }
.mapwrap { flex: 1; min-width: 0; min-height: 0; position: relative; display: flex; align-items: center; justify-content: center; background: #0d0f12; }
/* Territory names: drawn by the game (the map image has none), under the circles, in a
   compact face of our own so they take the same room everywhere (random maps lay them out
   by its letter widths). Roboto Condensed SemiBold, latin; SIL Open Font License. */
@font-face { font-family: "Map Names"; src: url(/roboto-condensed-600.woff2) format("woff2"); font-weight: 600; font-display: swap; }
.map-names { font: 600 15px "Map Names", "Roboto Condensed", "Arial Narrow", sans-serif; fill: #1b2630;
             stroke: #fff; stroke-opacity: .85; stroke-width: 1.5; paint-order: stroke; stroke-linejoin: round;
             pointer-events: none; }
.map-names .armed { stroke: none; fill: var(--accent); }
.mapscroll { display: contents; }  /* stacked (below), it scrolls the map sideways */
#map { width: 100%; height: 100%; display: block; touch-action: pan-x pan-y; user-select: none; -webkit-user-select: none; }  /* pinches are game.js's */
/* Zoomed (game.js sizes the map): its box scrolls both ways, the map centred while it's smaller. */
#mapscroll.zoomed { display: grid; position: absolute; inset: 0; overflow: auto; overscroll-behavior: contain; }
#mapzoom { display: contents; }
#mapscroll.zoomed > #mapzoom { display: block; position: relative; margin: auto; transform-origin: 0 0; }  /* (scaled from its corner while pinched) */
#mapzoom.pinch, .map-frame.pinch { will-change: transform; }  /* pinched: its picture is stretched as it is, not redrawn */
/* A map dialog's map: pinched (site.js), it scrolls both ways in its fitted box, its frame as wide as zoomed. */
.map-scroll { touch-action: pan-x pan-y; }
.map-scroll.zoomed { overflow: auto; overscroll-behavior: contain; }
.map-scroll.zoomed > .map-frame { transform-origin: 0 0; }
.maptip { position: absolute; left: 8px; top: 8px; background: rgba(0,0,0,.75); padding: 5px 10px; border-radius: 8px; font-size: 13px; pointer-events: none; }
/* A replay (game.js): its pause (play, paused) and stop buttons, in the map's corner. */
.replay { position: absolute; right: 8px; bottom: 8px; display: flex; gap: 6px; }
.replay .btn { padding: 8px; line-height: 0; background: rgba(0,0,0,.75); }
.replay svg { width: 14px; height: 14px; fill: currentColor; }
#rp-pause[aria-pressed=true] .i-pause, #rp-pause[aria-pressed=false] .i-play { display: none; }
/* A troop deployed or lost, floating off its circle (game.js: floatTroop). */
.float { pointer-events: none; font: 700 16px system-ui, sans-serif; fill: currentColor; stroke: rgba(0,0,0,.6); stroke-width: 2.5px; paint-order: stroke; stroke-linejoin: round; }
.gain { color: var(--good); }  /* a troop gained (floating up), or lucky */
.loss { color: var(--bad); }
.float .troop { stroke-width: 4px; }  /* (drawn at .6) */
.float > g { animation: float-up 1.2s ease-out both; }
/* A troop moved, walking to where it went (game.js: moveTroop). */
.mover { pointer-events: none; stroke-width: 3px; paint-order: stroke; stroke-linejoin: round; }
.mover > g { animation: troop-move .8s ease-in-out both; }
@keyframes troop-move { 0% { opacity: 0; transform: none; } 15%, 85% { opacity: 1; }
  50% { transform: translate(calc(var(--dx) / 2 + var(--wx)), calc(var(--dy) / 2 + var(--wy))); }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
@keyframes float-up { 0% { opacity: 0; } 12% { opacity: 1; } 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
/* Meanwhile PLAY's taps land on the pane itself, nothing in it, and stop the replay. */
#game:has(#replay:not([hidden])) :is(#tab-turn, #begin) > * { pointer-events: none; }

.terr { cursor: pointer; }
.terr .hit { fill: transparent; }
.name-hit { fill: transparent; cursor: pointer; }
/* Troops: see-through in their owner's colour, ringed in it (game.js sets both), a dark edge outside that.
   The ring reaches 1.6 inside the shape's edge and 0.85 outside: its stroke's cut off beyond that
   (masked to .edge-keep), so a thicker ring doesn't grow the pill, and the dark edge starts there (.edge-cut). */
.terr .badge-c, .ov-pill { fill-opacity: .45; stroke-width: 3.2; }
.terr .badge-e, .ov-edge { fill: none; stroke: rgba(0,0,0,.65); stroke-width: 3.2; }
.edge-cut, .edge-keep { stroke-width: 1.7; }
.edge-cut { fill: #000; stroke: #000; }
.edge-keep { fill: #fff; stroke: #fff; }
.terr:not(.zombie) .blood { display: none; }  /* INFECTION 26: blood on the zombies' circles */
.terr .blood { pointer-events: none; }
.mark:not(.on) { display: none; }  /* Rise of Rome: the barbarians' crossed axes round their pills (under the names) */
.mark { pointer-events: none; }
.terr text { font: 700 17px system-ui, sans-serif; text-anchor: middle; dominant-baseline: central; pointer-events: none;
  paint-order: stroke; stroke-width: 2.5; stroke-linejoin: round; }
.terr .ring { fill: none; stroke-width: 3; stroke: transparent; }
.terr.src .ring { stroke: var(--src); stroke-dasharray: 4 3; }
/* Its name stays clear as the armed ring's does (below): 14 dashes then a 4-dash gap, turned
   to face the name. (The ring's 126 long, its pathLength: 18 periods of 4 on, 3 off.) */
.terr.src[data-side]:not(.sel-from) .ring { stroke-dasharray: 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 31; }
.terr.tgt .ring { stroke: var(--tgt); }  /* steady: only the armed territory flashes */
.terr.sel-from .ring { stroke: var(--sel); stroke-dasharray: none; stroke-width: 4; }
/* Armed: the circle or pill keeps its owner's colour; a thick, flashing accent dashed ring round it,
   dashed like a clickable territory's ring (4 on, 3 off: 18 dashes round its 126). */
.terr .ring-d { fill: none; stroke: none; stroke-dasharray: 4 3; }
.terr.sel-to .ring { stroke: transparent; }
.terr.sel-to .ring-d { stroke: var(--accent); stroke-width: 6; }
/* Its flash (game.js: flashArmed): its ring and name again, dark, on a layer laid over the map (placeFlash),
   fading in and out. Only that layer's opacity changes, so nothing is painted again for it: a colour
   flashing in the map's own SVG has all of it painted every frame, and a replay's flights stutter. */
#flash { position: absolute; left: 0; top: 0; pointer-events: none; opacity: 0; animation: flash 1s ease-in-out infinite; }
#flash:not(:has(#flashon > *)) { display: none; }
.terr.dark .ring-d { stroke: #723b04; }  /* the accent at about half brightness. (Its own class, not #flash's: */
.map-names .armed.dark { fill: #723b04; }  /* a rule can't see past a wrap slice's copy of it, a <use>.) */
@keyframes flash { 50% { opacity: 1; } }
/* No dashes on the name's side: 14 dashes then a 4-dash gap, turned so the gap faces the
   name. A circle's rings start at 3 o'clock, a pill's at the left end of its top edge, going clockwise
   (so the pill's two rings, their corners rounder or less, turn a little differently). */
.terr[data-side] .ring-d { stroke-dasharray: 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 3 4 31; }
.terr.src[data-side="r"] .ring, .terr[data-side="r"] .ring-d { stroke-dashoffset: 110.5; }
.terr.src[data-side="b"] .ring, .terr[data-side="b"] .ring-d { stroke-dashoffset: 79; }
.terr.src[data-side="l"] .ring, .terr[data-side="l"] .ring-d { stroke-dashoffset: 47.5; }
.terr.src[data-side="t"] .ring, .terr[data-side="t"] .ring-d { stroke-dashoffset: 16; }
.pills .terr.src[data-side="r"] .ring { stroke-dashoffset: 73.35; }
.pills .terr.src[data-side="b"] .ring { stroke-dashoffset: 41.84; }
.pills .terr.src[data-side="l"] .ring { stroke-dashoffset: 10.34; }
.pills .terr.src[data-side="t"] .ring { stroke-dashoffset: 104.84; }
.pills .terr[data-side="r"] .ring-d { stroke-dashoffset: 73.75; }
.pills .terr[data-side="b"] .ring-d { stroke-dashoffset: 42.25; }
.pills .terr[data-side="l"] .ring-d { stroke-dashoffset: 10.75; }
.pills .terr[data-side="t"] .ring-d { stroke-dashoffset: 105.25; }
.terr.card .cardmark { display: block; }
.terr .cardmark { display: none; fill: var(--accent); }
.terr.card-sel .ring { stroke: var(--accent); stroke-width: 4; stroke-dasharray: none; }
.seam { stroke: #fff; stroke-opacity: .6; stroke-width: 2; stroke-dasharray: 6 4; pointer-events: none; }  /* where a wrap slice joins the map */
.arrow { stroke: var(--tgt); stroke-width: 3; fill: none; pointer-events: none; marker-end: url(#arrowhead); opacity: .9; }

/* ---- side panel ---- */
/* The action area and tab bar stay put; the active tab fills the rest and
   scrolls on its own. If the screen is too short for a pane's minimum height,
   the whole panel scrolls instead. Its width: 260px, or what the players table
   needs for long names (everything else wraps). */
.side { width: min-content; min-width: 260px; display: flex; flex-direction: column; background: var(--panel);
  border-left: 1px solid var(--line); min-height: 0; overflow-y: auto; }
.side > * { flex: none; }
.topbar .title { font-weight: 600; }
.side > .pane { flex: 1 1 0; min-height: 96px; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
/* Room for a scrollbar in every tab, scrolling or not, so content doesn't shift as tabs change. */
.side > .pane:not(.chat), .chat .feed { scrollbar-gutter: stable; }
.side > .chat { display: flex; flex-direction: column; overflow: hidden; min-height: 140px; } /* ~4 lines + the message box */
/* The tabs are radio buttons, each before its label: the chosen one's pane shows. The others
   still count toward the panel's width (not its height), so switching tabs never resizes it. */
.side:not(:has(#t-turn:checked)) #tab-turn, .side:not(:has(#t-players:checked)) #tab-players,
.side:not(:has(#t-log:checked)) #tab-log { display: block; visibility: hidden; flex: none; height: 0; min-height: 0; padding-block: 0; }
.chat .feed { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
.chat .feed::before { content: ""; margin-top: auto; }  /* lines sit at the bottom, by the message box, however many days are hidden; scroll up for older */
.feed { overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
/* The screen's height clear of a notch and the home bar, worked out (a length: game.js reads it, for
   the map's handle below). */
@property --safe-h { syntax: "<length>"; inherits: true; initial-value: 0px; }
:root { --safe-h: calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)); }
.maphandle { display: none; }
/* Portrait: stacked. The map across the top, scrolling sideways only (all of its height shows);
   the panel takes the rest of the screen, its tab scrolling on its own. The map: never smaller than
   its own size (a pixel to its unit: --mw by --mh, pages.game_page), but as wide as the screen if
   that's wider, and no taller than 60% of the screen's height clear of a notch and the home bar
   (the one thing that can shrink it). --r: how much wider a wrap's slices make it (game.js).
   Its handle dragged (game.js, kept on the device for the map): as tall as that makes it, its --share
   of the screen's height, bigger or smaller. */
@media (orientation: portrait) {
  :root:not(:fullscreen) .gbody { flex-direction: column; }
  :root:not(:fullscreen) .mapwrap { flex: none; display: block; }
  :root:not(:fullscreen) .mapscroll { display: block; position: relative; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; }
  :root:not(:fullscreen) #map { --tall: calc(var(--share, .6) * var(--safe-h));
    width: calc(var(--r, 1) * min(max(var(--mw) * 1px, 100%), var(--tall) * var(--mw) / var(--mh))); height: auto; margin-inline: auto; }
  :root:not(:fullscreen) #map[data-share] { width: calc(var(--r, 1) * var(--tall) * var(--mw) / var(--mh)); }
  :root:not(:fullscreen) .maphandle { display: block; flex: none; height: 20px; margin-block: -10px; position: relative; z-index: 2;
    cursor: ns-resize; touch-action: none; }
  .maphandle::before { content: ""; position: absolute; left: 50%; top: 50%; width: 44px; height: 5px; border-radius: 3px;
    transform: translate(-50%, -50%); background: var(--muted); box-shadow: 0 0 0 2px var(--panel); }
  .maphandle:is(.drag, :focus-visible)::before { background: var(--accent); }
  :root:not(:fullscreen) .side { flex: 1; width: auto; border-left: 0; border-top: 1px solid var(--line); }
  /* Fullscreen: landscape. Phones that can lock it turn; the rest are turned here. */
  :root:fullscreen #game { width: 100vh; width: 100dvh; height: 100vw; transform-origin: top left; transform: rotate(90deg) translateY(-100%); }
}
.newdot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #f33; box-shadow: 0 0 0 2px rgba(255,51,51,.25); margin-left: 5px; vertical-align: 2px; }

.action { padding: 8px; display: grid; align-content: start; gap: 8px; background: var(--panel-2); }
.action .row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.action .row .btn { flex: 1 1 auto; }
/* The step (DEPLOY, ATTACK...): a bar edge to edge across the action panel, text centred. */
.phase { margin: -8px -8px 0; padding: 5px 8px; text-align: center; font-size: 12px; font-weight: 800; letter-spacing: .06em; background: var(--line); }
.phase.mine { background: var(--turn); color: var(--turn-ink); }
.action .info { text-align: center; }  /* the lines under the step bar */
.action .text { font-size: 13px; color: var(--muted); }
/* The selection: a bar edge to edge across the action panel (out over its 8px padding). */
.action .sel { display: grid; grid-template-columns: 1fr; margin: 0 -8px; font-size: 16px; }
.action .sel.two { grid-template-columns: 1fr 1fr; }
.action .sel .end { display: flex; align-items: center; justify-content: center; gap: 4px; min-width: 0; padding: 5px 8px;
  white-space: nowrap; overflow: hidden; font-variant-numeric: tabular-nums; }
/* clip (not hidden) with a margin: the trimmed box below mustn't cut off descenders (g, y). */
.action .sel .end b { overflow: clip; overflow-clip-margin: 6px; text-overflow: ellipsis; margin-right: 4px; }
.action .sel .end.pick { background: var(--muted); color: var(--panel); }
/* Two ends: the first points into the second with a chevron (from -> to), centred on the
   middle (shoulders 6px left of it, point 6px right) so the first half's text looks centred;
   the second's matching notch sits 2px to the right, so the bar's black shows between them
   as a chevron-shaped line. */
.action .sel.two { background: #000; }
.action .sel .end strong { font-weight: 800; }
.action .sel.two .end:first-child { margin-right: -6px; padding-right: 20px; position: relative;
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 50%, calc(100% - 12px) 100%, 0 100%); }
.action .sel.two .end:nth-child(2) { margin-left: -6px; padding-left: 22px; clip-path: polygon(2px 0, 100% 0, 100% 100%, 2px 100%, 14px 50%); }
/* Names too long to sit side by side: stacked, the chevron pointing down. */
.action .sel.two.stack { grid-template-columns: 1fr; }
.action .sel.two.stack .end:first-child { margin: 0 0 -6px; padding: 5px 8px 20px;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 12px), 50% 100%, 0 calc(100% - 12px)); }
.action .sel.two.stack .end:nth-child(2) { margin: -6px 0 0; padding: 22px 8px 5px; clip-path: polygon(0 2px, 50% 14px, 100% 2px, 100% 100%, 0 100%); }
/* The soldier, drawn to its box's bottom: in a line, it sits just below the baseline, like a round letter. */
.troop { width: 1.15em; height: 1.15em; fill: currentColor; vertical-align: -.05em; }
.action .troop { margin: 0 .1em; }  /* in a line or a button */
.action .sel .troop { width: 18px; height: 18px; flex: none; position: relative; top: -3px; margin: 0; vertical-align: 0; }  /* its body is bottom-heavy: lift it to sit level with the caps */
/* Text boxes trimmed to the capitals' top and the baseline: a font's box also holds room
   for descenders (g, y), which caps and numbers don't use, so untrimmed they ride high
   and the bar's equal padding looks unequal. Firefox ignores this and looks as before. */
.action .sel .end > :not(svg) { text-box: trim-both cap alphabetic; }
.action .msg { font-size: 13px; color: var(--accent); }
.qty { display: flex; gap: 6px; align-items: center; }
.qty input[type=range] { flex: 1; min-width: 0; width: auto; accent-color: var(--accent); padding: 0; }
.qty-steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.qty-steps .btn { min-width: 0; padding: 6px 4px; font-weight: 700; }
.qty-steps .mm { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
.qty .val { min-width: 24px; text-align: left; font-weight: 700; font-size: 17px; }
.qty .troop { width: 18px; height: 18px; flex: none; margin: 0 -2px 0 0; position: relative; top: -1px; }
.rolls { font-size: 12px; color: var(--muted); display: flex; flex-direction: column; gap: 5px; max-height: 120px; overflow-y: auto; }
.rolls .who { color: var(--text); }
.rolls .round { display: flex; gap: 4px; align-items: center; padding: 2px; }
.rolls .vs { margin: 0 3px; }
/* The red dice always take three dice' room, packed to the right, so "vs" and the white
   dice line up down the log however many were rolled. */
.rolls .att-dice { display: flex; justify-content: flex-end; gap: 4px; width: calc(3 * 24px + 2 * 4px); flex: none; }
.die { display: inline-grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr); place-items: center; width: 24px; height: 24px; padding: 3px; box-sizing: border-box; border-radius: 5px; }
.die .pip { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.die.att { background: #d33; color: #fff; }
.die.def { background: #eee; color: #111; }
.die.won { box-shadow: 0 0 0 2px #ffd34d; }
.die.lost { opacity: .5; }
.die.unused { opacity: .18; }
.ln .die { vertical-align: middle; }  /* one in a line of the log, among its words (a revolt's) */
.city-roll { display: flex; align-items: center; gap: 4px; padding: 2px 0; }  /* a city and its revolt roll (rolledHtml) */
.city-roll > :first-child { margin-right: 4px; }
.busy { opacity: .5; pointer-events: none; }

table.players { width: 100%; border-collapse: collapse; font-size: 13px; }
.players th { font-weight: 500; color: var(--muted); font-size: 11px; text-align: right; padding: 6px 6px 2px; }
.players th:first-child { text-align: left; }
.players td { padding: 4px 6px; text-align: right; border-top: 1px solid var(--line); }
.players td.name { text-align: left; white-space: nowrap; }  /* never cut a name short: they're at most 20 characters */
/* In a replay, its board's table (REPLAY) or now's (CURRENT): the replay's counts in yellow, ticking as the map's do (game.js). */
/* REPLAY / CURRENT stays at the top as the pane scrolls, what scrolls under it hidden (its margin's worth round it). */
#boardpick { position: sticky; top: 6px; z-index: 1; background: var(--panel); box-shadow: 0 0 0 8px var(--panel); }
#tab-players:has(#b-replay:checked) :is(#players, #cards > .label), #tab-players:not(:has(#b-replay:checked)) :is(#replayed, #replay-set) { display: none; }
.players.replayed td:not(.name) { color: var(--playing); }
.players.replayed td.up, #replay-set .v.up { animation: tick-up .7s ease-out; }
#replay-set .v { color: var(--playing); padding: 1px 3px; }
.players.replayed td.down { animation: tick-down .7s ease-out; }
@keyframes tick-up { from { color: var(--good); background: color-mix(in srgb, var(--good) 22%, transparent); } }
@keyframes tick-down { from { color: var(--bad); background: color-mix(in srgb, var(--bad) 22%, transparent); } }
.players tr.out, .plist .out { opacity: .45; }  /* eliminated: the game's table, game cards */
.players tr.out .cp, .plist .out .cp { text-decoration: line-through; }  /* their name struck through: not their place */

.tabs { display: flex; border-bottom: 1px solid var(--line); }
.tabs input { position: absolute; opacity: 0; pointer-events: none; }  /* still there for the keyboard */
.tab { flex: 1; font-size: 13px; color: var(--muted); padding: 10px 8px; text-align: center; cursor: pointer; }
.tabs :checked + .tab { color: var(--text); box-shadow: inset 0 -2px 0 var(--accent); }
.tabs :focus-visible + .tab { outline: 2px solid var(--accent); outline-offset: -2px; }
.side:has(#tab-turn:empty) [for=t-turn] { display: none; }  /* PLAY: only once your turn has begun */
.feed { font-size: 12px; padding: 6px 8px; }
.feed .ln { padding: 2px 0; }
.feed .t { color: var(--muted); margin-right: 4px; }
/* A player's colour. */
.cp, .ulink.cp { color: var(--c); }
.cp a { color: inherit; }
#tab-log, .players, .plist { --pill-h: 18px; --pill-pad: 7px; }  /* the pills: players, positions, continents (.kpill), all as tall */
:is(#tab-log, .players) .kpill { font-weight: 700; white-space: nowrap; padding: 0 var(--pill-pad); border-radius: 99px; display: inline-block; line-height: var(--pill-h); }
#tab-log :not(.said) > .tx { line-height: calc(var(--pill-h) + 4px); }  /* a line wrapping: its pills clear of those above (a chat line has none) */
/* Players' and positions' pills, and the bounty cards, all as the one card: a hint of their colour
   for a ground, the colour itself a 4px bar down the left (both layers of the one background, so just
   as tall), a 1px dark line round it all (its border), and outside that a frame (a box shadow): grey,
   or 2px of orange or yellow. The dark line keeps an orange bar clear of an orange frame. A dark
   colour's bar has a faint light edge, so it shows on the dark ground. */
@property --f { syntax: "<color>"; inherits: false; initial-value: transparent; }
:is(#tab-log, .players, .plist) .cp, .cardchip { --f: var(--line); --bar-edge: transparent; --pg: color-mix(in srgb, var(--c) 14%, var(--panel-2));
  color: var(--text); border: 1px solid var(--bg); border-radius: 4px;
  background: linear-gradient(var(--c), var(--c)) 0 0 / 4px 100% no-repeat, linear-gradient(var(--bar-edge), var(--bar-edge)) 0 0 / 5px 100% no-repeat,
    linear-gradient(var(--pg), var(--pg)) padding-box; box-shadow: 0 0 0 calc(1px + var(--w, 0px)) var(--f); }
:is(#tab-log, .players, .plist) .cp.dark { --bar-edge: rgba(255, 255, 255, .28); }
/* A player's pill: as tall as a line's (1px clear of the next, for its frame); on their turn its
   frame 2px, ready to play in the your-turn orange, playing in yellow, breathing. */
:is(#tab-log, .players, .plist) .cp { display: inline-block; white-space: nowrap; font-weight: 700; line-height: calc(var(--pill-h) - 2px);
  padding: 0 var(--pill-pad) 0 calc(var(--pill-pad) + 3px); margin: 0 1px; }
:is(.players, .plist) .cp { margin: 3px; }
:is(#tab-log, .players, .plist) .cp.st-green { --f: var(--turn); --w: 1px; }
:is(#tab-log, .players, .plist) .cp.st-yellow { --f: var(--playing); --w: 1px; animation: breathe 1.6s ease-in-out infinite; }
@keyframes breathe { 50% { --f: color-mix(in srgb, var(--playing) 35%, transparent); } }
@media (prefers-reduced-motion: reduce) { :is(#tab-log, .players, .plist) .cp.st-yellow { animation: none; } }
/* A bounty (DETAILS', small; cashing in, big): its chip in its type's colour (chipIcon), its position's
   name below; one that can go in a set framed in orange, one chosen lit in it (its chip on a dark disc). */
.cards { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px; }
.cards.inline { padding: 4px 0; }
.cardchip { font: inherit; font-size: 12px; line-height: 1.2; flex: 0 0 84px; padding: 8px 4px 6px; background: var(--pg) padding-box;
  display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; overflow-wrap: anywhere; }
.cardchip { --pg: var(--panel-2); }
.cardchip.t0 { --c: #ff6b6b; } .cardchip.t1 { --c: #4cd97b; } .cardchip.t2 { --c: #5aa9ff; }
.cardchip .ct { color: var(--c); display: flex; }
.cardchip .chip { width: 26px; height: 26px; }
.chip .halo { fill: none; }
.cardchip.selectable { --f: var(--turn); --w: 1px; }
.cardchip.on { --pg: var(--accent); color: var(--accent-ink); }
.cardchip.on .halo { fill: var(--accent-ink); }
.cardchip.big { font-size: 15px; flex-basis: 110px; padding: 12px 6px 8px; gap: 6px; min-width: 0; }
.cardchip.big .chip { width: 36px; height: 36px; }
/* A bounty just earned, on its way in (earnChip): over everything, shadowed so it shows on the map. */
.cardchip.flying { position: fixed; z-index: 60; margin: 0; pointer-events: none; box-shadow: 0 0 0 1px var(--line), 0 10px 30px rgba(0, 0, 0, .55); }
.players th .chip { width: 1.15em; height: 1.15em; vertical-align: -.2em; }
/* The timeline: each entry its time, then its text. A turn's line opens to the turn, its
   lines, a battle's to its toll and dice: each set in under a rule, and led by the
   marker that opens it. */
.day > .ln { padding: 5px 0; border-top: 1px solid var(--line); }
.day > .ln:first-child, .stripe + .ln { border-top: 0; }
/* A chat line: its player's pill writ large, the pill's ground and text (the name bold, no pill of its own),
   and its colour bar square and the line's full height between its time and its words. After the lines'
   own padding and divider, which it replaces. */
#tab-log .say { --bar-edge: transparent; --pg: color-mix(in srgb, var(--c) 14%, var(--panel-2)); color: var(--text); }
#tab-log .say.dark { --bar-edge: rgba(255, 255, 255, .28); }
.day > .say { background: var(--pg); padding: 9px 8px; border-top: 0; margin: 4px -8px; }  /* edge to edge, its time in line with the others (.feed's 8px) */
#tab-log .say > .tx { margin-block: -9px; padding: 9px 0 9px 10px;
  background: linear-gradient(var(--c), var(--c)) 0 0 / 4px 100% no-repeat, linear-gradient(var(--bar-edge), var(--bar-edge)) 0 0 / 5px 100% no-repeat; }
#tab-log .say .tx > .cp:first-child { background: none; padding: 0; margin: 0; border: 0; color: inherit; box-shadow: none; }
/* Your newest message's EDIT, under its time. */
.feed .t .edit { display: block; margin: 4px 0 -2px; padding: 1px 0; border: 1px solid; border-radius: 4px; width: 100%;
  background: none; color: inherit; font: inherit; font-size: 9px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; }
.stripe { margin: 2px -8px; padding: 3px 8px; background: var(--bg); text-align: center; font-weight: 600; }
/* Each timed line in two columns: its time, as wide as it is (every time is as wide: HH:MM in
   tabular figures), then the rest. A turn's or battle's lines open under the rest: they have no
   times of their own, so a hidden one holds their first column at the same width. */
#tab-log .ln:has(> .t), .feed details:has(> summary > .t) > .fold {
  display: grid; grid-template-columns: auto 1fr; column-gap: 6px; align-items: baseline; }
#tab-log .t, .feed details:has(> summary > .t) > .fold::before { font-size: 11px; font-variant-numeric: tabular-nums; }
.feed details:has(> summary > .t) > .fold { margin-left: 0; padding-left: 0; border-left: 0; }  /* (no rule: being under the content column sets it off) */
.feed details:has(> summary > .t) > .fold::before { content: "00:00"; visibility: hidden; margin-right: 4px; grid-row: 1 / span 99; }
.feed details:has(> summary > .t) > .fold > * { grid-column: 2; }
/* A line's REPLAY button (▶), at its right. */
#tab-log .ln:has(> .rp) { display: grid; grid-template-columns: 1fr auto; column-gap: 6px; align-items: baseline; }
#tab-log .ln:has(> .t):has(> .rp) { grid-template-columns: auto 1fr auto; }
.rp { background: none; border: 0; padding: 4px 2px 4px 10px; margin: -4px 0; color: var(--muted); cursor: pointer; align-self: center; touch-action: manipulation; }
.rp svg { display: block; width: 9px; height: 11px; fill: currentColor; }
.feed summary { cursor: pointer; list-style: none; }
.feed summary::-webkit-details-marker { display: none; }
/* A line that opens (a turn, a battle, revolts' rolls): its chevron at its right, before REPLAY, turning
   as it opens. Its tap target 30px wide, and 28px between its mark and REPLAY's (as is the
   whole line, a tap target to open it). Its text then starts where every line's does: pills line up. */
#tab-log summary { display: grid; grid-template-columns: 1fr auto; column-gap: 6px; align-items: baseline; }
#tab-log summary:has(> .t) { grid-template-columns: auto 1fr auto; }
#tab-log summary:has(> .rp) { grid-template-columns: 1fr auto auto; }
#tab-log summary:has(> .t):has(> .rp) { grid-template-columns: auto 1fr auto auto; }
.fd { align-self: center; padding: 8px 12px 8px 8px; margin: -8px 0 -8px 4px; color: var(--muted); }
.fd svg { display: block; width: 10px; height: 10px; fill: none; stroke: currentColor; stroke-width: 1.8; transition: transform .15s; }
.feed details[open] > summary .fd { color: var(--text); }
.feed details[open] > summary .fd svg { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .fd svg { transition: none; } }
.feed details > .fold { margin: 4px 0 0 6px; padding-left: 8px; border-left: 2px solid var(--line); }
.feed .replaying { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px; }  /* the line a replay is on (game.js) */
#tab-log .troop { margin-right: 1px; }
/* "+3 → Red Barn": a fat arrow, on the player's pill ground, outlined in their colour.
   (Not .arrow: that's the map's attack arrow, with its arrowhead marker.) */
#tab-log .toarrow { width: 17px; height: 16px; margin: 0 3px; vertical-align: -4px;
  fill: var(--c); }  /* as its player's pills */
/* A continent in the log: a pill as the others are, its name lettered as the map's key has it, on
   its colour and texture (the variables: pages.key_css, a rule for each of the map's continents). */
:is(#tab-log, .players) .kpill { background: var(--tex) 0 0 / var(--ts), var(--bg); color: var(--ink);
  font-family: "Map Names", "Roboto Condensed", sans-serif; font-weight: 600;
  -webkit-text-stroke: calc(var(--ew) * 1em) var(--edge); paint-order: stroke fill; }
.feed .battle .rolls { max-height: none; padding: 4px 0 2px; }
/* The timeline's GAMEPLAY / CHAT / BOTH, and in a replay, DETAILS' REPLAY / CURRENT: radio buttons,
   each before its label, the chosen one lit; the timeline's stays put over the lines, which scroll under it. */
.slider { flex: none; display: flex; margin: 6px 8px 2px; padding: 2px; border: 1px solid var(--line); border-radius: 99px; }
.slider input { position: absolute; opacity: 0; pointer-events: none; }
.slider label { flex: 1; padding: 4px 0; border-radius: 99px; text-align: center; font-size: 11px; font-weight: 600;
  color: var(--muted); cursor: pointer; }
.slider :checked + label { background: var(--accent); color: var(--accent-ink); }
.slider :focus-visible + label { outline: 2px solid var(--accent); outline-offset: 1px; }
#tab-log:has(#f-game:checked) :is(.said, .chat-form), #tab-log:has(#f-chat:checked) .day > :not(.said, .date),
#tab-log:has(#f-game:checked) .day:not(:has(> :not(.said, .date))), #tab-log:has(#f-chat:checked) .day:not(:has(> .said)) { display: none; }
.chat-form { display: flex; gap: 6px; padding: 6px 8px; border-top: 1px solid var(--line); position: relative; }
/* The players a word being typed could name, over the message box: a tap names them. */
.mentions { position: absolute; left: 8px; right: 8px; bottom: 100%; display: flex; flex-wrap: wrap; gap: 6px; padding: 8px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px; --pill-h: 28px; --pill-pad: 12px; }
.mentions .cp { font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; }
.chat-form input { padding: 7px 10px; }

.toast { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); background: #3b1f22; border: 1px solid var(--bad);
  padding: 8px 14px; border-radius: 8px; max-width: 80%; font-size: 13px; z-index: 5; }
.toast.ok { background: #1d3524; border-color: var(--good); }
.action .btn[data-b] { text-transform: uppercase; letter-spacing: .03em; white-space: nowrap; }
.btn .lbls { display: inline-grid; justify-items: center; }
.btn .lbls > * { grid-area: 1 / 1; }
.btn .lbls > .off { visibility: hidden; }
.sync { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--muted); transition: background .3s; }
.sync.green { background: var(--good); }
.sync.yellow { background: var(--playing); }
.sync.red { background: var(--bad); box-shadow: 0 0 0 2px rgba(255,80,80,.3); }
/* Bounties on territories you hold, matching the map's card marker. */
.cardchip .owned { text-decoration: underline dotted var(--accent); text-decoration-thickness: 2px; text-underline-offset: 3px; }
.cardchip.on .owned { text-decoration-color: var(--accent-ink); }
/* Pending: a radar sweep erases the dot over one turn, then redraws it. */
@property --sweep { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.sync.yellow {
  -webkit-mask: conic-gradient(#000 calc(var(--sweep) - 360deg), transparent 0 var(--sweep), #000 0);
          mask: conic-gradient(#000 calc(var(--sweep) - 360deg), transparent 0 var(--sweep), #000 0);
  animation: radar 1.6s linear infinite;
}
.spinner { display: inline-block; width: 1em; height: 1em; margin-right: .5em; vertical-align: -.15em;
           border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
@keyframes radar { to { --sweep: 720deg; } }
@media (prefers-reduced-motion: reduce) { .sync.yellow { animation: none; -webkit-mask: none; mask: none; } }

/* ---- worldcon: invite links, lobby cards, profiles ---- */
#link-body .plist { color: var(--text); }
.game-card.lobby { cursor: default; opacity: .85; }
/* The whole card opens the game: a link under the card's own links and buttons. */
.game-card { position: relative; }
.card-link { position: absolute; inset: 0; border-radius: inherit; }
.card-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.game-card .ulink, .game-card .card-actions { position: relative; }  /* positioned after the cover link, so painted above it */
a.btn { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
.card-actions form { display: grid; }
/* The new-game dialog's checkbox pills: AUTOMATIC / RANKED when ticked, MANUAL / UNRANKED when not. */
.pill.toggle { position: relative; }
/* Its inputs, and the map ring's (below), unseen: their own small size, not an input's full width (the
   map ring's, placed against the dialog, ran past its edge: a sideways scroll). */
.pill.toggle input, .mapring input { position: absolute; opacity: 0; pointer-events: none; width: auto; padding: 0; }
.pill.toggle:has(input:checked) .off, .pill.toggle:has(input:not(:checked)) .on { display: none; }
.pill.toggle:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The new game's map: a ring of radio buttons; only the chosen one's label shows (it chooses the next). */
.mapring { display: contents; }
.mapring label:not(:checked + label) { display: none; }
.mapring :focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }
.pills .mapring .pill { grid-template-columns: 1fr; }  /* (no icon: a map has none) */
/* INFECTION 26 can be played alone: its sizes, from 1 player, in place of the others. */
[name=max_players_alone], form:has(#map-infection:checked) [name=max_players] { display: none; }
form:has(#map-infection:checked) [name=max_players_alone] { display: block; }
/* Only standard Classic games can be ranked (engine.UNRANKED: the rest are in beta): a fixed UNRANKED in place of the switch. */
.pills .pill.unranked, .pills:is(:not(:has(#map-classic:checked)), :has([name=jug]:checked)) .pill.toggle:has([name=ranked]) { display: none; }
.pills:is(:not(:has(#map-classic:checked)), :has([name=jug]:checked)) .pill.unranked { display: inline-grid; }
/* The mode: JUGGERNAUT's rules under the pills while it's chosen; none on INFECTION 26 (engine.settings_from). */
.pills:has(#map-infection:checked) .pill.toggle:has([name=jug]) { display: none; }
.mode-about { display: grid; gap: 6px; font-size: 14px; line-height: 1.4; }
.mode-about p { margin: 0; }
form:is(:not(:has([name=jug]:checked)), :has(#map-infection:checked)) .mode-about { display: none; }
.game-card.finished { opacity: .7; }
select { font: inherit; color: var(--text); background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 9px 10px; }
#toast-global { position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); z-index: 50; }
#link-form { gap: 20px; }
#link-new { display: grid; gap: 14px; }
/* Signed in, a game invite's NEW ACCOUNT (pages.link_page): its username and passkey under it, once opened. */
.link-more { display: grid; gap: 14px; }
.link-more > summary { list-style: none; cursor: pointer; text-align: center; }
.link-more > summary::-webkit-details-marker { display: none; }
.link-more[open] > summary { display: none; }
.link-signin { display: grid; gap: 8px; }
.link-signin p { margin: 0; }
#link-body { line-height: 1.5; display: grid; gap: 12px; }

.games .empty { grid-column: 1 / -1; }
/* Action buttons run edge to edge along the card's bottom: no padding around them, no
   borders, equal widths (two buttons = exactly half each). */
.game-card { overflow: hidden; }
.card-actions { grid-column: 1 / -1; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  margin: 6px -12px -12px; }
.card-actions .btn { border: 0; border-radius: 0; padding: 12px 8px; font-size: 13px; min-width: 0; }
.card-actions .btn.enter { background: var(--accent); color: var(--accent-ink); font-weight: 600; }  /* same as START NOW (.btn.primary) */
.game-card:not(.turn) .card-actions .btn.enter { background: var(--wait); color: #06121f; }  /* gold only when it's your turn */
.game-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* All buttons in caps (Daniel); icon buttons are unaffected. */
.btn { text-transform: uppercase; letter-spacing: .03em; }
.login h1, .topbar h1 { letter-spacing: .04em; }

.passkey-panel { --pad: 18px; padding: var(--pad); gap: 12px; width: min(420px, calc(100vw - 32px)); color: var(--text); }
.passkey-panel[open] { display: grid; }
#login-panel { width: fit-content; }  /* as wide as the code */
.passkey-panel::backdrop { background: rgba(0, 0, 0, .6); }
/* A dialog's title: a stripe across its top (out over its padding, --pad), staying put as it scrolls; its
   right end the square that closes it, its own left edge the stripe's line. */
.dialog-head { display: flex; height: 44px; margin: calc(-1 * var(--pad)) calc(-1 * var(--pad)) 0; position: sticky; top: calc(-1 * var(--pad));
  z-index: 1; background: var(--panel); border-bottom: 1px solid var(--line); border-radius: var(--radius) var(--radius) 0 0; }
.dialog-head .label { flex: 1; align-self: center; padding: 0 var(--pad); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dialog-head .x { width: 44px; flex: none; padding: 0; border: 0; border-left: 1px solid var(--line); background: none; color: var(--muted);
  font: inherit; font-size: 22px; line-height: 1; cursor: pointer; border-radius: 0 var(--radius) 0 0; }
.dialog-head .x:hover { background: var(--panel-2); color: var(--text); }
.pk-choose { display: flex; flex-wrap: wrap; gap: 8px; }
.pk-qr { background: #fff; border-radius: 8px; padding: 8px; width: min(240px, 100%); }
.pk-qr, .pk-valid, .pk-link { justify-self: center; }
.pk-valid { margin: 0; }  /* just the dialog's gap, as below the code */
.pk-qr svg { display: block; width: 100%; height: auto; }
.pk-link { display: flex; gap: 8px; width: 240px; max-width: 100%; }  /* as wide as the code: no need to see the whole link */
.pk-link input { flex: 1; min-width: 0; font-size: 12px; }

/* Top-bar actions live in a ☰ menu on every screen (a <details>, no script needed). */
.topbar { flex-wrap: nowrap; }  /* already position: sticky, which anchors the dropdown */
.topbar > *, .topbar > .tools > * { flex: none; }
.topbar > .tools { display: contents; }  /* its buttons sit in the bar like the rest (the game's are held at the right, below) */
.topbar > .spacer { flex: 1 1 0; min-width: 0; }
.topbar h1, .topbar .title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; min-width: 0; }
/* A game's name, round and clock: beside the logo when there's room, else a row of their own
   under it, still in the bar. Its buttons stay at the top right, kept clear of the name. */
.topbar:has(> .title) { flex-wrap: wrap; row-gap: 0; padding-right: 104px;  /* the buttons' width, and a gap */
  background: linear-gradient(var(--bg) calc(37px + env(safe-area-inset-top, 0px)), var(--panel) 0); }  /* past the first row (only once it wraps): a stripe across */
.topbar:has(> .title) > .logo { padding-block: 6px; }  /* the bar's 37px, the buttons being out of the flow */
.topbar:has(> .title) > .title { padding-block: 4px 8px; }
.whose { text-transform: none; }  /* (pages.whose: a username as typed) */
/* With whose it is too, a game's title may still be too long for its row: it breaks between its parts, each kept whole. */
.topbar > .title:has(> .whose) { white-space: normal; }
.topbar > .title:has(> .whose) > * { white-space: nowrap; }
.topbar:has(> .title) > .tools { display: flex; align-items: flex-start; gap: 10px; position: absolute; top: env(safe-area-inset-top, 0px); right: 0; bottom: 0; }
.topbar:has(> .title) .sync { margin-top: 13.5px; }  /* centred on the first row */
/* Fullscreen: the map takes the full height; the bar sits over the side panel, the logo and dot on
   top and the name under: stacked, so the panel is as narrow as it goes, or the logo or the name if wider. */
:root:fullscreen #game { display: grid; grid-template: "map head" auto "map side" 1fr / 1fr min-content; }
:root:fullscreen .gbody { display: contents; }
:root:fullscreen .mapwrap { grid-area: map; }
:root:fullscreen .side { grid-area: side; width: auto; }
:root:fullscreen .topbar { grid-area: head; flex-direction: column; align-items: flex-start; padding-right: 32px; border-left: 1px solid var(--line); }
:root:fullscreen .topbar .sync { margin-right: 12px; }
:root:fullscreen :is(#g-full, .topbar .menu) { display: none; }
/* The wordmark (made by tools/logo.py). */
.logo, .logo img { display: block; }
.logo img { height: 25px; max-width: calc(100vw - 150px); object-fit: contain; object-position: left; }  /* as tall as the bar allows; narrower on a phone so the menu still fits */
.logo-big { display: block; width: 100%; height: auto; }
.menu summary { list-style: none; cursor: pointer; }
.menu summary::-webkit-details-marker { display: none; }
/* Its items: full-width stripes, ruled apart, links and buttons alike; your name (to your profile) first, as typed. */
.menu-items { position: absolute; right: 0; top: 100%; z-index: 30; display: flex; flex-direction: column; min-width: 200px;
  overflow: hidden; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0,0,0,.35); }
.menu-items > * { display: block; padding: 13px 16px; border: 0; border-top: 1px solid var(--line); background: none; color: var(--text);
  font: inherit; text-align: left; text-decoration: none; text-transform: uppercase; letter-spacing: .03em; cursor: pointer; touch-action: manipulation; }
.menu-items > :first-child { border-top: 0; }
.menu-items > :is(:hover, :focus-visible) { background: var(--panel-2); outline: none; }
.menu-items .menu-user { font-weight: 700; text-transform: none; letter-spacing: normal; }
/* Other accounts signed in here (pages.menu): your name's stripe has a chevron, as a log line that opens has,
   showing theirs under it, each a stripe switching to it. */
.menu-items > .menu-me { display: flex; align-items: center; padding: 0; cursor: auto; }
.menu-me .menu-user { flex: 1; padding: 13px 16px; color: inherit; text-decoration: none; }
.menu-me .fd { padding: 13px 16px 13px 12px; margin: 0; cursor: pointer; }
.menu-me .fd input { position: absolute; opacity: 0; pointer-events: none; }
.menu-me .fd:has(input:checked) { color: var(--text); }
.menu-me .fd:has(input:checked) svg { transform: rotate(180deg); }
.menu-me .fd:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }
.menu-items > .menu-other { padding: 0; }
.menu-items:not(:has(#acct-more:checked)) > .menu-other { display: none; }
.menu-other button { display: block; width: 100%; padding: 13px 16px 13px 28px; border: 0; background: none; color: var(--text);
  font: inherit; text-align: left; cursor: pointer; touch-action: manipulation; }
.menu-other button:is(:hover, :focus-visible) { background: var(--panel-2); outline: none; }

/* Headings and labels in caps everywhere (Daniel); typed text stays as typed. */
h1, h2, .label, .tab, .slider label, .stripe, .players th { text-transform: uppercase; letter-spacing: .03em; }
input, select, textarea { text-transform: none; letter-spacing: normal; }

.modal-form { display: grid; gap: 12px; }

/* Rules as a two-column table: equal halves, each entry centred in its cell. */
.pills { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
.pills .pill { justify-content: center; min-width: 0; }
.pill { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; line-height: 1;
  padding: 4px 8px; border-radius: 99px; background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  display: inline-flex; align-items: center; gap: 5px; }
.pill .pi svg { width: 12px; height: 12px; flex: none; fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
/* Rows: header, players, spare space, rules, buttons. Any extra height from a taller
   neighbour lands between the players and the rules, which stay just above the buttons. */
.game-card { grid-template-rows: auto auto 1fr auto auto; }
.game-card .rules { grid-row: 4; }
.game-card .card-actions { grid-row: 5; }
/* Confirmations are short labels: caps like the buttons. Errors stay sentence case. */
.toast.ok { text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
/* A copy button confirms itself: green "COPIED" for a moment (copied() in app.js). */
.btn.copied, .btn.copied:disabled, .card-actions .btn.copied { background: var(--good); color: #062b13; border-color: transparent; font-weight: 600; opacity: 1; }
/* Usernames link to profiles without looking like links. */
.ulink { color: inherit; text-decoration: none; }
.ulink:hover { text-decoration: underline; }
/* The players page: everyone in a grid, a row each (its columns the grid's, so they line up). Each
   heading is the label of a radio button: the chosen one's column orders the rows, each row
   carrying its place in every order (players_page). */
.ranking-wrap { padding-block: 16px; overflow-x: auto; }  /* no side padding: an overflowing table would scroll it in on the left only */
.ranking { display: grid; grid-template-columns: auto repeat(6, auto); width: min(640px, 100%); margin: 0 auto;
  font-size: 13px; font-variant-numeric: tabular-nums; }
.ranking > input { display: none; }
.rk-head, .rk-row { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; }
.rk-head { order: -1; }
.rk-head label { padding: 6px; align-self: end; text-align: right; color: var(--muted); font-size: 10px; font-weight: 500;
  text-transform: uppercase; letter-spacing: .03em; cursor: pointer; }
.rk-row > span { padding: 8px 6px; text-align: right; border-top: 1px solid var(--line); }
.rk-head > :first-child, .rk-row > :first-child { text-align: left; white-space: nowrap; }
.ranking:has(#s-username:checked) .rk-row { order: var(--username); }
.ranking:has(#s-games_played:checked) .rk-row { order: var(--games_played); }
.ranking:has(#s-elo:checked) .rk-row { order: var(--elo); }
.ranking:has(#s-conquest_rate:checked) .rk-row { order: var(--conquest_rate); }
.ranking:has(#s-attendance:checked) .rk-row { order: var(--attendance); }
.ranking:has(#s-ranked_games:checked) .rk-row { order: var(--ranked_games); }
.ranking:has(#s-luck:checked) .rk-row { order: var(--luck); }
.ranking:has(#s-username:checked) [for=s-username],
.ranking:has(#s-games_played:checked) [for=s-games_played],
.ranking:has(#s-elo:checked) [for=s-elo],
.ranking:has(#s-conquest_rate:checked) [for=s-conquest_rate],
.ranking:has(#s-attendance:checked) [for=s-attendance],
.ranking:has(#s-ranked_games:checked) [for=s-ranked_games],
.ranking:has(#s-luck:checked) [for=s-luck] { color: var(--text); }
.ranking:has(#s-username:checked) [for=s-username]::after,
.ranking:has(#s-games_played:checked) [for=s-games_played]::after,
.ranking:has(#s-elo:checked) [for=s-elo]::after,
.ranking:has(#s-conquest_rate:checked) [for=s-conquest_rate]::after,
.ranking:has(#s-attendance:checked) [for=s-attendance]::after,
.ranking:has(#s-ranked_games:checked) [for=s-ranked_games]::after,
.ranking:has(#s-luck:checked) [for=s-luck]::after { content: " ▾"; }
/* Player profile */
.profile-wrap { display: grid; place-items: center; padding: 32px 16px; }
.profile { width: min(360px, 100%); padding: 24px; display: grid; gap: 16px; text-align: center; }
.profile h2 { margin: 0; font-size: 24px; text-transform: none; letter-spacing: 0; }  /* a name, as typed */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; margin: 0; }
.stats div { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 8px; }
.stats dt { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; }
.stats dd { margin: 4px 0 0; font-size: 24px; font-weight: 700; }
/* A number's card that needs it says what it means (its "i"), on hover or a tap: under them all, as wide (never off
   the screen), its own line for a player's own; one at a time, the one hovered first. Text, so it copies. */
.stats { position: relative; }
.stats > div:has(.tip) { cursor: help; outline: none; }
.stats > div:has(.tip):is(:hover, :focus-within) { border-color: var(--muted); }
.stats dt { display: flex; justify-content: space-between; align-items: center; gap: 4px; }
.stats > div:has(.tip) dt::after { content: "i"; flex: none; width: 15px; height: 15px; border: 1.5px solid; border-radius: 50%; display: grid;
  place-items: center; font: italic 700 10px/1 Georgia, serif; text-transform: none; letter-spacing: 0; }
.stats .tip { display: none; position: absolute; z-index: 1; top: calc(100% + 6px); left: 0; right: 0; margin: 0; padding: 8px 10px;
  border-radius: 6px; background: #000; color: var(--text); font-size: 13px; font-weight: 400; line-height: 1.4; white-space: pre-line;
  text-align: left; cursor: text; }
/* Luck's figures: its skirmishes across the top, troops lost expected and actual beneath, half and half. */
.stats .figs { width: 100%; margin-top: 10px; border-collapse: collapse; table-layout: fixed; white-space: nowrap;
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; }
.stats .figs td { padding: 6px 4px; border: 1px solid var(--line); text-align: center; }
.stats > div:is(:hover, :focus-within) .tip { display: block; }
.stats:hover > div:not(:hover) .tip { display: none; }
/* The freshness dot in the games list's top bar stands where the refresh button was. */
#games-sync { width: 12px; height: 12px; margin: 0 6px; cursor: pointer; }
#g-sync { cursor: pointer; }
#g-title { margin-right: .4em; }  /* the same gap as between the round and clock */
/* Top of the card: the map thumbnail in the corner (touching both edges) with the game's
   name beside it, out to the right edge, and under the name its round and countdown. */
.game-card .thumb { width: 76px; height: 52px; margin: -11px 0 0 -11px; }
.card-pills { position: absolute; width: 0; height: 0; }  /* each map's pills, once, for its cards' maps to show (pages.game_cards) */
.game-card .card-head { display: flex; flex-direction: column; justify-content: center; height: 52px; margin: -11px -11px 0 -11px;
  padding: 0 11px; background: var(--panel-2); white-space: nowrap; overflow: hidden; }
.game-card .card-name { font-size: 20px; line-height: 1.15; overflow: hidden; }
.game-card .gstat { font-size: 13px; font-weight: 600; }
/* A game's round and countdown (round_clock in pages.py), the clock in its status's colour. */
.gstat { display: inline-flex; gap: .4em; font-variant-numeric: tabular-nums; }
.gstat .clock { color: var(--tone); }
.tone-turn { --tone: var(--turn); }
.tone-waiting { --tone: var(--wait); }
.tone-finished { --tone: #4b5361; }
.pill .ulink { text-transform: none; letter-spacing: .02em; }  /* a username stays as typed */
/* A game card's rules: one row of equal boxes, each icon centred in its own; the
   name pops up above on hover, or on tap anywhere in the box (it takes focus). The
   boxes are positioned, so they sit over the card's cover link. */
.rules { display: flex; color: var(--muted); }
.game-card .rules { grid-column: 1 / -1; }
.rule { flex: 1; position: relative; display: grid; place-items: center; outline: none; }
.rule svg { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.rule:hover, .rule:focus { color: var(--text); }
.rule:hover::after, .rule:focus::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%; translate: -50% 0;
  padding: 4px 8px; border-radius: 6px; background: #000; color: var(--text); white-space: nowrap;
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; }
.rule:first-child::after { left: 0; translate: none; }  /* the end icons' tips stay inside the card */
.rule:last-child::after { left: auto; right: 0; translate: none; }
/* Atop the game's DETAILS tab: a stripe of its own, the names popping up below (the tab cuts off above). */
#tab-players .rules { padding: 6px 0; border-top: 1px solid var(--line); }  /* at the bottom: their names show above them */
/* Pill layout: the icon is centred in a square cell as tall as the pill's inside, at the
   left; the text is centred in the remaining width. Overrides the flex rules above. */
.pill, .pills .pill { --pill-h: 20px; display: inline-grid; grid-template-columns: calc(var(--pill-h) - 2px) 1fr;
  align-items: stretch; height: var(--pill-h); padding: 0; gap: 0; justify-content: stretch; }
.pill .pi { display: grid; place-items: center; }
.pill .pt { padding: 0 6px; line-height: calc(var(--pill-h) - 2px); text-align: center; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }  /* (inline: a soldier in it sits on the line, as everywhere) */
/* The new-game dialog's toggle pills switch on tap. */
.pill.toggle { cursor: pointer; border-style: dashed; color: var(--text); }
/* A finished game's winner gets a gold trophy where the status dot would be. */
.win { width: 13px; height: 13px; margin-right: 4px; vertical-align: middle; fill: none; stroke: var(--accent);
  stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; flex: none; }
/* THE JUG (JUGGERNAUT): their icon after their name, on game cards and the game's table. */
.jugm { width: 15px; height: 15px; margin-left: 5px; vertical-align: middle; flex: none; fill: none; stroke: currentColor;
  stroke-linecap: round; stroke-linejoin: round; color: var(--muted); }
/* An eliminated player's finishing place, where the dot would be: as wide as the dot, so the
   two line up down a column, its number centred on it (and spilling either side). */
.place { display: inline-flex; justify-content: center; width: 8px; margin-right: 4px; vertical-align: middle;
  font-size: 14px; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
/* The maps page: each map a card, its picture over its name, size and rules. */
.maps { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 14px; padding: 14px 0; }
.map-card { overflow: hidden; display: grid; grid-template-rows: auto 1fr; min-width: 0; }
button.map-card { font: inherit; color: inherit; text-align: left; padding: 0; cursor: pointer; }
button.map-card:hover { border-color: var(--accent); }
.map-card .map-pic { display: block; width: 100%; aspect-ratio: 16 / 10; background: var(--panel-2); }  /* (the whole map, fitted: pages.maps_page) */
.map-about { padding: 12px 14px 14px; display: grid; gap: 4px; align-content: start; }
.map-about .label { font-weight: 700; font-size: 15px; }
/* A map, tapped (its card, or in a game's invite): near full screen, a game on it and all about it. */
.map-dialog { --pad: 12px; width: calc(100vw - 16px); max-width: 1200px; max-height: calc(100dvh - 16px); padding: var(--pad); gap: 4px; color: var(--text); overflow: auto; }
.map-dialog[open] { display: grid; }
.map-dialog::backdrop { background: rgba(0, 0, 0, .6); }
.map-dialog .map-preview { padding: 0; }
/* A game's invite: its map, plain, with its name; tapped, the dialog above. */
.map-thumb { display: grid; gap: 6px; width: 100%; padding: 0; border: 0; background: none; color: var(--text); font: inherit; cursor: pointer; }
.map-thumb .map-pic { display: block; width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--line); }
