/* Platform pages and chrome. Tokens live in base.css; games have their own
   files in games/. Class names are the `gn-` ones in Sources/Platform/Views.swift. */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; min-height: 100vh; background: var(--page); color: var(--ink);
  font-family: var(--font); font-size: 1.125rem; line-height: 1.45;
  overflow-wrap: break-word;
}
h1 { font-size: 1.75rem; line-height: 1.2; margin: 0.4rem 0 0.8rem; }
h2 { font-size: 1.3rem; line-height: 1.25; margin: 1.5rem 0 0.6rem; }
p { margin: 0.5rem 0; }
a { color: var(--o); }
a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible, select:focus-visible {
  outline: 3px solid var(--accent); outline-offset: 2px;
}
.gn-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  white-space: nowrap;
}
.gn-muted { color: var(--ink-soft); }
/* A chair nobody sits in, labelled by whose it was (`SeatLabel` in GameKit): dim and slanted, whatever it sits on. */
.gn-vacant { opacity: 0.6; font-style: italic; }
.gn-rules { color: var(--ink-soft); font-style: italic; }
.gn-small { font-size: 0.9rem; }
.gn-center { text-align: center; }

/* ---- Header and layout ---- */
.gn-header {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.6rem 1rem; background: var(--felt); color: var(--felt-ink);
}
.gn-brand { color: var(--felt-ink); font-weight: 800; font-size: 1.3rem; text-decoration: none; padding: 0.4rem 0; }
/* The person's menu (`PersonMenu`): their name and a caret, which drops a
   small card down over the page from the bar's corner. menus.js puts
   gn-menu-open on .gn-person while it is open. Above the sheets and the
   questions that stick to the top, below a toast. */
.gn-person { position: relative; min-width: 0; }
.gn-person-button { display: flex; align-items: center; gap: 0.4rem; max-width: 100%; }
.gn-person-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gn-person-button .gn-icon { transition: transform 0.15s; }
.gn-menu-open .gn-person-button { background: rgb(255 255 255 / 0.12); }
.gn-menu-open .gn-person-button .gn-icon { transform: rotate(180deg); }
.gn-person-menu {
  display: none; position: absolute; right: 0; top: calc(100% + 0.35rem); z-index: 40;
  min-width: 18.5rem; max-width: calc(100vw - 1.5rem); margin: 0; padding: 0.35rem 0; list-style: none;
  color: var(--ink); background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 4px 18px rgb(0 0 0 / 0.16);
}
.gn-menu-open .gn-person-menu { display: block; }
.gn-person-menu form { margin: 0; }
.gn-person-menu a, .gn-person-menu button, .gn-menu-later {
  display: flex; align-items: center; justify-content: space-between; gap: 0.8rem;
  width: 100%; min-height: 2.75rem; padding: 0.4rem 1rem; font: inherit; text-align: left;
  color: var(--ink); background: none; border: 0; text-decoration: none; cursor: pointer;
}
.gn-person-menu a:hover, .gn-person-menu button:hover { background: var(--paper-2); }
.gn-person-menu :focus-visible { outline-offset: -3px; }
.gn-menu-later { color: var(--ink-soft); cursor: default; }
.gn-menu-note { font-size: 0.85rem; font-style: italic; }
.gn-menu-sound .gn-icon { width: 1.3rem; height: 1.3rem; }
.gn-menu-sound .gn-sound-slash, .gn-menu-sound.off .gn-sound-high { display: none; }
.gn-menu-sound.off .gn-sound-slash { display: inline-block; }
.gn-person-menu li:last-child { margin-top: 0.35rem; padding-top: 0.35rem; border-top: 1px solid var(--line); }
.gn-link {
  font: inherit; color: var(--felt-ink); background: none; border: 1px solid rgb(244 239 227 / 0.5);
  border-radius: var(--radius); padding: 0.45rem 0.8rem; min-height: 2.75rem; cursor: pointer;
}
.gn-link:hover { background: rgb(255 255 255 / 0.12); }
.gn-main { width: min(46rem, 100%); margin: 0 auto; padding: 1rem 1rem 3rem; }

/* ---- Cards, forms, buttons ---- */
.gn-card {
  display: block; padding: 1rem; background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); color: var(--ink); text-decoration: none;
}
.gn-narrow { max-width: 26rem; margin: 1.5rem auto 0; }
.gn-form { display: flex; flex-direction: column; gap: 0.9rem; }
.gn-form label { display: flex; flex-direction: column; gap: 0.3rem; }
.gn-form label > span, .gn-form legend { color: var(--ink-soft); font-size: 0.95rem; }
input[type=text], input[type=password], input[type=number], input:not([type]), select {
  font: inherit; width: 100%; min-height: 3rem; padding: 0.55rem 0.8rem; color: var(--ink);
  background: var(--card); border: 2px solid var(--edge); border-radius: var(--radius);
}
input:focus { border-color: var(--accent); }
.gn-row { display: flex; gap: 0.6rem; margin-top: 0.4rem; }
.gn-row input { flex: 1; min-width: 0; }
.gn-error { color: var(--bad); font-weight: 600; margin: 0; }

.gn-button {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
  min-height: 3rem; padding: 0.6rem 1.2rem; font: inherit; font-weight: 700; text-align: center;
  color: var(--ink); background: var(--paper-2); border: 2px solid var(--edge);
  border-radius: var(--radius); text-decoration: none; cursor: pointer;
  touch-action: manipulation;
}
.gn-button:hover { background: var(--line); }
.gn-button.gn-primary { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); }
.gn-button.gn-primary:hover { filter: brightness(0.93); }
.gn-button.gn-quiet { background: transparent; }
.gn-button.gn-quiet:hover { background: var(--paper-2); }
.gn-button.gn-danger { color: var(--accent-ink); background: var(--bad); border-color: var(--bad); }
.gn-button.gn-small { min-height: 2.5rem; padding: 0.3rem 0.8rem; font-size: 0.95rem; }
.gn-button[disabled] { opacity: 0.5; cursor: default; }

.gn-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 1rem; }
.gn-actions p { margin: 0; flex: 1 1 100%; }
.gn-stack { flex-direction: column; align-items: stretch; }
.gn-stack form { display: contents; }

/* ---- Home ---- */
.gn-game-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }
.gn-table-row, .gn-game-pick { min-height: 3.5rem; }
.gn-table-card:has(.gn-table-row:hover), .gn-game-pick:hover { border-color: var(--accent); }
/* A home card (`Views.tablesList`): the link covers the whole card, and the
   wave buttons under it sit above that cover. */
