/* Theme: same design language as the Ruleset Ledger reference doc -
   Fraunces for headings, IBM Plex Sans for everything else, a warm
   paper/ink neutral pair, and one plum accent - extended here across
   the whole site instead of staying a one-off artifact style. Fonts are
   loaded from index.html's <head> (a real site, unlike an artifact, is
   free to load an external stylesheet) rather than an @import here, so
   the font request starts in parallel with the page instead of waiting
   on this file to load first. Every color below is a token; a color
   literal outside this block is a deliberate, permanent exception (the
   two banners, and the board's own always-light middle overlay, all
   already fixed once for going illegible in dark mode - see the
   comments at each), not an oversight to fix later. */

:root {
  color-scheme: light dark;
  --font-display: "Fraunces", Georgia, serif;
  --font-body: "IBM Plex Sans", system-ui, sans-serif;

  --bg: #eef0ec;
  --surface: #ffffff;
  --surface-alt: #f7f8f5;
  --ink: #1c2321;
  --ink-soft: #5a655f;
  --rule: #dde1d8;
  --accent: #4a3b6b;
  --accent-soft: #4a3b6b1a;
  --accent-ink: #ffffff;

  --good: #2f6b4f;
  --good-bg: #2f6b4f14;
  --warn: #93690a;
  --warn-bg: #93690a14;
  --info: #2f5d7a;
  --info-bg: #2f5d7a14;
  --bad: #a8452f;
  --bad-bg: #a8452f14;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #141815;
    --surface: #1b201d;
    --surface-alt: #202620;
    --ink: #e8ece6;
    --ink-soft: #9aa59c;
    --rule: #313830;
    --accent: #b3a2d9;
    --accent-soft: #b3a2d926;
    --accent-ink: #1c1428;

    --good: #6cbb92;
    --good-bg: #6cbb9220;
    --warn: #dcae4a;
    --warn-bg: #dcae4a20;
    --info: #7cb3da;
    --info-bg: #7cb3da20;
    --bad: #e08a70;
    --bad-bg: #e08a7020;
  }
}

:root[data-theme="dark"] {
  --bg: #141815;
  --surface: #1b201d;
  --surface-alt: #202620;
  --ink: #e8ece6;
  --ink-soft: #9aa59c;
  --rule: #313830;
  --accent: #b3a2d9;
  --accent-soft: #b3a2d926;
  --accent-ink: #1c1428;

  --good: #6cbb92;
  --good-bg: #6cbb9220;
  --warn: #dcae4a;
  --warn-bg: #dcae4a20;
  --info: #7cb3da;
  --info-bg: #7cb3da20;
  --bad: #e08a70;
  --bad-bg: #e08a7020;
}

* {
  box-sizing: border-box;
}

/* min-height:100vh plus box-sizing:border-box (so the padding below
   does not push it taller than that) exists for one reason: it gives
   body's own flex layout a definite height to stretch #game-screen
   into (align-items defaults to stretch, and body has only the one flex
   child), which is what lets #board-section's flex:1 below have any
   real "space left over" to grow into - a flex item cannot grow to
   fill remaining space inside a parent that is itself only ever as
   tall as its own content. Content taller than the viewport (a long
   trade offers list, say) still grows body past 100vh and scrolls
   normally; this is a floor, not a cap. */
body {
  margin: 0;
  box-sizing: border-box;
  min-height: 100vh;
  /* Less top/bottom than left/right on purpose: the complaint was
     specifically vertical gap above and below the game, not the page's
     side margins. */
  padding: 0.75rem 1.25rem;
  display: flex;
  justify-content: center;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  line-height: 1.5;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  text-wrap: balance;
}

button {
  font-family: var(--font-body);
  font-weight: 600;
  background: var(--accent);
  color: var(--accent-ink);
  border: none;
  border-radius: 0.35rem;
  cursor: pointer;
}

button:hover:not(:disabled) {
  filter: brightness(1.1);
}

button:focus-visible,
input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  filter: none;
}

button.secondary {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--rule);
}

input[type="text"],
input[type="number"] {
  font-family: var(--font-body);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 0.3rem;
}