.gn-table-card { position: relative; padding: 0; }
.gn-table-row {
  display: grid; grid-template-columns: 1fr auto; gap: 0.15rem 0.8rem; align-items: center;
  padding: 1rem; color: var(--ink); text-decoration: none; border-radius: var(--radius);
}
.gn-table-row::after { content: ""; position: absolute; inset: 0; border-radius: var(--radius); }
.gn-table-waves {
  position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 0.4rem 0.6rem;
  margin: -0.5rem 0 0; padding: 0 1rem 0.8rem;
}
.gn-game-name { font-weight: 700; font-size: 1.2rem; }
.gn-game-players { color: var(--ink-soft); grid-column: 1 / -1; }
.gn-game-here, .gn-game-waiting { grid-column: 1 / -1; font-size: 0.95rem; }
.gn-game-pick { display: flex; }
/* The card is a button (see `Views.gameList`), so it needs a button's resets,
   and a pill that reads as the thing to press. */
button.gn-game-pick {
  width: 100%; font: inherit; text-align: left; cursor: pointer; touch-action: manipulation;
  flex-direction: row; align-items: center; justify-content: space-between; gap: 0.3rem 1rem;
}
.gn-pick-text { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.gn-pick-go {
  flex: none; padding: 0.4rem 0.9rem; font-weight: 700; border-radius: var(--radius);
  color: var(--accent-ink); background: var(--accent); border: 2px solid var(--accent);
}
.gn-game-pick:hover .gn-pick-go { filter: brightness(0.93); }
.gn-game-pick:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
@media (max-width: 30rem) {
  button.gn-game-pick { flex-direction: column; align-items: stretch; }
  .gn-pick-go { text-align: center; }
}
.gn-new-table strong { display: block; }
.gn-game-pick strong { font-size: 1.25rem; }
.gn-config fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.6rem 0.9rem; margin: 0; }

/* ---- Table page ---- */
.gn-table-title { display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem 0.8rem; margin-bottom: 0.6rem; }
.gn-table-title h1 { margin-right: auto; }
/* "← Home" sits in this block (`Views.tableTitle`), so that a phone can put
   it on the name's line; here it is a line of its own above the name. The
   table menu (⋯) opens its tools as a row under the name (Rename and
   Remove here; Copy link stays in the row), on a phone too, below. */
.gn-table-title .gn-home-link { flex-basis: 100%; margin: 0 0 -0.1rem; }
.gn-table-tools { display: none; }
.gn-menu-open .gn-table-tools {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.6rem;
  flex-basis: 100%; padding: 0.3rem 0 0.2rem;
}
/* Remove is only offered between games (`TableView.removable`): the area,
   pushed with every change, says so, and the menu, which isn't, hides it. */
#gn-live:has(#table-area[data-removable="false"]) .gn-remove-table { display: none; }
/* In its place, mid-game: the button greyed, and why (`Views.tableTools`). */
.gn-remove-blocked { display: none; }
#gn-live:has(#table-area[data-removable="false"]) .gn-remove-blocked {
  display: inline-flex; align-items: center; flex-wrap: wrap; gap: 0.2rem 0.7rem;
}
/* In the menu, Remove's question opens from a button like Rename's. */
.gn-table-tools .gn-remove-table { margin: 0; }
.gn-table-tools .gn-remove-table > summary {
  display: inline-flex; align-items: center; min-height: 2.5rem; padding: 0.3rem 0.8rem;
  font-size: 0.95rem; font-weight: 700; color: var(--ink); list-style: none;
  border: 2px solid var(--edge); border-radius: var(--radius);
}
.gn-table-tools .gn-remove-table > summary::-webkit-details-marker { display: none; }
.gn-table-tools .gn-remove-table > summary:hover { background: var(--paper-2); }
.gn-table-tools .gn-remove-table[open] { flex-basis: 100%; padding: 0; }
.gn-table-tools .gn-remove-table[open] > summary { background: var(--paper-2); }
/* Drawn for one width only: Copy link is in the row on a wide screen and
   in the menu on a phone (`Views.copyLink`). */
@media (max-width: 59.99rem) { .gn-wide-only { display: none !important; } }
@media (min-width: 60rem) { .gn-narrow-only { display: none !important; } }
.gn-table-menu {
  display: inline-flex; align-items: center; justify-content: center; width: 2.75rem; height: 2.75rem; padding: 0;
  font: inherit; color: var(--ink); background: transparent; border: 2px solid var(--edge);
  border-radius: var(--radius); cursor: pointer; touch-action: manipulation;
}
.gn-table-menu .gn-icon { width: 1.5rem; height: 1.5rem; }
.gn-table-menu[aria-expanded="true"] { background: var(--paper-2); }
/* "Rename" is a button that opens a box: its summary looks like the one beside it. */
.gn-rename summary { list-style: none; }
.gn-rename summary::-webkit-details-marker { display: none; }
.gn-rename[open] { flex-basis: 100%; }
.gn-rename-form { display: flex; gap: 0.6rem; margin: 0.5rem 0 0.3rem; }
.gn-rename-form input { flex: 1; min-width: 0; }
.gn-removed h2 { margin-top: 0; }
.gn-table-area { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
/* One quiet line per other table with people at it; nothing when empty. */
.gn-elsewhere:empty { display: none; }
.gn-elsewhere { margin: 0.4rem 0 0.8rem; color: var(--ink-soft); }
.gn-elsewhere p { display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem 0.7rem; margin: 0.3rem 0; }
.gn-elsewhere .gn-button { text-decoration: none; }
/* On a wide screen a line's buttons sit in the line, and the phone's button
   for them is not there (`Views.ElsewhereLine`). */
.gn-else-actions { display: contents; }
.gn-else-more.gn-button { display: none; }
/* Someone asked everyone to come to their table: the one line here that
   asks something of you, so it is a card in the accent colour. */
.gn-elsewhere .gn-invite {
  padding: 0.6rem 0.8rem; color: var(--ink); background: var(--card);
  border: 2px solid var(--accent); border-radius: var(--radius);
}
.gn-game-call { grid-column: 1 / -1; font-weight: 700; color: var(--ink); }
.gn-table-card.gn-calling { border: 2px solid var(--accent); }
.gn-one-game { text-align: center; margin: 0 0 0.8rem; }
.gn-picker h2 { margin-top: 0; }
/* What the table has played, folded away at the bottom of the page
   (`Views.earlier`): one quiet line until it is opened. */
.gn-earlier { margin: 1.5rem 0 0.5rem; }
.gn-earlier summary { cursor: pointer; padding: 0.3rem 0; }
.gn-earlier-fold > summary { color: var(--ink-soft); }
.gn-earlier h3 { font-size: 1rem; margin: 0.6rem 0 0.4rem; }
.gn-evening { margin: 0.3rem 0; }
.gn-evening > .gn-history-list { margin: 0.3rem 0 0.6rem 1.2rem; }
.gn-history-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
.gn-history-list .gn-time { margin-left: 0.5rem; font-size: 0.9rem; white-space: nowrap; }
/* The evening's score for a run of rematches (`Views.finished`). */
.gn-tally { text-align: center; font-weight: 700; margin: 0 0 0.6rem; }
.gn-seat-offer { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem 1rem; }
.gn-seat-offer p { margin: 0; flex: 1 1 14rem; }
.gn-board { display: flex; flex-direction: column; align-items: center; padding: 1rem 0.5rem; }
/* Every game's status line (`p.status`, above its board) keeps room for
   two lines whether it needs them or not, so a turn whose words run to two
   lines and the next one's that fit on one don't move the board under the
   person playing (Jason, in Ur: "It is the text row above it, alternating
   in number of lines"). One rule here, not one per game (093). A phone
   keeps room for three: Ur's longest, worded to fit two lines on a laptop
   ("Grandma took Grandpa's piece on Shared 12! Grandpa is choosing a
   move."), takes three there. */
.gn-board .status { min-height: 2.9em; min-height: 2lh; }
@media (max-width: 30rem) { .gn-board .status { min-height: 4.4em; min-height: 3lh; } }
.gn-players { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.gn-player {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; padding: 0.4rem 0.8rem;
  background: var(--card); border: 1px solid var(--line); border-radius: 999px;
}
.gn-watching { opacity: 0.75; font-style: italic; }
.gn-name { font-weight: 700; }
.gn-dot { width: 0.8rem; height: 0.8rem; border-radius: 50%; background: var(--line); border: 2px solid var(--ink-soft); flex: none; }
.gn-dot.gn-online { background: var(--good); border-color: var(--good); }
/* A bot's dot is square: always there, and plainly not a person. */
.gn-dot.gn-bot-dot { border-radius: 2px; background: var(--ink-soft); border-color: var(--ink-soft); }
/* A bot whose move is coming: its chip says so, the dot breathing (still,
   for anyone who asked for less motion). */
.gn-badge.gn-thinking { font-style: italic; }
@media (prefers-reduced-motion: no-preference) {
  .gn-bot:has(.gn-thinking) .gn-bot-dot { animation: gn-think 1.2s ease-in-out infinite alternate; }
}
@keyframes gn-think { from { opacity: 1; } to { opacity: 0.25; } }
/* How long is left of a bot's wait (`WaitBar`): a bar draining from what
   is left to nothing, and the seconds counted down by countdown.js. The
   bar is restarted by every render at what is left then, so a push or a
   reconnect never puts it wrong. With reduced motion it stands still and
   the seconds tell it. */
.gn-wait { display: inline-flex; flex-direction: column; justify-content: center; gap: 0.2rem; min-width: 6.5rem; font-size: 0.8rem; }
.gn-wait-bar {
  display: block; height: 0.35rem; border-radius: 999px; background: currentColor; opacity: 0.85;
  transform-origin: left; transform: scaleX(var(--from, 1));
}
@media (prefers-reduced-motion: no-preference) {
  .gn-wait-bar { animation: gn-drain var(--left, 0s) linear forwards; }
}
@keyframes gn-drain { from { transform: scaleX(var(--from, 1)); } to { transform: scaleX(0); } }
.gn-bot-offer { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.5rem 0; }
/* A bot's chair offered to watchers: one line, so the board stays in view on a phone. */
.gn-bot-chairs { display: grid; gap: 0.3rem; padding: 0.4rem 0.7rem; }
.gn-bot-chairs .gn-seat-offer { flex-wrap: nowrap; gap: 0.6rem; }
.gn-bot-chairs .gn-seat-offer p { flex: 1 1 auto; min-width: 0; font-size: 0.85rem; line-height: 1.25; color: var(--ink-soft); }
.gn-bot-chairs .gn-button { flex: none; white-space: nowrap; min-height: 2.25rem; padding: 0.2rem 0.6rem; font-size: 0.85rem; }
.gn-badge {
  padding: 0 0.5rem; font-size: 0.8rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; border-radius: 999px; color: var(--ink-soft); background: var(--paper-2);
}
.gn-badge.gn-you { color: var(--accent-ink); background: var(--o); }
.gn-lobby h2 { margin-top: 0; }
.gn-finished { justify-content: center; }
/* Back at the table to find someone in your chair: why you're watching. */
.gn-taken { margin: 0; }

/* ---- The table on a phone ----
   Decided (088; Dave, 030: "The long header takes up most of the phone
   screen before the cards start"): on a phone the game starts as high as
   it can. The site bar is slimmer; "← Home", the table's name and the
   table menu (`Views.TableMenu`, with Copy link and Rename) share one
   line; the people are one line that scrolls sideways, the players first
   and the watchers after them; and nothing a player uses mid-game moves.
   "A phone" is the same width as everywhere else here, below the 60rem
   where chat and rules turn from docks into sheets, so the page changes
   at one width and not two. Wide screens are as they were. */
@media (max-width: 59.99rem) {
  .gn-header { padding-top: 0.2rem; padding-bottom: 0.2rem; }
  .gn-brand { font-size: 1.15rem; }
  .gn-header .gn-link { min-height: 2.5rem; padding-top: 0.25rem; padding-bottom: 0.25rem; }
  .gn-main { padding-top: 0.5rem; }
  .gn-table-title {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
    gap: 0 0.7rem; margin-bottom: 0.5rem;
  }
  .gn-table-title .gn-home-link { margin: 0; white-space: nowrap; }
  .gn-table-title h1 { margin: 0; font-size: 1.3rem; }
  /* Open, the tools are a row under the name that pushes the game down,
     not a pop-up over it: nothing covers the board, and Rename's box gets
     the whole width. */
  .gn-menu-open .gn-table-tools { grid-column: 1 / -1; padding: 0.5rem 0 0.2rem; }
  /* Out to the screen's edges, so a chip scrolled half out of view is cut
     by the edge of the phone, which reads as "there's more this way".
     Positioned, so the chips' screen-reader words (`gn-sr`, placed
     absolutely) are clipped with them: otherwise the last chip's words
     stretch the page sideways, and a phone zooms the whole page out to
     fit them. */
  .gn-players {
    position: relative; flex-wrap: nowrap; gap: 0.4rem; overflow-x: auto; scrollbar-width: none;
    margin: 0 -1rem; padding: 0 1rem;
  }
  .gn-players::-webkit-scrollbar { display: none; }
  /* A fade on the edge that has more chips (`chips.js`, 111). */
  .gn-players.gn-more-end { mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent); }
  .gn-players.gn-more-start { mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent); }
  .gn-players.gn-more-start.gn-more-end {
    mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent);
  }
  .gn-player { flex: none; flex-wrap: nowrap; white-space: nowrap; padding: 0.2rem 0.7rem; }
  .gn-players .gn-watching { order: 1; }
  .gn-table-area { gap: 0.6rem; }
  .gn-board { padding-top: 0.25rem; padding-bottom: 0.5rem; }
  /* Where everyone is (090): one short line per table, the game they are
     in left out; "Join them", the waves and "Ask everyone to come here"
     behind one button (`Views.ElsewhereLine`), opened on the region. An
     invitation (`.gn-invite`) has none of this: it is always in full. */
  .gn-elsewhere { margin: 0 0 0.5rem; }
  .gn-else { flex-wrap: nowrap; }
  .gn-else > span:first-child { flex: 1; min-width: 0; }
  .gn-else-doing { display: none; }
  .gn-else-more.gn-button { display: inline-flex; flex: none; }
  .gn-else-more[aria-expanded="true"] { background: var(--paper-2); }
  .gn-elsewhere:not(.gn-else-open) .gn-else-ask { display: none; }
  .gn-elsewhere:not(.gn-else-open) .gn-else-actions { display: none; }
  .gn-else-open .gn-else { flex-wrap: wrap; }
  .gn-else-open .gn-else-actions { display: flex; flex-wrap: wrap; flex-basis: 100%; gap: 0.4rem 0.6rem; }
}