/* #game-screen sets its own display:flex below (needed to lay out its
   children), which otherwise wins over the browser's default
   [hidden]{display:none} regardless of specificity, since that default
   is a lower priority user agent rule, not because of selector weight.
   Without this, toggling the hidden property in showScreen() (main.js)
   stops actually hiding it: the home page and an "open" game screen were
   rendering side by side. !important is deliberate here, not a
   shortcut - it is what keeps this guarantee true regardless of
   whatever display value any hideable element sets for itself. */
[hidden] {
  display: none !important;
}

#join-screen {
  max-width: 24rem;
  text-align: center;
}

#join-screen input {
  font-size: 1rem;
  padding: 0.5rem;
  margin-right: 0.5rem;
}

#join-screen button {
  font-size: 1rem;
  padding: 0.5rem 1rem;
}

#home-screen {
  max-width: 30rem;
  width: 100%;
}

#rules-details {
  margin: 1rem 0;
  padding: 0.6rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
}

#rules-details summary {
  cursor: pointer;
  font-weight: 600;
  font-family: var(--font-display);
}

#rules-note {
  margin: 0.6rem 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

#rules-list {
  margin: 0;
  padding-left: 1.2rem;
}

#rules-list li {
  margin-bottom: 0.4rem;
  font-size: 0.9rem;
}

#game-options-details {
  margin: 0 0 1rem;
  padding: 0.6rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
}

#game-options-details summary {
  cursor: pointer;
  font-weight: 600;
  font-family: var(--font-display);
}

#game-options-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 0.7rem;
}

#game-options-form label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

#game-options-form label.checkbox-row {
  flex-direction: row;
  align-items: center;
}

#game-options-form input[type="number"] {
  font-size: 1rem;
  padding: 0.35rem;
  max-width: 10rem;
}

/* Nested inside #game-options-details, holding the deeper knobs (the per
   level rent curve, the income tax basis) that most games never need to
   touch - a second, collapsed-by-default tier rather than a peer section,
   so Simple stays the 9 fields above and Full is this same panel with
   Advanced also opened. */
#advanced-options-details {
  margin-top: 0.8rem;
  padding: 0.5rem 0.7rem 0.6rem;
  border-left: 2px solid var(--rule);
}

#advanced-options-details summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

#advanced-options-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 0.7rem;
}

.opt-default {
  font-weight: normal;
  color: var(--ink-soft);
  font-size: 0.8em;
}

#profile-buttons {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.2rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--rule);
}

#profile-buttons-label {
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.profile-button {
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
}

#cleanup-button,
#back-to-home-button,
#back-to-home-from-create-button {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--rule);
}

#home-screen > button {
  font-size: 1rem;
  padding: 0.5rem 1rem;
}

#create-screen {
  max-width: 30rem;
  width: 100%;
}

#create-screen #new-game-name-input {
  width: 100%;
  font-size: 1rem;
  padding: 0.5rem;
  margin-bottom: 1rem;
}

#create-screen-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 1rem;
}

#create-screen-actions button {
  font-size: 1rem;
  padding: 0.5rem 1rem;
}

#games-list {
  list-style: none;
  padding: 0;
}

#games-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem;
  border-bottom: 1px solid var(--rule);
}

#game-screen {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  width: 100%;
  max-width: 1450px;
}

/* flex:1 (with min-height:0, so it is also free to shrink below its
   content's natural size) claims whatever vertical space #game-screen's
   flex column has left after every sibling's own natural height - the
   real, browser-computed number, not a guess. sizeBoardToAvailableSpace()
   (main.js) reads that back via this section's own clientWidth/clientHeight
   and sizes #board-wrap to the largest square that fits both, which is
   also what actually answers "make the board use more of the page's
   width" - a width driven only by a height guess (vh) could never do
   that on its own. */
/* flex-basis:0, not auto: this was the actual bug behind the resize
   misalignment and the board not visibly growing. With auto, this
   section's own "preferred" size before flex-grow is computed FROM its
   content - and its content, #board-wrap, is a square whose size
   sizeBoardToAvailableSpace() (main.js) sets directly. That made this
   section's own computed size depend on its child's current size,
   which the JS had just set FROM this section's size the moment
   before - a circular dependency that could settle on a stale or
   inconsistent value depending on exactly when a resize fired, instead
   of the clean, one-directional "available space in, board size out"
   this was supposed to be. flex-basis:0 makes this section's size
   purely a function of leftover space in #game-screen's column, with
   no dependency on what is inside it. */
/* min-height matches MIN_BOARD_PX in main.js, the smallest the board is
   ever sized to. With min-height:0, anything that ate enough of the
   column (a tall trade panel, the game over banner, a wrapped players
   bar) left this section shorter than that floor, and the board spilled
   out of it on top of the panels below. Now the page scrolls a little
   instead. Like flex-basis:0, this is a fixed number, not anything read
   from the board itself, so it adds no circular dependency. */
#board-section {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1 1 0;
  min-height: 320px;
}

/* width/height are set directly by sizeBoardToAvailableSpace() (main.js,
   above 700px wide) to the largest square that actually fits
   #board-section's real available space in both directions - width:100%
   here is only the fallback for the instant before that first runs, and
   what mobile (that function is a no-op under 700px) uses instead: full
   page width, natural aspect-ratio, normal page flow. Every element
   drawn inside the board (drawBoard() etc., main.js) is one shared
   0-820/0-820 viewBox, so all of it - property cards, corner logos,
   flags, upgrade icons - scales together as this element's own size
   changes, not something sized per element. */
/* max-width must match MAX_BOARD_PX in main.js exactly, not just be
   "close" - sizeBoardToAvailableSpace() sets width/height inline to the
   same computed number, but max-width still wins over an inline width
   regardless of where that width came from. A lower CSS ceiling here
   than the JS one meant any computed size landing between the two
   numbers got its width clamped down while its height (no matching
   max-height exists anywhere) did not - board-wrap silently stopped
   being square, and every percentage positioned inside it (the void,
   the dice row pinned to its bottom edge) drifted out of alignment
   with the actual square board the svg was still drawing beneath it.
   That mismatch is exactly what got screenshotted, and why it only
   showed up at some window sizes and not others: only a computed size
   landing in the old 1150-1300px gap ever hit it. */
#board-wrap {
  position: relative;
  width: 100%;
  max-width: 1300px;
}

/* Direct child only, not #board-wrap svg: #dice-display also lives
   inside #board-wrap now (nested in #dice-controls-overlay), and a
   descendant selector here previously forced it to this same
   width:100%/aspect-ratio:1 sizing too - dice sized like a second full
   width square board, overflowing everything below them. */
#board-wrap > svg {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
}

/* Sized to the board svg's own empty middle square: with viewBox 0 0 820
   820 and a 510x510 hole centered in it, that square starts at 155/820
   (18.9%) and spans 510/820 (62.2%) on each axis, so this scales with the
   board at any width without needing to know its rendered pixel size.
   My properties and the dice/roll row both live in here now (Players
   moved to #players-bar above the board entirely), but as two
   independently absolutely positioned children now, not flex column
   items sharing this box's total height as one budget - that let
   #dice-controls-overlay's real content overflow past this box's own
   percentage-of-board-wrap height in more than one way (flex-wrap
   briefly doubling its height was one; more fundamentally, two flex
   items sharing one hard-capped total meant anything that made either
   one taller than expected could still push the other past this box's
   own bottom edge, straight into the board's drawn property row right
   below it, exactly what got reported and screenshotted). Each of the
   two positions itself against this box directly instead: see
   #my-properties-panel and #dice-controls-overlay's own rules, and
   positionDiceRow() in main.js, which is what actually keeps them from
   overlapping - #dice-controls-overlay is never budget-constrained at
   all, only ever sized by its own real content, and
   #my-properties-panel's own "bottom" is set to match wherever that
   real content actually ends. Text and border colors on this element
   and its children below are hardcoded to the light theme's own values
   rather than the normal tokens, on purpose: on a wide viewport this
   sits over the board's own always-light svg background regardless of
   the page's light/dark mode, so a token that flips to light text in
   dark mode would go near invisible here. The under-700px media query
   further down swaps every one of these back to the real tokens, since
   below that width this panel leaves the board and sits in normal page
   flow instead, where the page's own background (which does flip) is
   what is actually behind it. Also a named size container, so the dice
   row inside can respond to this box's own width (see the @container
   rules after #last-roll below) rather than the viewport's, since the
   board's size depends on the window's height as much as its width. */