.gn-leave { margin-top: 0.5rem; color: var(--ink-soft); }
.gn-leave summary { cursor: pointer; padding: 0.7rem 0; min-height: 2.75rem; }
.gn-leave[open] { padding: 0 0.2rem 0.8rem; }

/* ---- Toasts: appear, linger, leave, all by themselves ---- */
/* At the top of the screen, where nothing else is fixed. The bottom belongs
   to the Rules and Chat bar on a phone and to the open sheet over it (a
   toast there was under or over the thing the person was about to use),
   and the top is where the status line and the board's first row are, so
   it is where an eye already is. It is centred in what the sheet or panel
   leaves, and it takes no taps, so it never blocks the board beneath. */
.gn-toast {
  position: fixed; left: 50%; top: max(0.8rem, env(safe-area-inset-top)); z-index: 100;
  width: max-content; max-width: calc(100vw - 2rem);
  padding: 0.8rem 1.2rem; font-weight: 700; color: var(--accent-ink); background: var(--bad);
  border-radius: var(--radius); box-shadow: 0 4px 16px rgb(0 0 0 / 0.3);
  animation: gn-toast 4.5s ease forwards; pointer-events: none;
}
.gn-toast-calm { color: var(--felt-ink); background: var(--felt); }
@media (min-width: 60rem) {
  body.gn-chat-open .gn-toast { left: calc(50% - 10rem); max-width: calc(100vw - 22rem); }
  body.gn-rulebook-open .gn-toast, body.gn-game-page-open .gn-toast { left: calc(50% - 13rem); max-width: calc(100vw - 28rem); }
}
@keyframes gn-toast {
  0% { opacity: 0; transform: translate(-50%, -1rem); }
  8%, 85% { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, 0); visibility: hidden; }
}

/* ---- Waves (`Views.wave`) ----
   A hello from someone at another table. Where a toast is, at the top,
   for the same reasons (the bottom is the phone's bar and its sheets),
   but a card in the page's own colours rather than a warning's red, and
   slower to go, since it has a link to follow. Several stack. Only the
   link takes a tap, and the note holds still while it is pointed at. */
.gn-waves {
  position: fixed; left: 50%; top: max(0.8rem, env(safe-area-inset-top)); z-index: 100;
  transform: translateX(-50%); width: max-content; max-width: calc(100vw - 2rem);
  display: grid; gap: 0.5rem; pointer-events: none;
}
/* Icon, words, link in a row whatever the width: the words wrap in their
   own column, so the hand stays beside them on a phone. */
.gn-wave {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.7rem; padding: 0.6rem 0.9rem;
  font-weight: 700; color: var(--ink); background: var(--card); border: 2px solid var(--edge);
  border-radius: var(--radius); box-shadow: 0 4px 16px rgb(0 0 0 / 0.25);
  animation: gn-wave 9s ease forwards;
}
.gn-wave-sent { animation-duration: 4.5s; }
.gn-wave .gn-icon { width: 1.6rem; height: 1.6rem; color: var(--accent); }
.gn-wave .gn-button { white-space: nowrap; }
.gn-wave a { pointer-events: auto; }
.gn-wave:has(a:hover, a:focus-visible) { animation-play-state: paused; }
@keyframes gn-wave {
  0% { opacity: 0; transform: translateY(-1rem); }
  5%, 90% { opacity: 1; transform: none; }
  100% { opacity: 0; transform: none; visibility: hidden; }
}
@media (min-width: 60rem) {
  body.gn-chat-open .gn-waves { left: calc(50% - 10rem); max-width: calc(100vw - 22rem); }
  body.gn-rulebook-open .gn-waves, body.gn-game-page-open .gn-waves { left: calc(50% - 13rem); max-width: calc(100vw - 28rem); }
}
.gn-wave-button .gn-icon { width: 1.2em; height: 1.2em; }

/* ---- Connection state ---- */
.gn-reconnecting {
  display: none; margin-bottom: 0.8rem; padding: 0.5rem 0.9rem; font-weight: 700;
  color: var(--ink); background: var(--paper-2); border: 1px dashed var(--ink-soft); border-radius: var(--radius);
}
body.gn-offline .gn-reconnecting { display: block; }
body.gn-offline .gn-board { opacity: 0.6; pointer-events: none; }

/* ---- Drag (see Public/js/drag.js) ---- */
[data-gn-drag] { touch-action: none; cursor: grab; -webkit-user-select: none; user-select: none; }
.gn-dragging { opacity: 0.4; }
body.gn-drag-active { cursor: grabbing; -webkit-user-select: none; user-select: none; }
.gn-drag-ghost { filter: drop-shadow(0 6px 10px rgb(0 0 0 / 0.35)); transition: none; }
.gn-drop-ready { outline: 3px dashed var(--accent); outline-offset: 2px; }
.gn-drop-over { outline-style: solid; }


/* ---- Sound ---- */
/* The status line with the speaker at its end (`SiteSound.Toggle`), so the
   toggle costs a phone no height. Any game that makes a noise uses both. */
/* At the top, not the middle: the line keeps room for two (`.gn-board
   .status`), and a speaker centred on that room would sit below the words
   whenever they take one. */
.gn-status-row { display: flex; align-items: flex-start; gap: 0.5rem; }
.gn-status-row .status { flex: 1 1 auto; min-width: 0; }
/* A status line that runs to two or three lines (Ur's, Pachisi's, 107)
   can't give up a column to the speaker: on a phone the longest ("Grandpa
   rolled 3 but can't move, so the turn passes. Waiting for Grandma to
   roll.") then wraps to a fourth line and the board under it jumps. So
   there the speaker sits at the top right and the words wrap round it for
   its two lines and run the full width below. A float does that, but it
   has to come before the text, and the speaker comes after (as in every
   game's row), so an empty float of the speaker's size stands in for it
   (::before) and the speaker is placed over it. Its words ("Tap for
   sound") are left to its label and its dimmed speaker for the same
   reason, and so the line doesn't change length when the first tap turns
   the sound on. */