#board-middle-overlay {
  position: absolute;
  top: 18.9%;
  left: 18.9%;
  width: 62.2%;
  height: 62.2%;
  box-sizing: border-box;
  padding: 0.6rem;
  color: #1c2321;
  container: board-middle / inline-size;
}

#board-middle-overlay .hand-card-caption {
  color: #5a655f;
}

/* top/left/right pin this to the box above; bottom is set in px by
   positionDiceRow() (main.js) to end exactly where #dice-controls-overlay
   actually starts, with a small gap - never a shared flex budget with
   it, so this can never push it (or be pushed by it) into overflow. */
#board-middle-overlay #my-properties-panel {
  position: absolute;
  top: 0.6rem;
  left: 0.6rem;
  right: 0.6rem;
  min-width: 0;
  overflow-y: auto;
}

#board-middle-overlay h2 {
  margin: 0 0 0.4rem;
  font-size: 0.95rem;
}

/* Pinned to this box's own bottom edge and sized purely by its own
   content (dice, the Roll dice button, and a Recent Activity preview,
   all one row) - never a percentage or a shared flex budget, so it can
   never itself be the thing that overflows. positionDiceRow() (main.js)
   reads its real rendered height back out to keep #my-properties-panel
   from overlapping it; nothing here needs to know that height itself.
   nowrap, not the flex-wrap this briefly had: #recent-activity already
   has min-width:0 and truncates its own text with an ellipsis rather
   than needing a second line to fall back on. */
#dice-controls-overlay {
  position: absolute;
  left: 0.6rem;
  right: 0.6rem;
  bottom: 0.6rem;
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 0.6rem;
}

@media (max-width: 700px) {
  /* sizeBoardToAvailableSpace() (main.js) is a no-op down here - the
     board goes back to plain width:100% in normal page flow - so this
     section should not still be claiming a flex-distributed share of
     #game-screen's height either; that fights the board's own natural,
     width-driven size instead of just leaving it alone. */
  #board-section {
    flex: none;
    min-height: auto;
  }

  #board-middle-overlay {
    position: static;
    width: auto;
    height: auto;
    margin-top: 1rem;
    /* Not a size container down here: this box is page width now, and
       the dice row has room for everything, so none of the small board
       @container rules below should ever apply. */
    container-type: normal;
    /* Back to the real, theme aware tokens: down here this sits in
       normal page flow on the page's own background, not over the
       board's always-light svg, so the hardcoded colors above (picked
       specifically for that svg background) are the wrong choice here. */
    color: var(--ink);
  }

  #board-middle-overlay #my-properties-panel {
    position: static;
    overflow-y: visible;
  }

  #board-middle-overlay .hand-card-caption {
    color: var(--ink-soft);
  }

  /* wrap is fine again down here: the nowrap reasoning on the base rule
     is about staying one row inside the board's middle, which this no
     longer is. See #recent-activity's own override further down. */
  #dice-controls-overlay {
    position: static;
    margin-top: 0.5rem;
    flex-wrap: wrap;
  }

  /* #recent-activity, #recent-activity-label and #last-roll get their
     own overrides for this width in a second max-width: 700px block
     right after their base rules further down, not here: same
     specificity, so from here those later base rules would win on
     source order alone. */
}

#panels-section {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
  align-items: flex-start;
  border-top: 1px solid var(--rule);
  padding-top: 1rem;
}

#controls-panel {
  min-width: 14rem;
}

/* Activity, proposing a trade, and reading trade offers share one row,
   a third each, rather than the activity log spanning the full page
   width it never actually needs (the text in it is never more than a
   short line) or trade offers sitting stacked below the propose form
   where a new one could scroll out of sight unnoticed. */
#bottom-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 2rem;
  align-items: start;
}

@media (max-width: 1000px) {
  #bottom-row {
    grid-template-columns: 1fr 1fr;
  }
}

@media (max-width: 700px) {
  #bottom-row {
    grid-template-columns: 1fr;
  }
}

/* The property card's own white background and black outline are a
   deliberate, explicit spec (a blank card, colored only by its header),
   not part of this theme pass - the ink/muted text tokens below are as
   far as the board's own art follows the site theme. */
.property-card-bg {
  fill: #ffffff;
  stroke: #111;
  stroke-width: 1.5;
}

.property-card-bg.owner-in-prison {
  stroke-dasharray: 4 3;
}

.property-card-header {
  stroke: #111;
  stroke-width: 1;
}

.property-card-name {
  font-size: 10px;
  font-weight: 600;
  dominant-baseline: middle;
  pointer-events: none;
  fill: #111;
}

.property-card-code {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 1px;
  dominant-baseline: middle;
  pointer-events: none;
  fill: #111;
}

.property-card-price {
  font-size: 9px;
  dominant-baseline: middle;
  pointer-events: none;
  fill: #333;
}

.flag-ring {
  fill: none;
  stroke: #111;
  stroke-width: 1.5;
  pointer-events: none;
}

.board-space-special {
  fill: #f7f3e8;
  stroke: #111;
  stroke-width: 1.5;
}

.space-symbol {
  font-size: 46px;
  font-weight: bold;
  text-anchor: middle;
  dominant-baseline: middle;
  pointer-events: none;
  fill: #705a99;
}

.board-middle {
  fill: #fbfbf8;
  stroke: #ccc;
  stroke-width: 1;
}

.die-face {
  fill: #ffffff;
  stroke: #4a3b6b;
  stroke-width: 2;
}

.die-pip {
  fill: #4a3b6b;
}

#game-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--rule);
}

#game-header button {
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
}

#game-header button.secondary {
  background: var(--surface);
}

#game-label {
  margin-left: auto;
  color: var(--ink-soft);
  font-size: 0.85rem;
}

.space-label {
  font-size: 9px;
  text-anchor: middle;
  dominant-baseline: middle;
  pointer-events: none;
  fill: #555;
}

.player-token {
  stroke: #fff;
  stroke-width: 1.5;
}

#dice-controls-overlay button {
  font-size: 1rem;
  padding: 0.5rem 1rem;
  flex-shrink: 0;
}

/* Compact dice, drawn small enough to share a row with the button
   (drawDice() in main.js) rather than the board's own dead center - the
   space that freed up went to #board-middle-overlay's #my-properties-panel
   instead. Sized to the button's own height (1rem font-size * 1.5 line
   height + 0.5rem padding top/bottom = 2.5rem, since border is none),
   not an arbitrary smaller number, so the two sit as a matched pair
   rather than one dwarfing the other. */
#dice-display {
  width: 4.6rem;
  height: 2.5rem;
  flex-shrink: 0;
}

/* A live preview of the most recent entry from the same log stream that
   fills #log-list (Full Activity History, bottom of the page) - not
   just dice rolls, socket.on("log", ...) in main.js updates this on
   every entry so it also shows rent paid, a purchase, a card drawn, and
   so on. Shares the dice/button row, so it needs to be able to shrink
   and truncate rather than wrapping or forcing the row taller. */
#recent-activity {
  min-width: 0;
  flex: 1 1 auto;
}

#recent-activity-label {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* See #last-roll below for why this is hardcoded rather than a token. */
  color: #8a7a9a;
}

#last-roll {
  margin: 0;
  font-size: 0.85rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Hardcoded to match #dice-controls-overlay's own light-theme-only
     color (see its comment): a flipping token here would go near
     invisible in dark mode against the board's always-light svg. */
  color: #5a655f;
}