.gn-status-row.wraps { position: relative; display: flow-root; }
.gn-status-row.wraps::before { content: ""; float: right; width: 3.1rem; height: 2.6rem; }
.gn-status-row.wraps .gn-sound { position: absolute; top: 0; right: 0; }
.gn-status-row.wraps .gn-sound-text { display: none; }
.gn-sound {
  flex: none; display: inline-flex; align-items: center; gap: 0.3rem;
  min-width: 2.6rem; min-height: 2.6rem; padding: 0 0.5rem; margin: 0;
  font: inherit; font-size: 0.8rem; color: var(--ink-soft); cursor: pointer;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  touch-action: manipulation;
}
.gn-sound:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }
.gn-sound .gn-icon { width: 1.4rem; height: 1.4rem; }
.gn-sound .gn-sound-slash { display: none; }
.gn-sound.on { color: var(--ink); }
.gn-sound:not(.on) .gn-sound-high { opacity: 0.55; }
.gn-sound.off .gn-sound-high { display: none; }
.gn-sound.off .gn-sound-slash { display: inline-block; }
.gn-sound-text:empty { display: none; }

/* ---- Chat ---- */
/* A tab opens a panel; what that looks like depends on room.

   On a phone the tab is a bar across the bottom and the panel is a sheet
   that rises from the top of the bar, short enough that the board above it
   is still on screen. The bar stays showing under it (Carol, 032: "The Rules
   sheet I had opened was sitting over the Chat button"), so Chat and Rules
   work like tabs: the other one switches with one tap, the open one's
   closes it. The sheet doesn't hide the game or catch its taps: the
   point of chatting at a game table is to keep playing, and nobody should
   have to close the chat to take a turn. Opening it makes the page taller by
   the sheet's height (--gn-sheet, on body), so the last row of anything can
   still be scrolled clear of it. It closes with its own button (or Escape),
   never by a tap elsewhere, because a tap elsewhere is a move.

   On a wide screen the tab is a handle on the right edge and the panel
   docks beside the page, which makes room (body gets gn-chat-open too).

   Either way it never sits inside #table-area, which every push replaces.
   "Your turn" shows on the tab, which a phone shows whatever is open, so it
   can be seen without opening chat; see chat.js for how it gets there. */
:root { --gn-sheet: clamp(14rem, 35vh, 18rem); --gn-sheet-full: 100vh; }
@supports (height: 1dvh) { :root { --gn-sheet: clamp(14rem, 35dvh, 18rem); --gn-sheet-full: 100dvh; } }
/* An icon (`Icon`, GameKit) is a picture in the colour of the words around
   it, as tall as they are. */
.gn-icon { width: 1em; height: 1em; flex: none; vertical-align: -0.125em; }
.gn-chat-tab .gn-icon, .gn-rulebook-tab .gn-icon { width: 1.35rem; height: 1.35rem; }
.gn-chat-tab {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; min-height: 3.25rem; padding: 0 1rem;
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  font: inherit; font-weight: 700; cursor: pointer;
  color: var(--accent-ink); background: var(--o); border: 0; box-shadow: 0 -2px 8px rgb(0 0 0 / 0.15);
}
/* The bar's real height (chat.js measures it into --gn-bar), so the last row of
   anything on the page, and a game's sticky buttons, clear it whatever it
   grows to; 3.25rem is its minimum, for a page that hasn't measured yet. */
.gn-main { padding-bottom: calc(var(--gn-bar, 3.25rem) + 1.25rem); }
body.gn-chat-open .gn-main { padding-bottom: calc(var(--gn-sheet) + var(--gn-bar, 3.25rem) + 1rem); }
.gn-chat-turn { display: none; white-space: nowrap; font-size: 0.85rem; font-weight: 700; padding: 0 0.6rem; line-height: 1.5rem; border-radius: 999px; }
.gn-your-turn .gn-chat-turn { display: inline-block; }
.gn-chat-tab .gn-chat-turn { color: var(--ink); background: var(--paper); }
.gn-chat-name { display: contents; }
.gn-chat-ask {
  display: none; min-height: 2.5rem; padding: 0 0.7rem; margin-right: 0.2rem; font: inherit; font-size: 0.95rem; font-weight: 700;
  white-space: nowrap; cursor: pointer; color: var(--accent-ink); background: var(--bad); border: 0; border-radius: 999px;
}
.gn-asked .gn-chat-ask { display: inline-block; }
.gn-chat-unread {
  writing-mode: horizontal-tb; min-width: 1.4rem; padding: 0 0.35rem; line-height: 1.4rem;
  text-align: center; font-size: 0.85rem; border-radius: 999px; background: var(--accent);
}
.gn-chat-unread[hidden] { display: none; }
/* The phone's peek at the newest line (122, chat.js): one line standing on
   the bar, in for a few seconds and gone. It only fades and slides, opacity
   and transform, so no frame of it restyles the board (086). Out of the
   way on a wide screen, where the chat is beside the board, and under any
   open sheet, which is the chat or a better place for the eyes. */
.gn-chat-peek {
  display: none; position: fixed; left: 0.75rem; right: 0.75rem; bottom: calc(var(--gn-bar, 3.25rem) + 0.5rem); z-index: 21;
  min-height: 2.5rem; padding: 0 0.9rem; font: inherit; font-size: 0.95rem; text-align: left; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--ink); background: var(--paper); border: 2px solid var(--edge); border-radius: var(--radius); box-shadow: var(--shadow);
  opacity: 0; transform: translateY(0.5rem); visibility: hidden; pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease, visibility 0s linear 0.25s;
}
.gn-chat-peek.gn-peek-on { opacity: 1; transform: none; visibility: visible; pointer-events: auto; transition-delay: 0s; }
@media (max-width: 59.99rem) {
  .gn-chat-peek { display: block; }
  body.gn-chat-open .gn-chat-peek, body.gn-rulebook-open .gn-chat-peek, body.gn-game-page-open .gn-chat-peek { display: none; }
}
@media (prefers-reduced-motion: reduce) { .gn-chat-peek { transition: none; transform: none; } }
.gn-chat-panel {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; height: var(--gn-sheet);
  display: flex; flex-direction: column; background: var(--paper); border-top: 1px solid var(--line);
  box-shadow: 0 -4px 16px rgb(0 0 0 / 0.18); transform: translateY(100%); visibility: hidden;
  transition: transform 0.2s ease, height 0.2s ease, visibility 0s linear 0.2s;
}
.gn-chat-open .gn-chat-panel { transform: none; visibility: visible; transition-delay: 0s; }
@media (prefers-reduced-motion: reduce) { .gn-chat-panel { transition: none; } }
@media (min-width: 60rem) {
  .gn-main { padding-bottom: 3rem; }
  body.gn-chat-open .gn-main { padding-bottom: 3rem; }
  /* The board is beside the panel here and says whose turn it is. */
  .gn-chat-turn, .gn-chat-ask { display: none !important; }
  .gn-chat-panel {
    left: auto; top: 0; width: 20rem; height: auto; border-top: 0; border-left: 1px solid var(--line);
    box-shadow: var(--shadow); transform: translateX(100%);
  }
}
.gn-chat-head, .gn-rulebook-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.8rem; border-bottom: 1px solid var(--line); }
/* A phone's sheet, chat's and the rules' alike, has two heights: its usual
   one, with the board above it, and the whole screen, for reading back
   (Carol, 031: "A way to see the board and read chat at once on a phone",
   and the ticket's "it can be pulled up to full height"). The grip along
   its top (`Views.SheetGrip`, sheet.js) goes between them, tapped, clicked
   or dragged, and so does a drag on the head, which therefore keeps the
   page from taking that drag as a scroll. The grip is a pill, the way
   sheets on phones show they can be pulled, on a strip the width of the
   sheet so a thumb can't miss it. A wide screen's docks don't have one. */