/* On a small board (a short window drives #board-wrap all the way down
   to MIN_BOARD_PX, 320px, in main.js), #board-middle-overlay is only
   about 199px wide, and the dice plus the Roll dice button alone fill
   nearly all of it. #recent-activity used to get squeezed to zero width
   there, with its label spilling out over the board's right hand
   property column. Below 20rem (the dice, button, gaps, padding, and
   the label's own one line width of about 6.4rem, plus a little slack)
   it is hidden instead; the same entry is still in Full Activity
   History further down the page. */
@container board-middle (width < 20rem) {
  #recent-activity {
    display: none;
  }
}

/* Near the MIN_BOARD_PX floor itself, the dice and button fit with
   almost no room to spare, so a slightly wider fallback font could
   still push the button past the edge. Tighten the button's side
   padding and the row's gap to leave some margin. */
@container board-middle (width < 14rem) {
  #dice-controls-overlay {
    gap: 0.4rem;
  }

  #dice-controls-overlay button {
    padding: 0.5rem 0.7rem;
  }
}

/* Down here the dice row sits in normal page flow on the page's own
   background (see the first max-width: 700px block above), so the
   label and #last-roll go back to the theme aware token instead of the svg matched hardcoded
   colors. #last-roll also wraps rather than truncating, since the row
   is no longer squeezed into the board's middle. The label keeps its
   one line nowrap from the base rule: it is a fixed two word heading,
   and down here it has the whole row to fit in.
   This block has to come after the base rules above, not live in that
   earlier block, or source order hands the win back to them. */
@media (max-width: 700px) {
  /* A real minimum share of the row, so once the Roll dice button grows
     (its "Waiting for 1 more player" label, say) this drops onto its own
     line instead of being squeezed to zero width beside it. */
  #recent-activity {
    flex-basis: 12rem;
  }

  #recent-activity-label {
    color: var(--ink-soft);
  }

  #last-roll {
    white-space: normal;
    color: var(--ink-soft);
  }
}

#purchase-panel,
#auction-panel,
#jail-panel,
#debt-panel {
  margin-top: 1rem;
  padding: 0.75rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
}

#purchase-panel button,
#auction-panel button,
#jail-panel button,
#debt-panel button {
  margin-right: 0.5rem;
  font-size: 1rem;
  padding: 0.4rem 0.8rem;
}

/* Both banners below keep an explicit, theme independent background and
   text color on purpose: each was already fixed once for going
   illegible in dark mode when it inherited page level color instead
   (see ROADMAP.md, phase 7), and both sit in places (mid board overlay
   context, a game ending alert) where "always legible regardless of
   theme" matters more than following the palette. */
#game-over-banner {
  padding: 0.75rem 1rem;
  background: #fdf6e3;
  color: #1c2321;
  border: 1px solid #93690a;
  font-weight: bold;
}

#spectator-banner {
  padding: 0.75rem 1rem;
  background: #ece7f5;
  color: #2f2447;
  border: 1px solid #4a3b6b;
}

/* --hand-card-width drives every size below: renderMyProperties() (main.js)
   sets it per render based on how many properties are owned, larger with
   few and shrinking smoothly as more come in (handCardWidthRem()), so a
   player with only one or two properties sees them big and readable
   right away instead of at the same small size someone with fifteen
   would need to avoid a scrollbar. Default here is the "few properties"
   ceiling, used before the first render sets it for real. */
#my-properties-list {
  --hand-card-width: 5.4rem;
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: calc(var(--hand-card-width) * 0.139);
}

.property-hand-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: var(--hand-card-width);
}

.hand-card-svg {
  width: var(--hand-card-width);
  /* Matches the 74x116 viewBox drawPropertyCard() actually draws into
     (buildHandCard(), main.js), so the card scales as one uniform unit -
     border, text, icons all together - rather than stretching. */
  aspect-ratio: 74 / 116;
}

.hand-card-caption {
  margin-top: 0.25rem;
  font-size: calc(var(--hand-card-width) * 0.139);
  text-align: center;
  color: var(--ink-soft);
}

#my-properties-list button {
  margin-top: 0.25rem;
  font-size: calc(var(--hand-card-width) * 0.139);
  padding: 0.2rem 0.4rem;
}