.gn-sheet-grip {
  flex: none; display: flex; align-items: center; justify-content: center; width: 100%; height: 1.5rem;
  margin: 0; padding: 0; background: transparent; border: 0; cursor: row-resize; touch-action: none;
}
.gn-sheet-grip::before { content: ""; width: 3rem; height: 0.3rem; border-radius: 999px; background: var(--edge); }
.gn-sheet-grip:hover::before { background: var(--ink-soft); }
@media (max-width: 59.99rem) {
  .gn-chat-head, .gn-rulebook-head { padding-top: 0; padding-bottom: 0.35rem; touch-action: none; }
}
@media (min-width: 60rem) { .gn-sheet-grip { display: none; } }
.gn-chat-head h2, .gn-rulebook-head h2 { margin: 0; font-size: 1.1rem; }
.gn-chat-close, .gn-rulebook-close {
  white-space: nowrap; min-height: 2.5rem; padding: 0 0.8rem; font: inherit; font-weight: 600;
  cursor: pointer; color: var(--ink); background: var(--paper-2); border: 2px solid var(--edge);
  border-radius: var(--radius);
}
/* The panel doesn't go away, it tucks back into its tab, so the button
   shows which way: a caret down on a phone, where the sheet slides down,
   and two carets right on a wide screen, where it slides to the rail. Both
   icons are in the markup (`Views.HideIcons`); the words, "Hide chat" and
   "Hide rules", are for a screen reader. */
.gn-chat-close, .gn-rulebook-close { display: inline-flex; align-items: center; justify-content: center; min-width: 2.75rem; }
.gn-chat-close .gn-icon, .gn-rulebook-close .gn-icon { width: 1.3rem; height: 1.3rem; }
.gn-icon-side { display: none; }
@media (min-width: 60rem) {
  .gn-icon-down { display: none; }
  .gn-icon-side { display: inline-block; }
}
.gn-chat-log { flex: 1; min-height: 0; overflow-y: auto; padding: 0.6rem 0.8rem; display: flex; flex-direction: column; gap: 0.4rem; }
.gn-chat-msg { margin: 0; padding: 0.35rem 0.7rem; max-width: 90%; overflow-wrap: anywhere; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); }
.gn-chat-mine { align-self: flex-end; background: var(--paper-2); }
.gn-chat-who { margin-right: 0.15rem; }
.gn-chat-marker { margin: 0.4rem 0; text-align: center; font-size: 0.85rem; color: var(--ink-soft); }
/* A result line opens the finished board; an evening's divider stands out
   from the markers between games, so the evenings can be told apart. */
.gn-chat-marker a { color: inherit; }
.gn-chat-evening { margin-top: 0.9rem; font-weight: 700; color: var(--ink); }
.gn-chat-form { display: flex; gap: 0.5rem; padding: 0.6rem 0.8rem; border-top: 1px solid var(--line); }
.gn-chat-input { flex: 1; min-width: 0; min-height: 2.75rem; padding: 0 0.7rem; font: inherit; border: 1px solid var(--edge); border-radius: var(--radius); }

/* ---- Rules ---- */
/* The Rules sheet (`Views.rulesSheet`) is built like the chat's, and only
   one of the two is open at a time (rules.js, chat.js). On a phone its tab
   is the left end of the bottom bar, and the sheet rises from the bar,
   taller than chat's because it is for reading, but short
   enough that the top of the table, where a question to the reader
   appears, is still in view. On a wide screen its tab is a handle on the
   right edge above chat's, and it docks beside the page. */
:root { --gn-rail: 3.5rem; --gn-rulebook-tab: 6.5rem; --gn-rulebook-sheet: clamp(15rem, 55vh, 34rem); }
@supports (height: 1dvh) { :root { --gn-rulebook-sheet: clamp(15rem, 55dvh, 34rem); } }
body:has(#rules) .gn-chat-tab { left: var(--gn-rulebook-tab); }
.gn-rulebook-tab {
  position: fixed; left: 0; bottom: 0; z-index: 20; width: var(--gn-rulebook-tab); min-height: 3.25rem;
  display: flex; align-items: center; justify-content: center; gap: 0.4rem;
  font: inherit; font-weight: 700; cursor: pointer; color: var(--felt-ink); background: var(--felt);
  border: 0; border-right: 1px solid rgb(255 255 255 / 0.35); box-shadow: 0 -2px 8px rgb(0 0 0 / 0.15);
}
body.gn-rulebook-open .gn-main, body.gn-game-page-open .gn-main { padding-bottom: calc(var(--gn-rulebook-sheet) + var(--gn-bar, 3.25rem) + 1rem); }
.gn-rulebook-panel {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; height: var(--gn-rulebook-sheet);
  display: flex; flex-direction: column; background: var(--paper); border-top: 1px solid var(--line);
  box-shadow: 0 -4px 16px rgb(0 0 0 / 0.18); transform: translateY(100%); visibility: hidden;
  transition: transform 0.2s ease, height 0.2s ease, visibility 0s linear 0.2s;
}
.gn-rulebook.gn-rulebook-open .gn-rulebook-panel { transform: none; visibility: visible; transition-delay: 0s; }
@media (prefers-reduced-motion: reduce) { .gn-rulebook-panel { transition: none; } }
.gn-rulebook-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0.4rem 1rem 1.5rem; }
.gn-rulebook-body:focus { outline: none; }
/* On a phone both sheets sit on the bar, which stays showing under them
   (see Chat above), here after both sheets' own rules so these win. */
@media (max-width: 59.99rem) {
  /* Both heights stop at the top of the bar, and a closed sheet slides
     all the way down past it. */
  .gn-chat-panel, .gn-rulebook-panel {
    bottom: var(--gn-bar, 3.25rem); transform: translateY(calc(100% + var(--gn-bar, 3.25rem)));
  }
  .gn-chat-panel.gn-sheet-full, .gn-rulebook-panel.gn-sheet-full { height: calc(var(--gn-sheet-full) - var(--gn-bar, 3.25rem)); }
  /* The open sheet's tab is in the sheet's colours, as if the sheet came
     out of it, the way a tab on a folder is the folder's. */
  .gn-chat-tab[aria-expanded="true"], .gn-rulebook-tab[aria-expanded="true"] {
    color: var(--ink); background: var(--paper); box-shadow: inset 0 -4px 0 var(--accent);
  }
  .gn-chat-tab[aria-expanded="true"] .gn-chat-turn { color: var(--accent-ink); background: var(--accent); }
}
@media (min-width: 60rem) {
  body.gn-rulebook-open .gn-main, body.gn-game-page-open .gn-main { padding-bottom: 3rem; }
  .gn-rulebook-panel {
    left: auto; top: 0; width: 26rem; height: auto; border-top: 0; border-left: 1px solid var(--line);
    box-shadow: var(--shadow); transform: translateX(100%);
  }

  /* One rail down the right edge holds both handles, Chat and Rules,
     alike and always there, the way other apps fold a sidebar: a panel
     opens to the rail's left, its name in the rail shows it open, and
     pressing that name folds it back. Each is its icon above its word.
     The page keeps clear of the rail and of whichever panel is open. */
  body:has(.gn-chat-tab, .gn-rulebook-tab) { padding-right: var(--gn-rail); }
  body.gn-chat-open { padding-right: calc(20rem + var(--gn-rail)); }
  body.gn-rulebook-open, body.gn-game-page-open { padding-right: calc(26rem + var(--gn-rail)); }
  body:has(.gn-chat-tab, .gn-rulebook-tab)::after {
    content: ""; position: fixed; top: 0; right: 0; bottom: 0; z-index: 19; width: var(--gn-rail);
    background: var(--paper-2); border-left: 2px solid var(--edge);
  }
  .gn-chat-panel, .gn-rulebook-panel { right: var(--gn-rail); }
  .gn-chat-tab, .gn-rulebook-tab {
    display: flex; flex-direction: column; gap: 0.25rem; left: auto; right: 0; bottom: auto;
    width: var(--gn-rail); min-width: 0; min-height: 4.5rem; padding: 0.5rem 0; font-size: 0.8rem;
    color: var(--ink); background: transparent; border: 0; border-radius: 0; box-shadow: none;
  }
  .gn-chat-tab .gn-icon, .gn-rulebook-tab .gn-icon { width: 1.75rem; height: 1.75rem; }
  body:has(#rules) .gn-chat-tab { left: auto; }
  .gn-chat-tab { top: 0.75rem; }
  .gn-rulebook-tab { top: 5.5rem; }
  .gn-game-history-tab { top: 10.25rem; }
  /* The count sits on the icon's corner, so it adds no height to the rail. */
  .gn-chat-tab .gn-chat-unread { position: absolute; top: 0.15rem; right: 0.15rem; min-width: 1.2rem; line-height: 1.2rem; padding: 0 0.25rem; font-size: 0.75rem; }
  .gn-chat-tab:hover, .gn-rulebook-tab:hover { background: var(--line); }
  body .gn-chat-tab[aria-expanded="true"], body .gn-rulebook-tab[aria-expanded="true"] {
    background: var(--paper); box-shadow: inset 4px 0 0 var(--accent);
  }
}

/* ---- History and Lore ----
   A game with a history or lore page (`Views.extraSheet`) has a sheet for
   each, a page of the same book, so it is the Rules sheet in every part
   (the .gn-rulebook classes) but its tab: beside Rules on a phone's bar,
   below it on the rail, History first. While the table is on a game
   without one the sheet is hidden, and the bar and rail are as they were.
   game-pages.js puts gn-game-page-open on the body while either is open.

   Three or four tabs side by side, each its icon beside its word, don't
   fit a 390px phone once Chat shows "Your turn" and an unread count,
   which is when it matters most. So while there are more than two, Rules,
   History and Lore stand their icon above their word, as phones' tab
   bars do and as the rail does, and Chat keeps the rest of the bar. With
   all four, the book's tabs are narrower and Chat stands too, so "Your
   turn" and the count still fit beside it. On the rail "History" is the
   longest word, so its letters sit a little closer to clear the edges. */
:root { --gn-book-tab-stacked: 4.5rem; }
@media (max-width: 59.99rem) {
  body:has(.gn-game-page:not([hidden])) .gn-rulebook-tab {
    flex-direction: column; gap: 0.15rem; width: var(--gn-book-tab-stacked); padding: 0.3rem 0; font-size: 0.8rem;
  }
  .gn-game-history-tab, .gn-game-lore-tab { left: var(--gn-book-tab-stacked); }
  body:has(#game-history:not([hidden])) .gn-game-lore-tab { left: calc(2 * var(--gn-book-tab-stacked)); }
  body:has(.gn-game-page:not([hidden])) .gn-chat-tab { left: calc(2 * var(--gn-book-tab-stacked)); }
  body:has(#game-history:not([hidden])):has(#game-lore:not([hidden])) { --gn-book-tab-stacked: 4rem; }
  body:has(#game-history:not([hidden])):has(#game-lore:not([hidden])) .gn-chat-tab {
    left: calc(3 * var(--gn-book-tab-stacked)); padding: 0 0.5rem; gap: 0.4rem;
  }
  body:has(#game-history:not([hidden])):has(#game-lore:not([hidden])) .gn-chat-name {
    display: flex; flex-direction: column; align-items: center; gap: 0.15rem; font-size: 0.8rem;
  }
  body:has(#game-history:not([hidden])):has(#game-lore:not([hidden])) .gn-game-history-tab { letter-spacing: -0.04em; }
}
@media (min-width: 60rem) {
  .gn-game-history-tab { letter-spacing: -0.04em; }
  .gn-game-lore-tab { top: 10.25rem; }
  body:has(#game-history:not([hidden])) .gn-game-lore-tab { top: 15rem; }
}

/* ---- Rule books: a chapter on its page or in the sheet ---- */
.gn-book h1 { font-size: 1.6rem; margin-top: 0.6rem; }
.gn-book h2 { font-size: 1.25rem; margin: 1.4rem 0 0.4rem; }
.gn-book ul, .gn-book ol { margin: 0.4rem 0; padding-left: 1.4rem; }
.gn-book li { margin: 0.3rem 0; }
.gn-book li > ul { margin: 0.2rem 0; }
.gn-book-table { overflow-x: auto; }
.gn-book table { border-collapse: collapse; margin: 0.6rem 0; }
.gn-book th, .gn-book td { padding: 0.3rem 0.6rem; text-align: left; border: 1px solid var(--line); }
.gn-book td:last-child { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.gn-rulebook-body .gn-book h1 { font-size: 1.4rem; }
.gn-book-nav { font-size: 0.95rem; }
.gn-at-table { padding: 0.5rem 0.8rem; background: var(--paper-2); border-radius: var(--radius); }
.gn-book-games-title { margin-top: 0.4rem; }
.gn-book-games { margin: 0.4rem 0 1rem; padding-left: 1.4rem; }
.gn-book-games li { margin: 0.35rem 0; }
/* The About page (`About.page`): the version stands out as the one fact a
   person comes for, and a licence notice is folded until asked for. */
.gn-about-version { padding: 0.5rem 0.8rem; background: var(--paper-2); border-radius: var(--radius); }
.gn-notice { margin: 0.5rem 0; }
.gn-notice summary { cursor: pointer; padding: 0.5rem 0; }
.gn-notice pre, .gn-licence {
  margin: 0.3rem 0 0.8rem; padding: 0.7rem 0.9rem; background: var(--paper-2); border: 1px solid var(--line);
  border-radius: var(--radius); font: 0.8rem var(--font); white-space: pre-wrap; overflow-wrap: anywhere;
}
.gn-howto { display: inline-flex; align-items: center; min-height: 2.5rem; font-size: 0.95rem; }
/* A picker card holds two things to press: the game (a button, the most of
   it) and "How to play" along its foot. The card is the list item, so the
   link plainly belongs to the game above it and not the next one down. */
.gn-pick {
  display: grid; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.gn-pick:has(.gn-game-pick:hover) { border-color: var(--accent); }
.gn-pick .gn-game-pick { border: 0; box-shadow: none; background: transparent; padding-bottom: 0.3rem; }
.gn-pick .gn-howto { justify-self: start; padding: 0 1rem 0.4rem; }

/* ---- Who is who: the line above a two-player board ---- */
.gn-matchup { margin: 0.3rem 0; }
.gn-side { font-weight: 600; white-space: nowrap; }
.gn-swatch {
  display: inline-block; width: 0.8em; height: 0.8em; margin-right: 0.35em; border-radius: 50%;
  vertical-align: baseline; border: 1px solid rgb(0 0 0 / 0.35);
}
.gn-swatch.red { background: var(--x); }
.gn-swatch.blue { background: #3d86c6; }
.gn-swatch.black { background: #2a2622; }
.gn-swatch.white { background: #f7f2e6; }

/* ---- Chairs: who got up, putting a game away, nudging ---- */
.gn-chairs .gn-put-away { flex: none; margin-top: 0; }
/* A game nobody in it has tended for an hour (`Views.nobodyHere`). */
.gn-nobody-here p { margin: 0 0 0.4rem; }
.gn-nobody-here .gn-put-away { margin-top: 0; }
/* A put-away question for someone who must answer it (`Views.putAwayRequest`):
   stuck to the top of the screen, whatever is scrolled or open, and pulsed
   once by PlayOnce when it arrives. The thick border carries it for anyone
   who has turned motion off. */
.gn-question {
  position: sticky; top: 0.5rem; z-index: 25; border: 3px solid var(--bad);
  box-shadow: 0 4px 16px rgb(0 0 0 / 0.3);
}
.gn-question p { margin: 0 0 0.4rem; }
.gn-ask-line { font-size: 1.3rem; line-height: 1.3; }
.gn-ask-buttons { display: flex; gap: 0.6rem; margin-top: 0.7rem; }
.gn-ask-buttons .gn-button { flex: 1; min-height: 3.5rem; font-size: 1.05rem; }
@media (prefers-reduced-motion: no-preference) {
  .gn-question.gn-fresh { animation: gn-ask 0.8s ease-out calc(0s - var(--played, 0s)) 3; }
}
@keyframes gn-ask {
  0% { box-shadow: 0 0 0 0 var(--bad); transform: scale(1); }
  30% { transform: scale(1.03); }
  100% { box-shadow: 0 0 0 1.2rem transparent; transform: scale(1); }
}
/* For everyone else it's a line, not a card. */
.gn-ask-quiet { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 1rem; padding: 0.2rem 0.2rem; color: var(--ink-soft); }
.gn-ask-quiet p { margin: 0; flex: 1 1 14rem; }
.gn-ask-quiet .gn-button { flex: none; }
.gn-ask-mine { position: sticky; top: 0.5rem; z-index: 25; padding: 0.7rem 1rem; }
.gn-countdown { font-variant-numeric: tabular-nums; font-weight: 700; }
.gn-table-controls { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0 1.2rem; }
.gn-table-controls > .gn-button { margin-top: 0.5rem; }
.gn-resign { align-self: center; }

/* A draw on the table (`DrawTalk`), and the row where Offer a draw and
   Resign sit side by side (`gn-controls`), for the board games that have both. */
.gn-offer { margin: 0 0 0.5rem; padding: 0.4rem 0.7rem; background: var(--paper-2); border: 1px solid var(--line); border-radius: var(--radius); font-weight: 700; }
.gn-offer p { margin: 0; }
.gn-offer .gn-actions { margin-top: 0.4rem; }
.gn-controls { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0 1.5rem; }

/* ---- The cage (`Cage`): the wallet's balance, asking for chips, the ledger ---- */
.gn-cage-given { padding: 0.5rem 0.8rem; background: var(--paper-2); border-left: 4px solid var(--good); border-radius: var(--radius); font-weight: 700; }
.gn-cage-balance { margin-bottom: 1rem; }
.gn-cage-label { margin: 0; color: var(--ink-soft); }
.gn-cage-chips { margin: 0 0 0.6rem; font-size: 2.2rem; font-weight: 800; line-height: 1.15; font-variant-numeric: tabular-nums; }
.gn-cage-net { margin: 0; display: grid; gap: 0.3rem; }
.gn-cage-net > div { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0 1rem; padding-top: 0.3rem; border-top: 1px solid var(--line); }
.gn-cage-net dt { color: var(--ink-soft); }
.gn-cage-net dd { margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }
.gn-cage-net dd.gn-cage-none { font-weight: 400; color: var(--ink-soft); font-style: italic; }
.gn-cage-quick { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.8rem; }
.gn-cage-quick form { margin: 0; }
.gn-cage-ledger { list-style: none; margin: 0; padding: 0; }
.gn-cage-ledger li {
  display: grid; grid-template-columns: 1fr auto; gap: 0 1rem; align-items: baseline;
  padding: 0.5rem 0; border-bottom: 1px solid var(--line);
}
.gn-cage-ledger time { grid-column: 1 / -1; font-size: 0.85rem; color: var(--ink-soft); }
.gn-cage-amount { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.gn-cage-in { color: var(--good); }
.gn-cage-out { color: var(--bad); }