/* A row of player chips above the board, standing in for the old
   sidebar-style list that used to share the board's middle overlay with
   My properties - out here in normal page flow, a horizontal bar of
   chips reads better than a narrow vertical list, and leaves each chip
   room to grow into a small stat block later (balance today, more
   later) rather than staying a single line of text. */
#players-bar {
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--rule);
}

#players-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

#players-list li {
  display: flex;
  align-items: center;
  padding: 0.35rem 0.65rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
}

#players-list li.current-turn {
  font-weight: bold;
  border-color: var(--accent);
}

#players-list li.bankrupt {
  color: var(--bad);
  text-decoration: line-through;
}

.player-swatch {
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  border-radius: 50%;
  margin-right: 0.5em;
  flex-shrink: 0;
}

#trade-panel {
  min-width: 0;
}

#trade-columns {
  display: flex;
  gap: 1.5rem;
}

/* Capped like #log-list and #trade-offers-list, and lower than either:
   this panel already has the most fixed chrome of the three (heading,
   target picker, two column headings, money inputs, button), and these
   lists grow one line per property owned, often two once a long name
   wraps in the narrow columns. Uncapped, a player with a handful of
   properties made #bottom-row tall enough to squeeze the board down to
   its MIN_BOARD_PX floor, since every pixel this row takes comes out of
   #board-section's leftover height. 6.75rem is four and a half 1.5rem
   lines on purpose: a half cut off line is what tells you there is more
   to scroll to where scrollbars stay hidden until used (macOS). Not
   capped under 700px, see below. */
#trade-columns ul {
  list-style: none;
  padding: 0;
  min-height: 1.5rem;
  max-height: 6.75rem;
  overflow-y: auto;
}

/* Under 700px the board is sized by width in normal page flow and does
   not compete with this row for height, so the lists go back to their
   full length rather than a scroll box inside a scrolling page. */
@media (max-width: 700px) {
  #trade-columns ul {
    max-height: none;
    overflow-y: visible;
  }
}

#trade-columns input[type="number"] {
  width: 5rem;
}

#trade-propose-button {
  margin-top: 0.75rem;
  font-size: 1rem;
  padding: 0.4rem 0.8rem;
}

#trade-offers-panel {
  min-width: 0;
}

#trade-offers-panel h2 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Persistent while any offer is waiting on you (not just a fleeting
   toast), since the panel can sit below the fold or lose a scrolled
   list item to view - a count you can't miss beats one you might. */
#trade-offers-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.4rem;
  height: 1.4rem;
  padding: 0 0.4rem;
  border-radius: 999px;
  background: var(--warn);
  color: var(--accent-ink);
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 600;
}

#trade-offers-badge.just-arrived {
  animation: trade-offer-pulse 1.4s ease-out 2;
}

@keyframes trade-offer-pulse {
  0% { transform: scale(1); }
  30% { transform: scale(1.35); }
  60% { transform: scale(1); }
  100% { transform: scale(1); }
}

#trade-offers-list {
  list-style: none;
  padding: 0;
  margin: 0;
  max-height: 14rem;
  overflow-y: auto;
}

#trade-offers-list li {
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid var(--rule);
}

#trade-offers-list button {
  margin-top: 0.25rem;
  margin-right: 0.5rem;
  font-size: 0.8rem;
  padding: 0.25rem 0.5rem;
}

#log-panel {
  min-width: 0;
}

#log-list {
  list-style: none;
  padding: 0;
  margin: 0;
  max-height: 10rem;
  overflow-y: auto;
}

#log-list li {
  padding: 0.15rem 0;
  font-size: 0.9rem;
  color: var(--ink-soft);
}

#auction-panel p {
  margin: 0 0 0.5rem;
}

#auction-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

#auction-controls button {
  margin-right: 0;
}

#auction-amount {
  width: 6rem;
  font-size: 1rem;
  padding: 0.3rem 0.4rem;
}

/* The property on the block: a dashed ring, distinct from the solid
   owner ring, since nobody owns it yet. */
.property-card-bg.up-for-auction,
.board-space-special.up-for-auction {
  stroke: #d4a017;
  stroke-width: 6;
  stroke-dasharray: 10 5;
}
