/* Belwe Bold is the typeface Catan itself is set in, and it is what this asks
   for — but it is a licensed Bitstream/ITC face and is not on this machine, so
   it cannot be shipped. It is first in the stack anyway: install it and it takes
   over everywhere, with no other change. Behind it, self-hosted and open
   licensed, sit the two nearest things — Bree Serif for display, which has
   Belwe's warmth and weight in its slab, and Bitter for running text, which was
   drawn to stay legible at the sizes a lot of this interface uses. Self-hosted
   rather than linked, because the app is meant to work with the network off. */
@font-face {
  font-family: 'Bree Serif'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/fonts/breeserif.woff2') format('woff2');
}
@font-face {
  font-family: 'Bitter'; font-style: normal; font-weight: 400 700;
  font-display: swap; src: url('/fonts/bitter.woff2') format('woff2');
}

:root {
  --font-display: 'Belwe Bd BT', 'Belwe Bold', 'Belwe', 'Bree Serif', Georgia, serif;
  --font-body: 'Bitter', Georgia, 'Times New Roman', serif;
  --sea: #1b3a5c;
  --sea-deep: #12283f;
  --ink: #f2ece0;
  --ink-dim: #a99f8d;
  --panel: #22201d;
  --panel-2: #2c2925;
  --line: #453f37;
  --gold: #d8a44a;
  --wood: #4a6b32;
  --brick: #a04a2a;
  --sheep: #8fbb52;
  --wheat: #d9a83a;
  --ore: #6d7278;
  --desert: #c9b382;
}

* { box-sizing: border-box; }

/* The view switcher toggles the `hidden` attribute, and `hidden` is only a
   UA-stylesheet `display: none` — any author `display` below (.centered,
   #view-game) would beat it and leave two views stacked on screen. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font: 15px/1.5 var(--font-body);
  background: #150c05;
  color: var(--ink);
}

/* The table the whole app sits on. It replaced a full-screen ocean video, which
   the board floated on; now the board is a board and this is the wood under it.
   Fixed rather than per-view, so the login card, the server list and the game are
   all the same table. */
/* Two sconces on the wall behind the table, and the light they throw onto it.
   They sit outside where the cabinet lands on a wide window and disappear behind
   it on a narrow one, which is the right way round: they are the room, not the
   furniture. */
/* Two elements rather than two background layers on one. Layers are positioned
   from the left edge, so the right-hand sconce was placed with its own left edge
   at 97vw and hung off the screen; and a layer cannot be mirrored, so the pair
   were lit from the same side and did not read as a matching pair. Each is its
   own box, inset from its own edge, and the right one is flipped. */
#sconces { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
#sconces i {
  position: absolute; top: 6vh; height: 21vh; width: 16vh;
  background: url('/ui/sconce.png') center / contain no-repeat;
}
#sconces i.left { left: 2.5vw; }
#sconces i.right { right: 2.5vw; transform: scaleX(-1); }
#sconces::after {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(26vh 22vh at 5.5vw 16vh, rgba(255,186,92,0.30), transparent 70%),
    radial-gradient(26vh 22vh at 94.5vw 16vh, rgba(255,178,84,0.28), transparent 70%);
  mix-blend-mode: screen;
  animation: flicker-b 3.7s ease-in-out infinite;
}
@media (max-width: 900px), (max-height: 620px) {
  /* No room for them beside the cabinet, and behind it they are just weight. */
  #sconces { display: none; }
}

#table-bg {
  position: fixed; inset: 0; z-index: -2; pointer-events: none;
  /* Photographed oak, cropped well inside its own bevelled edge — the render
     came with rounded corners and a darker rim, and `cover` would have turned
     those into a vignette landing somewhere different on every screen. The warm
     wash on top is the candlelight from the frame reaching the table; the dark
     one underneath keeps the corners from competing with the cabinet. */
  background:
    radial-gradient(70% 55% at 50% 0%, rgba(255,186,92,0.13), transparent 72%),
    radial-gradient(120% 100% at 50% 45%, transparent 40%, rgba(0,0,0,0.42)),
    url('/ui/table.jpg') center / cover;
  background-color: #1c1108;
}

/* The boot screen. Sits over everything until the app knows where to send you,
   so a signed-in player never sees the login card flash past on the way to the
   lobby. It is markup, not script, so it is up on the very first paint. */
/* The load screen is the first thing anyone sees, and it was the one screen that
   looked like a different game: a flat blue-grey wash and a hairline bar, in front
   of an app made of painted wood, gilt frames and candlelight. It is the same room
   as the rest now — the table's own boards, lit from above, with the sign hung on
   it. */
#boot {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  background:
    /* the pool of candlelight the sign hangs in */
    radial-gradient(ellipse 70% 55% at 50% 38%, rgba(255, 196, 110, 0.16), transparent 70%),
    /* and darkness closing in at the corners, so the middle is where you look */
    radial-gradient(ellipse 90% 90% at 50% 50%, transparent 30%, rgba(0, 0, 0, 0.72) 100%),
    url('/ui/table.jpg') center / cover no-repeat,
    #17110b;
}
#boot.done { opacity: 0; pointer-events: none; transition: opacity 0.25s ease; }
.boot-card {
  display: flex; flex-direction: column; align-items: center; gap: 18px;
  padding: 26px 40px;
}

/* Sized against the viewport rather than fixed, so it fills a laptop and still
   fits a phone held sideways. The drop shadow is what sets it off the boards —
   without it the sign looks printed on the wood rather than hung above it. */
.boot-card .wordmark {
  width: min(52vw, 460px); height: auto;
  filter: drop-shadow(0 14px 26px rgba(0, 0, 0, 0.7));
  animation: boot-hang 5.5s ease-in-out infinite;
  transform-origin: 50% 4%;   /* the beam it hangs from, not its middle */
}
/* It hangs on chains, so it swings — barely. Enough to be alive, not enough to
   notice you are watching it. */
@keyframes boot-hang {
  0%, 100% { transform: rotate(-0.5deg); }
  50%      { transform: rotate(0.5deg); }
}

.boot-bar {
  width: min(46vw, 240px); height: 5px; border-radius: 3px;
  background: rgba(0, 0, 0, 0.5);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(120, 92, 48, 0.5);
  overflow: hidden;
}
/* Indeterminate on purpose: the models arrive one at a time and at their own
   pace, so a percentage would be a guess dressed up as a fact. */
.boot-bar i {
  display: block; width: 40%; height: 100%; border-radius: 3px;
  /* Molten rather than flat gold, with the light along the top edge, matching the
     gilt on the buttons and the frame. */
  background: linear-gradient(180deg, #f0cc7a, var(--gold) 55%, #8a6524);
  box-shadow: 0 0 10px rgba(216, 164, 74, 0.55);
  animation: boot-slide 1.1s ease-in-out infinite;
}
@keyframes boot-slide {
  0% { transform: translateX(-105%); }
  100% { transform: translateX(355%); }
}
#boot-status {
  margin: 0; min-height: 20px;
  font-family: var(--font-display);
  letter-spacing: 0.08em; font-size: 13px;
  color: #cdbb96; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

@media (prefers-reduced-motion: reduce) {
  .boot-bar i { animation: none; width: 100%; opacity: 0.5; }
  .boot-card .wordmark { animation: none; }
}

button {
  /* inline-flex, not the default block-ish button box. The label used to sit on
     the text baseline inside a box whose height comes from a painted border, so
     it floated high or low depending on the font's metrics — and changing the
     font changed the misalignment everywhere at once. Centring on both axes does
     not care what the metrics are. */
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1.1;
  font: inherit;
  padding: 7px 13px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--gold);
  color: #2a1c06;
  font-weight: 600;
  cursor: pointer;
}
button:hover:not(:disabled) { filter: brightness(1.08); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
button.ghost { background: transparent; color: var(--ink); font-weight: 500; }
button.small { padding: 3px 8px; font-size: 13px; }

input, select {
  font: inherit;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #17150f;
  color: var(--ink);
}

/* Display face on everything that names or labels something; running text keeps
   the body face, which is the more legible of the two when it gets small. */
h1, h2, h3, .brand, button, .badge, .session-row .name, .sign, .hand-label,
.me-plaque .who, .rival .who, .pip, #banner, .cost-row .what, .tabs button,
.dev-card, .boot-card .brand {
  font-family: var(--font-display);
}
h1, h2, h3 { margin: 0 0 10px; font-weight: 400; letter-spacing: 0.02em; }
h3 { font-size: 13px; text-transform: uppercase; color: var(--ink-dim); }
.muted { color: var(--ink-dim); font-size: 13px; }
.error { color: #e8735e; min-height: 20px; font-size: 13px; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.spacer { flex: 1; }

#topbar {
  /* Wrap, and let the labels shrink. The painted buttons are wider than the flat
     ones they replaced, and unwrapped they pushed the page out to 522px on a
     375px phone — which expands the layout viewport, so the browser zooms the
     whole document out and every screen is suddenly too small. A row that cannot
     wrap is a page-wide problem, not a topbar one. */
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  min-width: 0;
  padding: 10px 16px; background: var(--panel);
  border-bottom: 1px solid var(--line);
}
.brand { font-weight: 800; letter-spacing: 0.16em; color: var(--gold); }
/* The brand is the painted sign now, not lettering. Height-driven so it keeps its
   own proportions whatever the bar does, and `width: auto` so it is never squashed
   by a flex parent deciding how wide it should be. */
img.brand { height: 30px; width: auto; display: block; flex: none; }
/* On the login card the sign is the heading, so it takes the width and sets its
   own height — the same sign, doing the job the h1 used to. */
/* The card's id is view-auth. A rule written against #view-login matched nothing
   and the sign came through at its natural 760px, straight out through both sides
   of the card. */
#view-auth .wordmark { width: min(82%, 300px); height: auto; display: block;
  margin: 0 auto 12px; filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.6)); }
#session-name { color: var(--ink-dim); min-width: 0; overflow: hidden;
                text-overflow: ellipsis; white-space: nowrap; }
/* Belt and braces: nothing in this app is meant to scroll sideways, and a single
   overflowing child silently rescales every other screen. */
html, body { overflow-x: hidden; }

.centered { display: grid; place-items: center; min-height: 100vh; }
.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px; padding: 18px; margin: 16px;
  display: flex; flex-direction: column; gap: 10px;
}
#view-lobby { max-width: 780px; margin: 0 auto; }
#auth-form { width: min(360px, 92vw); }

.list { display: flex; flex-direction: column; gap: 8px; }
.session-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel-2);
}
/* Fixed columns for the name and the status, so a list of a dozen tables scans
   down the page instead of every row starting somewhere new. */
.session-row .name { font-weight: 600; min-width: 132px; }
.badge {
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1;
  font-size: 11px; padding: 2px 7px; border-radius: 99px;
  background: #3a352d; color: var(--ink-dim); text-transform: uppercase;
  white-space: nowrap; flex: none;
}
.badge.yours { background: var(--gold); color: #2a1c06; }

/* ==== the game, in the cabinet ===============================================
   Carried over from public/mockup.html. The cabinet is drawn at a fixed
   1180x830 and scaled to fit by fitCabinet(); everything inside is positioned
   against that fixed size, which is why it can be absolute and in pixels. */

#view-game { height: calc(100vh - 45px); }
.stage { position: relative; width: 100%; height: 100%; overflow: hidden; }
/* Centred by hand rather than by a grid: a grid asked to centre an item wider
   than itself pins it to the start instead, which only misbehaves once the
   window is narrower than the design size — exactly the case this is for. */
.cabinet-fit {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(var(--fit, 1));
}
.cabinet {
  position: relative; width: 1180px; height: 830px;
  border-style: solid; border-width: 48px 52px 47px 54px;
  border-image: url('/ui/frame.png') 138 149 136 156 stretch;
  filter: drop-shadow(0 24px 60px rgba(0,0,0,0.75));
}
/* The frame has no candles of its own now — they are sconces on the wall behind
   it, so the light they throw belongs to the room rather than to the cabinet.
   What is left here is the warmth that reaches the board itself. */
.cabinet::before {
  content: ''; position: absolute; inset: -20px; pointer-events: none; z-index: 7;
  mix-blend-mode: screen;
  background:
    radial-gradient(30% 34% at 4% 0%, rgba(255,186,92,0.26), transparent 72%),
    radial-gradient(30% 34% at 96% 0%, rgba(255,178,84,0.24), transparent 72%);
  animation: flicker-a 5.3s ease-in-out infinite;
}
@keyframes flicker-a {
  0%, 100% { opacity: 0.88; } 18% { opacity: 1; } 37% { opacity: 0.8; }
  56% { opacity: 0.97; } 74% { opacity: 0.85; }
}
@keyframes flicker-b {
  0%, 100% { opacity: 1; } 22% { opacity: 0.82; } 48% { opacity: 0.95; } 71% { opacity: 0.86; }
}
@media (prefers-reduced-motion: reduce) {
  .cabinet::before, .cabinet::after { animation: none; }
}

/* The cloth on the table, under the board. */
.felt {
  position: relative; height: 100%; border-radius: 8px; overflow: hidden;
  background: url('/ui/felt.jpg') center / cover;
  box-shadow: inset 0 0 0 2px var(--gilt-dim, #8a6a2c), inset 0 0 70px rgba(0,0,0,0.75);
}

.sign {
  position: absolute; z-index: 5; top: 2px; left: 50%; transform: translateX(-50%);
  width: 252px; aspect-ratio: 900 / 444;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: url('/ui/banner.png') center / 100% 100% no-repeat;
  pointer-events: none;
}
/* The sign carries both things the board cannot tell you: which of your tables
   this is, and whose move it is. It used to say CATAN — which you know — and the
   turn was a floating pill over the middle of the island, sitting on the one part
   of the screen you are trying to look at. The pill is gone; #banner is in here.
   Table above, quietly; turn below, in the big letters. */
/* 64%, not 82%: the plank's painted panel is about two thirds of the sign and the
   rest is carved frame. Text sized to the whole sign runs out over the carving. */
.sign #banner {
  max-width: 64%; font-size: 11.5px; line-height: 1.12; letter-spacing: 0.01em;
  color: #3a2410; text-shadow: 0 1px 0 rgba(255,225,175,0.4);
  text-align: center; margin-top: 14px;
  /* Two lines and then an ellipsis: "Your move — place your settlement" is the
     longest thing this says, and a plank is only so wide. */
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}

/* Your own seat, top left. */
#me-seat { position: absolute; z-index: 6; top: 10px; left: 10px; }
/* The plaque art is a frame with a genuinely transparent middle, so `fill`
   painted nothing there and the table showed straight through it. The wood is a
   crop from the middle of panel.png — the same board the other frames are cut
   from, so the plaque matches them rather than approximating them. */
.me-plaque {
  display: flex; align-items: center; gap: 8px;
  background: url('/ui/wood.jpg') center / 220px auto;
  border-style: solid; border-width: 20px 22px;
  border-image: url('/ui/plaque.png') 70 74 75 73 fill stretch;
  filter: drop-shadow(0 6px 16px rgba(0,0,0,0.6));
}
.me-plaque.active { filter: drop-shadow(0 6px 16px rgba(0,0,0,0.6))
                            drop-shadow(0 0 14px rgba(216,164,74,0.45)); }
.seat-side { min-width: 0; padding-right: 6px; }
.me-plaque .who { font-weight: 700; font-size: 14px; white-space: nowrap; line-height: 1.15; }
.turn-note {
  font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--gold); line-height: 1.2; margin-bottom: 1px;
}
/* The frame is a whole picture, not a border: scaled to the box rather than
   9-sliced, so its carved corners keep their proportions instead of being
   squashed into a 13px border. The portrait sits in a child inset to the
   opening. */
.face {
  position: relative; width: 68px; height: 68px; flex: none; display: block;
  background: url('/ui/portrait-frame.png') center / 100% 100% no-repeat;
}
.face i {
  position: absolute; inset: 15.5%;
  background-size: cover; background-position: center top;
  border-radius: 1px;
  /* The ring is an inset shadow rather than a border so it does not change the
     tile's size and knock the portrait out of the opening. */
}
.stats { display: flex; gap: 4px; margin-top: 4px; }
/* Painted shields, with the number on the shield and the word under it.
   A shield is a SHAPE — nine-slicing one stretches its middle until it is a
   rounded rectangle, which is what these were before.

   background-size is 100% 100% rather than contain, deliberately. The three
   shields came out of the generator at slightly different aspects (0.838, 0.796,
   0.856), and `contain` letterboxes each one differently inside the same box, so
   they sat at three different heights in a row. Forcing them to one box costs
   about four percent of distortion, which nothing can see, and buys an alignment
   that everything can. */
.pip-wrap { display: flex; flex-direction: column; align-items: center; gap: 1px; }
.pip {
  display: grid; place-items: center;
  width: 40px; height: 46px; flex: none;
  background: url('/ui/pip-cards.png') center / 100% 100% no-repeat;
}
.pip--vp { background-image: url('/ui/pip-vp.png'); }
.pip--army { background-image: url('/ui/pip-army.png'); }
/* Interim. There is no pip-road.png yet, so this borrows the default shield and
   shifts it toward the road's own colour — pointing at a file that is not there
   would drop the shield entirely and leave a floating number. Replace this rule
   with a background-image the moment the art exists; the prompt for it is in
   ASSET-PROMPTS.md. */
.pip--road { filter: hue-rotate(-28deg) saturate(1.25); }
.pip b { font-size: 16px; line-height: 1; text-shadow: 0 1px 3px #000, 0 0 6px rgba(0,0,0,0.6); }
.pip-label {
  font-size: 8px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-dim); line-height: 1;
}
.stats { align-items: flex-start; }

/* Both sides are columns, not separately-placed boxes: stacking in flow is the
   thing that cannot collide however many players are at the table. */
.rail {
  position: absolute; z-index: 4; top: 12px; bottom: 12px;
  display: flex; flex-direction: column; gap: 8px;
}
.rail.left { left: 10px; width: 186px; padding-top: 140px; }
.rail.right { right: 10px; width: 254px; }
.rail .grow { flex: 1; min-height: 0; }

#players { display: flex; flex-direction: column; gap: 5px; flex: none; }
.rival {
  display: flex; align-items: center; gap: 7px;
  background: url('/ui/wood.jpg') center / 200px auto;
  border-style: solid; border-width: 13px 16px;
  border-image: url('/ui/plaque.png') 70 74 75 73 fill stretch;
}
.rival .face { width: 44px; height: 44px; }
.rival .who { flex: 1; min-width: 0; font-size: 12.5px; font-weight: 650;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rival.turn { filter: drop-shadow(0 0 12px rgba(216,164,74,0.45)); }
.mini { display: flex; gap: 4px; flex: none; }
.mini i { font-style: normal; font-size: 11px; font-weight: 700; padding: 1px 6px;
          border-radius: 5px; border: 1px solid rgba(0,0,0,0.5); }
.mini i.vp { background: linear-gradient(#6c3f9e, #3d2160); }
.mini i.cards { background: linear-gradient(#1f5f96, #123a5e); }
.mini i.road { background: linear-gradient(#2f6f4a, #143a26); }
.mini i.army { background: linear-gradient(#8a3a2a, #4d1e14); }

#view-game .panel {
  position: relative; z-index: 4;
  border-style: solid; border-width: 17px;
  border-image: url('/ui/panel.png') 58 fill stretch;
  filter: drop-shadow(0 8px 20px rgba(0,0,0,0.55));
}
#view-game .panel h3 { margin: 0 0 4px; padding: 0 2px 5px; font-size: 10.5px;
                       letter-spacing: 0.16em; color: var(--gold);
                       border-bottom: 1px solid rgba(216,164,74,0.28); }

.talk { flex: 1; min-height: 150px; display: flex; flex-direction: column; }
.tabs { display: flex; flex: none; border-bottom: 1px solid rgba(216,164,74,0.28); }
.tabs button {
  flex: 1; border: none; background: none; border-image: none; min-height: 0;
  font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase;
  padding: 4px 0 6px; color: var(--ink-dim); text-shadow: none;
}
.tabs button.on { color: var(--gold); background: rgba(216,164,74,0.1);
                  box-shadow: inset 0 -2px 0 var(--gold); }
/* Only a phone gets a way to close the feed, because only a phone opens it over
   everything. */
#talk-close { display: none; }
.talk .feed { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 2px;
              font-size: 12px; max-height: none; }
.talk .row { flex: none; padding: 6px 2px 0; border-top: 1px solid rgba(216,164,74,0.2); }
.talk input { flex: 1; min-width: 0; border-width: 8px 10px; font-size: 12px; }
.talk button.small { border-width: 0 14px; min-height: 26px; font-size: 11px; }

/* The rail is a fixed height and the talk panel wants all of it, so with enough
   actions the column ran past the bottom of the felt — which clips — and End
   turn, always last, was the one you could not reach. The actions get a hard
   share of the rail and scroll inside it if they exceed it; the feed gives way
   first, because a short chat is a nuisance and an unreachable End turn is a
   stuck game. */
#view-game .actions {
  flex: 0 1 auto; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 5px;
  max-height: 58%; overflow-y: auto; overscroll-behavior: contain;
}
#view-game .actions button { flex: none; width: 100%; }
.rail.right > .talk { flex: 1 1 0; min-height: 84px; }

#board-wrap {
  position: absolute; inset: 96px 276px 128px 200px;
  overflow: hidden; touch-action: none; overscroll-behavior: contain;
  /* Zoomed in, the map runs past this box and was being cut by it — a hard
     rectangular edge straight across the water, which reads as a photograph
     trimmed with scissors rather than as a board you are leaning over. The mask
     feathers the last few percent on every side so the sea passes under the
     cloth instead of stopping dead on it. Both properties: -webkit- for Safari,
     which is most of the phones this is played on. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 3.5%, #000 96.5%, transparent 100%),
                      linear-gradient(to bottom, transparent 0, #000 3.5%, #000 96.5%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(to right, transparent 0, #000 3.5%, #000 96.5%, transparent 100%),
              linear-gradient(to bottom, transparent 0, #000 3.5%, #000 96.5%, transparent 100%);
  mask-composite: intersect;
}
#terrain { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
#board { position: relative; z-index: 1; width: 100%; height: 100%; display: block; touch-action: none; }

.hand-label { font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase;
              color: var(--ink-dim); }

/* Hand: one card per resource held, overlapping into a stack you can fan out.
   It sits on the table now rather than in a sidebar, so it is pinned to the
   bottom of the cabinet and centred. */
/* The bottom strip: resources on the left of it, development cards on the right,
   laid out together so they cannot collide however many of either you hold. */
.tableau {
  position: absolute; z-index: 6; bottom: 8px; left: 200px; right: 276px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.tableau-row {
  display: flex; flex-wrap: nowrap; align-items: flex-end; justify-content: center;
  gap: 18px; max-width: 100%; min-width: 0;
}
#view-game #hand { flex-wrap: nowrap; gap: 9px; padding: 0; }

/* Neither tray may wrap. `.cards` sets flex-wrap: wrap, which is fine for the
   three or four cards this was built for and comes apart at twelve: the spent
   ones wrapped into a grid that grew upward across the board, took the playable
   ones with it, and left nothing you could reliably point at. A hand is a row.
   The playable tray never shrinks — those are the ones you have to hit. */
#view-game #dev-cards { display: flex; flex-wrap: nowrap; gap: 4px; flex: none; }

/* Spent cards overlap into a pile. They are a record, not a choice, so they can
   cost a fifth of the width: twelve of them take about the room of three. The
   pile fans out on hover if you want to count them. */
#view-game #dev-spent { display: flex; flex-wrap: nowrap; flex: 0 1 auto; min-width: 0; }
#view-game #dev-spent .dev-card { flex: none; transition: margin-left 0.14s ease; }
#view-game #dev-spent .dev-card + .dev-card { margin-left: -42px; }
#view-game #dev-spent:hover .dev-card + .dev-card { margin-left: -12px; }
#view-game .stack .count { width: 18px; height: 18px; font-size: 10px; }
#view-game .res-card { width: 40px; height: 52px; }
#view-game .stack { padding: 6px 0 0 6px; }
#view-game .dev-card { width: 56px; height: 73px; font-size: 8.5px; }
.hand { display: flex; gap: 16px; flex-wrap: wrap; padding-bottom: 4px; }
.stack { position: relative; display: flex; padding: 7px 0 0 7px; }
.res-card {
  /* Sized to the card art's own 0.77 aspect, so the painted frame is not sliced
     down its sides by background-size: cover. */
  width: 34px; height: 44px; flex: none;
  border-radius: 4px; border: 1px solid rgba(0,0,0,0.55);
  background-size: cover; background-position: center;
  box-shadow: 0 1px 3px rgba(0,0,0,0.5);
  transition: margin-left 0.14s ease;
}
.stack .res-card + .res-card { margin-left: -25px; }
.stack:hover .res-card + .res-card { margin-left: -9px; } /* fan out on hover */
.stack.empty .res-card { opacity: 0.25; filter: grayscale(1); }
/* A count on the corner of the card, not a caption under it. */
.stack .count {
  position: absolute; top: -7px; left: -7px; z-index: 2;
  width: 21px; height: 21px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 11.5px; font-weight: 800; line-height: 1;
  background: linear-gradient(#3f86c8, #1b4a78); color: #fff;
  border: 2px solid var(--gold); box-shadow: 0 2px 6px rgba(0,0,0,0.6);
}
.stack.empty .count { background: linear-gradient(#4a4a4a, #2a2a2a); border-color: #6a6a6a; }

/* The blown-up card under the pointer. Sized off the viewport height so it is
   large on a desktop and still fits on a laptop, and kept to the art's own
   aspect so the painted frame is never sliced. */
#card-preview {
  position: fixed; z-index: 40; margin: 0;
  height: min(58vh, 460px); aspect-ratio: 338 / 440;
  border-radius: 10px; border: 1px solid #6b5433;
  background-size: cover; background-position: center; background-color: #16130f;
  box-shadow: 0 18px 44px rgba(0,0,0,0.72);
  opacity: 0; transform: scale(0.94); transform-origin: right center;
  transition: opacity 0.12s ease, transform 0.12s ease;
  pointer-events: none; /* it sits over the board; it must never eat a click */
}
#card-preview.shown { opacity: 1; transform: scale(1); }
/* Several faces at once — what a 3:1 harbour will take. Three across, then two
   centred underneath, which is what flex-wrap does with five of them. */
#card-preview .faces {
  position: absolute; inset: 0;
  display: flex; flex-wrap: wrap; gap: 7px;
  align-content: center; justify-content: center;
  padding: 14px 14px 46px;
}
#card-preview .face {
  width: 28%; aspect-ratio: 338 / 440;
  background-size: cover; background-position: center;
  border-radius: 4px; border: 1px solid rgba(0,0,0,0.55);
  box-shadow: 0 2px 6px rgba(0,0,0,0.5);
}
/* A harbour says what it trades and on what terms; a hand card is its own label
   and shows nothing here. */
#card-preview figcaption { display: none; }
#card-preview.captioned figcaption {
  display: block;
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 12px 10px 13px;
  background: linear-gradient(transparent, rgba(8,6,4,0.9) 42%);
  border-radius: 0 0 9px 9px;
  color: var(--gold); font-size: 19px; font-weight: 800; letter-spacing: 0.04em;
  text-align: center; text-transform: uppercase; text-shadow: 0 2px 6px #000;
}

/* Dice */
#dice {
  /* Bottom-left corner, over the foot of the left rail — which is empty space,
     since the rail's panels stack from the top. */
  position: absolute; bottom: 14px; left: 14px; z-index: 6;
  display: flex; align-items: center; gap: 9px;
  background: rgba(0,0,0,0.55); padding: 8px 12px; border-radius: 12px;
  border: 1px solid var(--line);
  /* A readout, not a control — and it sits right over the southern coastline, so
     without this it swallows the clicks on the two settlement corners beneath it.
     Same reason #banner has it. */
  pointer-events: none;
  z-index: 2;
}
#dice .total { font-size: 19px; font-weight: 800; color: var(--gold); min-width: 22px; text-align: center; }
#dice .who { font-size: 12px; color: var(--ink-dim); max-width: 130px; }
/* The pip size follows the die rather than being its own fixed number. It used
   to be 8px flat, so shrinking the die to 22px on a phone left a 3x3 grid of 8px
   pips inside 12px of usable space — they overlapped into a blot, which is what
   made the dice look wonky. One variable, and every size stays a die. */
.die {
  --die: 42px;
  --pip: calc(var(--die) * 0.19);
  width: var(--die); height: var(--die);
  background: #f2ece0; border-radius: calc(var(--die) * 0.19);
  border: 1px solid #b9ad93; padding: calc(var(--die) * 0.12);
  display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr);
}
.die .pip {
  width: var(--pip); height: var(--pip); border-radius: 50%;
  background: #1d1a15; align-self: center; justify-self: center;
}
#dice.rolling .die { animation: tumble 0.28s linear infinite; }
#dice.rolling .total { opacity: 0.25; }
@keyframes tumble {
  0%   { transform: rotate(0deg)   translateY(0); }
  25%  { transform: rotate(-13deg) translateY(-6px); }
  50%  { transform: rotate(0deg)   translateY(0); }
  75%  { transform: rotate(13deg)  translateY(-6px); }
  100% { transform: rotate(0deg)   translateY(0); }
}
#dice.settled .die { animation: land 0.3s ease-out; }
@keyframes land {
  0% { transform: scale(1.22); }
  60% { transform: scale(0.94); }
  100% { transform: scale(1); }
}

/* Build cost reference */
.cost-row {
  display: flex; align-items: center; gap: 7px;
  padding: 4px 0; border-bottom: 1px solid #2e2a25; font-size: 13px;
}
.cost-row:last-child { border-bottom: none; }
.cost-row .what { width: 78px; flex: none; }
.cost-row .pieces { display: flex; gap: 3px; flex-wrap: wrap; }
.cost-chip {
  width: 19px; height: 19px;
  background-size: contain; background-position: center; background-repeat: no-repeat;
}
.cost-row.affordable .what { color: var(--gold); font-weight: 700; }
.cost-row:not(.affordable) { opacity: 0.5; }

/* Counting cards out of a hand you can see. The prompt sits over the board and
   the tableau, so when a seven is rolled this is the only place your hand is
   still visible — it has to read as cards, not as a form. */
.counter { flex-wrap: wrap; gap: 8px; }
.counter-res {
  align-items: center;
  padding: 3px 5px;
  border-radius: 7px;
  background: rgba(0, 0, 0, 0.22);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);
}
/* What you have picked so far, so a glance over the row shows the selection
   without reading five separate numbers. */
.counter-res.picked { background: rgba(212, 168, 83, 0.22); box-shadow: inset 0 0 0 1px var(--gold); }
/* Greyed rather than hidden: the row keeps its shape between hands, and the gap
   itself tells you what you are out of. */
.counter-res.none { opacity: 0.35; }
.counter-held { font-size: 12px; color: var(--ink-dim); min-width: 16px; }

.cards { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 9px; }
.dev-card {
  width: 68px; height: 88px; border-radius: 7px; border: 1px solid var(--line);
  background-size: cover; background-position: center; background-color: #35302a;
  cursor: pointer; font-size: 0;   /* the card is the label */
}
.dev-card:disabled { cursor: not-allowed; filter: grayscale(0.7) brightness(0.65); }
/* A card you have already played. It stays — a spent knight is what your army is
   counted from — but it is not a choice, so it lives in its own tray and reads as
   spent rather than as merely not-playable-yet. */
.dev-card.spent {
  filter: grayscale(1) brightness(0.5) contrast(0.9);
  border-color: #2e2a25; cursor: default;
}
/* Set apart from the playable ones by a rule and a little distance, so the two
   trays do not read as one row of cards half of which are broken. */
.spent-tray {
  padding-left: 10px; margin-left: 2px;
  border-left: 1px solid rgba(216,164,74,0.25);
  opacity: 0.85;
}

.actions { display: flex; gap: 7px; flex-wrap: wrap; }
.actions button.armed { outline: 2px solid var(--ink); }
/* A bot's argument for a trade it is offering you. */
.pitch { width: 100%; margin: 6px 0 0; font-size: 13px; font-style: italic; color: var(--gold); }

.log { max-height: 210px; overflow-y: auto; font-size: 12.5px; color: var(--ink-dim); }
.log div { padding: 2px 0; border-bottom: 1px solid #2e2a25; }

.chat { max-height: 190px; overflow-y: auto; font-size: 13px; margin-bottom: 7px; }
.chat div { padding: 3px 0; }
.chat .who { font-weight: 700; }
.chat .mine .who { color: var(--gold); }
.chat .bot .who { color: #8fbfe8; }
.portrait {
  width: 26px; height: 26px; border-radius: 50%; object-fit: cover;
  border: 2px solid var(--line); flex: none; background: var(--panel-2);
}
.portrait.tiny { width: 17px; height: 17px; border-width: 1px; vertical-align: -4px; margin-right: 4px; }
#chat-form { gap: 6px; }
#chat-input { flex: 1; min-width: 0; }

/* Modals. Without this the backdrop is an ordinary div at the end of the body,
   so a discard prompt renders below the board and has to be scrolled to — which
   is exactly what it looked like before this existed. */
#modal-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(0, 0, 0, 0.62);
  display: grid; place-items: center; padding: 20px;
  backdrop-filter: blur(2px);
}
#modal {
  margin: 0; width: min(460px, 92vw); max-height: 86vh; overflow-y: auto;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
  border-color: var(--gold);
}
#modal h2 { color: var(--gold); }
.modal-pitch {
  margin: 0; padding: 9px 11px; border-radius: 7px;
  background: var(--panel-2); border-left: 3px solid var(--gold);
  font-style: italic; font-size: 14px;
}
/* What a bot said about its own move. */
.log div.thought { color: var(--gold); font-style: italic; }

/* Board pieces */
.tile-num { fill: #1d1a15; font-size: 17px; font-weight: 700; text-anchor: middle; }
.tile-num-bg { fill: #efe4c8; stroke: #6b5c3a; }
.tile-num.red { fill: #a8331f; }
/* Thinner than it was: the bevel rims below now separate the tiles, and a heavy
   black outline on top of them just reads as a cartoon border. */
.tile-outline { fill: none; stroke: rgba(0,0,0,0.30); stroke-width: 1; }
.tile-rim-lit { fill: none; stroke: #fff; stroke-opacity: 0.38; stroke-width: 3.5; stroke-linejoin: round; }
.tile-rim-shade { fill: none; stroke: #000; stroke-opacity: 0.42; stroke-width: 3.5; stroke-linejoin: round; }
.port { fill: #efe4c8; stroke: #5c4a28; font-size: 11px; font-weight: 700; text-anchor: middle; }
/* Deliberately sets NO fill: the resource pattern arrives as a fill attribute,
   and any fill here would win over it and blank the photo out. */
.port-disc { stroke: #5c4a28; stroke-width: 1.5; }
.port-any { font-size: 12px; }
.port-ribbon { fill: rgba(12,10,7,0.82); stroke: #5c4a28; stroke-width: 1; }
.port-rate { fill: #f4ead2; stroke: none; font-size: 10px; font-weight: 700; text-anchor: middle; }
/* Somewhere to point at a harbour. Invisible, and the ONLY thing on the board
   outside the build targets that takes a pointer — see the pointer-events rule
   below for why everything else is scenery. It sits far enough out to sea that
   it cannot cover a corner you might build on: the token floats a good forty
   units past the coast and its own radius is under twenty. */
.port-hit { fill: rgba(255,255,255,0); cursor: help; }
.robber { fill: #14110d; stroke: #efe4c8; stroke-width: 1.5; }
/* Only the targets are clickable; everything else on the board is scenery. Once
   pieces stand at their terrain's height they overlap each other from a low
   angle, and a harbour disc or the robber sitting over a legal corner was
   quietly eating the click. */
#board .port, #board .port-disc, #board .port-ribbon, #board .port-rate,
#board .robber,
#board .building, #board .road, #board .road-spread, #board .road-bed,
#board .tile-outline,
#board .tile-num, #board .tile-num-bg { pointer-events: none; }

/* The jetties marking which corners can reach a harbour. Never a click target:
   they lie across the very spots you build on, and a settlement has to stay
   selectable through them. */
#board .dock { pointer-events: none; }
#board .dock-deck {
  fill: #6a4a2c; stroke: #2b1c0f; stroke-width: 0.9;
  /* Quiet on purpose. Eighteen of these ring a coast that already carries nine
     harbour tokens; they are a hint about a spot, not scenery competing with it. */
  opacity: 0.82;
}
#board .dock-plank { stroke: #46301a; stroke-width: 1.5; stroke-linecap: round; opacity: 0.85; }

/* The robber's smoke.
   Never a click target: it lies over the tile he is on, and that tile still has
   to be selectable to move him off it. */
#board .robber-aura { pointer-events: none; }

/* The wisps are turned by animateTransform in the markup, not from here — the
   pivot has to be the robber's feet, and SVG cannot say that in CSS without
   either pivoting about each ellipse's own middle or about the whole island.
   See robberAura().

   A slow breath on the pool underneath, so it reads as smoke rather than as a
   turning wheel. This one is safe in CSS: the circle is centred on the aura's
   origin already, so its own box centre is the right pivot. */
#board .robber-aura .smoke-core {
  transform-box: fill-box;
  transform-origin: center;
  animation: smoke-breathe 7s ease-in-out infinite;
}
@keyframes smoke-breathe {
  0%, 100% { transform: scale(0.92); opacity: 0.85; }
  50%      { transform: scale(1.08); opacity: 1; }
}

/* Motion is the whole mechanism here, so when it is unwelcome the aura has to
   stay legible standing still — it keeps the smoke, and loses the drift. The
   wisps handle this themselves, in robberAura. */
@media (prefers-reduced-motion: reduce) {
  #board .robber-aura .smoke-core { animation: none; }
}

.target { cursor: pointer; }
.target-vertex { fill: rgba(255,255,255,0.34); stroke: #fff; stroke-width: 2; }
.target-vertex:hover { fill: #fff; }
.target-edge { stroke: rgba(255,255,255,0.4); stroke-width: 9; stroke-linecap: round; }
.target-edge:hover { stroke: #fff; }
.target-tile { fill: rgba(255,255,255,0.001); }
.target-tile:hover { fill: rgba(255,255,255,0.22); }
/* A road is paint on the ground, not a bar laid across it: squared ends so two
   roads meeting at a corner join rather than overlap into a bead, and a little
   translucency so the terrain reads through the colour.

   Not `multiply`, which was the first thing tried. It looks right on a red or
   blue road and erases a white one completely, since multiplying by white is the
   identity — and whose road it is has to survive the effect. */
.road, .road-spread, .road-bed { stroke-linecap: butt; }
/* These three numbers were picked by putting the variants on the board and
   looking. Fainter than this and a white road on a pale stone rim stops telling
   you whose it is, which the paint has to do above all else; bolder, and it goes
   back to being a bar lying on top of the picture. */
.road { stroke-width: 6.5; stroke-opacity: 0.76; }
/* The colour again, wider and thin, so the edge of the paint fades into the
   ground rather than stopping dead. */
.road-spread { stroke-width: 11; stroke-opacity: 0.26; }
/* A dark line under the paint. It seats the colour into the ground, and it is
   what keeps a pale road legible against pasture and sand — these tiles have
   their own dirt tracks, and a road has to read as more deliberate than those. */
.road-bed { stroke-width: 13.5; stroke: rgba(10, 8, 6, 0.36); }
.building { stroke: #14110d; stroke-width: 2; }

/* Zoom controls */
/* The container spans more board than the three buttons do, so it must not take
   clicks — only the buttons themselves may. On a narrow window the board scales
   down until the north-east coastline passes under here, and a transparent box
   eating those clicks is invisible to diagnose. */
/* Round, and not the painted pill. The pill is nine-sliced with 18px of cap at
   each end, so on a 44px square button the caps ARE the button — the label had
   8px to live in and the two seams read as the weird edges around it.
   
   The disc is drawn on a pseudo-element inset from the button, so what you press
   stays 44px on a touch screen while what you SEE is 30. That distinction is the
   whole trick here: the note below about the 44px floor is right — this cluster
   sits over the board, and a tap that misses it places a road you did not mean
   to place — but 44px of visible button over the island is too much furniture. */
#zoom-controls button {
  position: relative; z-index: 0;
  border: none; border-image: none; background: none; box-shadow: none;
  color: var(--ink); text-shadow: 0 1px 3px #000;
  padding: 0; width: 34px; height: 34px; min-height: 0;
  font-size: 15px; line-height: 1; filter: none;
}
#zoom-controls button::before {
  content: ''; position: absolute; z-index: -1;
  top: 50%; left: 50%; width: 30px; height: 30px; transform: translate(-50%, -50%);
  border-radius: 50%; border: 1px solid var(--gilt-dim, #8a6a2c);
  background: rgba(24, 14, 6, 0.82);
}
#zoom-controls button:hover:not(:disabled) { filter: none; }
#zoom-controls button:hover:not(:disabled)::before { background: rgba(52, 33, 13, 0.92); }
#btn-zoom-reset { font-size: 11px; letter-spacing: 0.04em; }

#zoom-controls {
  position: absolute; top: 12px; right: 12px;
  display: flex; flex-direction: column; gap: 5px;
  pointer-events: none;
  /* Above the board. #board took a z-index of its own to sit over the terrain
     canvas, which put it over these too — they still drew, because the SVG is
     transparent, but every click landed on the board behind them. */
  z-index: 2;
}
#zoom-controls button {
  width: 30px; height: 30px; padding: 0;
  background: rgba(0,0,0,0.55); border-color: var(--line);
  font-size: 15px; line-height: 1;
  pointer-events: auto;
}
#zoom-controls button:disabled { opacity: 0.35; }
#board.zoomed { cursor: grab; }
#board.panning { cursor: grabbing; }

/* Gains. Only transform and opacity move, so none of this reflows the panel. */
.stack.gained .res-card { animation: gain-pop 0.5s ease-out; }
@keyframes gain-pop {
  0%   { transform: translateY(0) scale(1); }
  35%  { transform: translateY(-7px) scale(1.1); }
  100% { transform: translateY(0) scale(1); }
}
.gain-badge {
  position: absolute; top: -14px; left: 50%;
  font-size: 12px; font-weight: 800; color: #8fbb52;
  text-shadow: 0 1px 3px #000; pointer-events: none; white-space: nowrap;
  animation: gain-badge-float 1.4s ease-out forwards;
}
@keyframes gain-badge-float {
  0%   { transform: translate(-50%, 8px);   opacity: 0; }
  20%  { transform: translate(-50%, 0);     opacity: 1; }
  100% { transform: translate(-50%, -18px); opacity: 0; }
}
.gain-chip { animation: gain-chip-float 1.5s ease-out forwards; pointer-events: none; }
@keyframes gain-chip-float {
  0%   { transform: translateY(8px);   opacity: 0; }
  18%  { transform: translateY(0);     opacity: 1; }
  100% { transform: translateY(-30px); opacity: 0; }
}

/* An offer you owe an answer to, at the top of the panel where a modal used to
   cover the board you were being asked about. */
#trade-offer {
  border: 1px solid var(--gold); border-radius: 8px;
  padding: 11px; background: var(--panel-2);
}
#trade-offer h3 { color: var(--gold); }
#trade-offer .offer-line { margin: 0 0 8px; font-size: 14px; }

/* Server list */
.session-row .when { color: var(--ink-dim); font-size: 12px; white-space: nowrap; }
.session-row.seated { border-color: #5c6f4a; }
.session-row.finished { opacity: 0.6; }
button.danger { background: #7d2f22; color: var(--ink); }
#server-count { font-size: 13px; }

@media (prefers-reduced-motion: reduce) {
  .stack.gained .res-card, .gain-badge, .gain-chip { animation: none; }
  .gain-badge, .gain-chip { opacity: 1; }
}

@media (max-width: 820px) {
  #view-game { grid-template-columns: 1fr; height: auto; }
  #panel { border-left: none; border-top: 1px solid var(--line); }

  /* The island is about as tall as it is wide, so a flat 58vh box left a band
     of dead water under it on a phone — 146px of it at 375x812. Sizing off the
     WIDTH instead means the box is the shape of the thing inside it, and the
     reclaimed height goes to the panel where the hand and the actions live.
     58vh still caps it on a short landscape screen. */
  /* The island runs about 1.09 tall for every 1 wide, so that plus the banner
     strip is the height at which it exactly fills the screen width — any less
     and the fit becomes height-bound and the island shrinks below what it had
     before. The 64vh cap keeps it sane in landscape, where vw is the large
     dimension and the formula would otherwise ask for an absurd box. */
  #board-wrap { height: min(64vh, calc(109vw + 44px)); display: flex; flex-direction: column; }

  /* The banner floats over the sea on a wide screen, which is where it should
     be. Once the island is fitted to the box there is no spare sea left for it
     to float over, and it started covering the northern harbour. Reserving
     enough water to clear it would cost more board than the fit just won back
     (the view is height-bound here, so headroom shrinks the island), so on a
     phone it stops being an overlay and becomes a strip above the board. */
  #banner {
    position: static; transform: none; white-space: normal;
    border-radius: 0; border-left: 0; border-right: 0; border-top: 0;
    text-align: center; padding: 8px 12px; flex: none;
  }
  #banner:empty { display: none; }
  #board { flex: 1 1 auto; min-height: 0; }

  /* A table row is name + status + seats + age + two buttons. Held on one line
     at 375px that came to ~658px, so the row ran off the side of the screen
     and BOTH buttons sat out in the dark past the right edge — the game was
     unreachable without a horizontal scroll nobody thinks to try. */
  .session-row { flex-wrap: wrap; }
  .session-row .name { min-width: 0; flex: 1 0 100%; }
  .session-row .when { margin-left: auto; }

  /* The header ran to two lines because "Log out" wrapped mid-word. */
  #topbar { gap: 8px; padding: 8px 12px; flex-wrap: nowrap; }
  #topbar button { white-space: nowrap; padding: 7px 10px; }
  #session-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* Touch targets. Gated on the POINTER, not the width: a small window on a
   desktop still has a mouse and does not need 44px buttons, while a large
   tablet has a finger and does. Five controls sat under the 44px floor,
   the 30px zoom cluster worst of all — and that one sits over the board,
   where a missed tap places a road you did not mean to place. */
@media (pointer: coarse) {
  button, select, input { min-height: 44px; }
  button.small { min-height: 38px; padding: 6px 12px; }
  #zoom-controls { gap: 4px; }
  /* The target grows to the 44px floor; the disc inside it does not. */
  #zoom-controls button { width: 44px; height: 44px; min-height: 44px; font-size: 16px; }
  #zoom-controls button::before { width: 32px; height: 32px; }
}

/* ==== painted chrome ==========================================================
   The lobby, wearing the same art as the game will. Everything here is a
   9-sliced PNG on a border-image, so a panel stretches to its content and the
   ornament at its corners keeps its shape instead of smearing.

   The slice numbers are measured off each file, not guessed. Where a piece has a
   transparent middle (the status pills, which are rings with a label inside) the
   slice has no `fill`; where the middle is painted (panels, rows, buttons) it
   does, and that painted middle is what stretches. */

#topbar {
  /* A painted beam, stretched to the bar's exact height rather than repeated —
     it is one object seen close up, and a repeat would put a join in the middle
     of it. Cropped below the lowest point of its own hand-painted top edge,
     because that edge waves and a flat crop left slivers of the background in
     the highlight. */
  background: url('/ui/topbar.jpg') center / 100% 100% no-repeat;
  background-color: #2a1a0e;
  border-bottom: 2px solid var(--gilt-dim, #8a6a2c);
  box-shadow: 0 4px 14px rgba(0,0,0,0.55);
}
/* The beam is darker and busier than the flat panel this used to sit on, so the
   mark needs more than a hairline shadow to stay off it. */
#topbar .brand { color: var(--gold); text-shadow: 0 1px 3px #000, 0 0 10px rgba(0,0,0,0.8); }
#topbar img.brand { filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.85)); }
#topbar #session-name { text-shadow: 0 1px 3px #000; }

/* Buttons. One pill, stretched: the caps hold their shape and the middle grows
   to whatever the label needs, which is why the slice is horizontal only. */
button {
  border-style: solid;
  border-width: 0 26px;
  border-image: url('/ui/button.png') 0 150 fill stretch;
  border-radius: 0;
  background: none;
  color: #f4e4c2;
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  padding: 8px 2px;
  min-height: 34px;
}
button:hover:not(:disabled) { filter: brightness(1.14); }
/* The primary action is the same pill, warmer — one file, not two. */
button:not(.ghost):not(.small) { filter: saturate(1.3) brightness(1.12); }
button.ghost { filter: saturate(0.55) brightness(0.86); color: var(--ink); }
button.ghost:hover:not(:disabled) { filter: saturate(0.8) brightness(1.05); }
button.small { border-width: 0 18px; padding: 4px 2px; min-height: 28px; font-size: 13px; }
/* These two carried their own background from before there was paint, and a
   modifier outranks the plain `button`/`.badge` rule that cleared it — so the
   flat colour kept painting a rectangle behind the pill. It showed as a yellow
   block behind YOUR MOVE and a red one behind Delete. */
button.danger, .badge.yours { background: none; }
button.danger { filter: hue-rotate(-20deg) saturate(1.6) brightness(0.9); }

/* Text fields are the one thing on the screen that should look pressed into the
   wood rather than sitting on it. */
input, select {
  border-style: solid;
  border-width: 12px 14px;
  border-image: url('/ui/field.png') 60 fill stretch;
  border-radius: 0;
  background: none;
  color: var(--ink);
  padding: 0 4px;
}
input::placeholder { color: #8a7a60; }

/* A select was painting itself white over the field behind it. `background: none`
   only clears the background; the control's own native appearance is a separate
   thing and was still being drawn, so the wood went under a system widget. */
select {
  appearance: none;
  -webkit-appearance: none;
  /* Room for the caret, which appearance: none takes away with the rest. */
  padding-right: 18px;
}

/* The caret sits in a wrapper rather than on the select, because the field's
   border-image uses `fill` and the fill paints over the element's own
   background — a caret drawn there is hidden by the texture. */
.select-wrap { position: relative; display: inline-flex; }
.select-wrap::after {
  content: '';
  position: absolute;
  right: 18px; top: 50%;
  width: 0; height: 0;
  margin-top: -2px;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--gold);
  pointer-events: none; /* the arrow must not eat clicks meant for the select */
}

/* The open list is drawn by the operating system and inherits nothing, so the
   options stayed white however the closed control was styled. */
select option {
  background-color: var(--panel-2);
  color: var(--ink);
}

/* Panels. The lobby's two cards, the login form and the modal all share one. */
.card {
  border-style: solid;
  border-width: 26px 28px;
  border-image: url('/ui/panel-fine.png') 55 fill stretch;
  border-radius: 0;
  background: none;
  padding: 0;
  gap: 12px;
}
.card h2 { color: var(--gold); text-shadow: 0 1px 2px #000; }

/* One table in the server list. Sliced wide on the left and right so the corner
   flourishes survive; the plain middle is what stretches across the row. */
.session-row {
  border-style: solid;
  border-width: 12px 26px;
  border-image: url('/ui/row.png') 34 84 34 84 fill stretch;
  border-radius: 0;
  background: none;
  gap: 12px;
}
.session-row .name { color: var(--ink); }
.session-row.finished { filter: saturate(0.6) brightness(0.82); }

/* Status pills. Three files, one shape, three temperatures — lit gold for your
   move, brass for a game in progress, cold iron for one that is over. They are
   rings, so no `fill`: the wood shows through behind the label. */
/* The ring's inner edge is soft, so the label needs a little air past the border
   or it reads as touching the metal. */
.badge {
  padding: 0 7px;
  border-style: solid;
  border-width: 9px 15px;
  border-image: url('/ui/status-open.png') 17 18 stretch;
  border-radius: 0;
  background: none;
  color: var(--ink-dim);
  padding: 0 2px;
  font-size: 10.5px; letter-spacing: 0.1em;
}
.badge.yours {
  border-image-source: url('/ui/status-turn.png');
  color: var(--gold); text-shadow: 0 1px 2px #000;
}
.session-row.finished .badge { border-image-source: url('/ui/status-done.png'); }
/* The painted border eats about 52px of a row's width, so the seat list — which
   is the longest thing on the line and the least important — is the one that
   gives. Two lines, then an ellipsis, rather than four lines that push every
   other row down the page. */
/* The row also contains a .spacer at flex: 1, which was splitting the free width
   with the seat list and leaving it about as wide as one name. The seat list is
   what the free width is FOR — and it is the ONLY thing in the row that gets it,
   which is why it has a name of its own now rather than sharing .muted with the
   status text beside it. That sharing is what turned "in progress" into
   "in progre…". */
.session-row .spacer { flex: 0 0 0; }
.session-row .seats {
  flex: 1 1 auto; min-width: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.session-row .status { flex: none; white-space: nowrap; }
.session-row .name { min-width: 104px; }
.session-row .when { flex: none; white-space: nowrap; }
/* Everything else in a row keeps its natural width. */
.session-row > span:not(.seats):not(.spacer) { flex: none; white-space: nowrap; }

/* ==== phones =================================================================
   Landscape only, and not by scaling. Scaled to fit, a landscape phone gives the
   cabinet 0.40 — 477x335, with the smallest label at 4.2px and buttons 14px tall
   against the 44 a thumb needs. The shortfall is height specifically: the design
   is 830 tall and the phone offers about 345. So below this height the cabinet
   comes apart into a grid at native text sizes, and the heavy carved frame — 95px
   of the two axes — is traded for a thin one, because on a phone that width is
   the game.

   Anything taller keeps the scaled cabinet: an iPad in landscape fits at 0.85,
   where the smallest text is 8.9px and it holds together as drawn. */

#rotate-me { display: none; }

@media (orientation: portrait) and (max-width: 900px) {
  body.in-game #rotate-me {
    display: grid; place-items: center;
    position: fixed; inset: 0; z-index: 80;
    background: linear-gradient(160deg, #2a1a0e, #150c05);
  }
  body.in-game #view-game, body.in-game #topbar { visibility: hidden; }
  .rotate-card { text-align: center; padding: 24px; }
  .rotate-card p { margin: 4px 0; }
  .rotate-card b { color: var(--gold); font-size: 17px; letter-spacing: 0.04em; }
  /* A phone that tips over, once, so it reads as an instruction and not a badge. */
  .rotate-phone {
    width: 54px; height: 92px; margin: 0 auto 18px;
    border: 3px solid var(--gilt-dim, #8a6a2c); border-radius: 10px;
    background: rgba(0,0,0,0.35);
    animation: tip 2.4s ease-in-out infinite;
  }
  @keyframes tip {
    0%, 34% { transform: rotate(0deg); }
    60%, 92% { transform: rotate(-90deg); }
    100% { transform: rotate(0deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    .rotate-phone { animation: none; transform: rotate(-90deg); }
  }
}

@media (orientation: landscape) and (max-height: 560px) {
  /* Reclaim the topbar: on a phone those 45px are about an eighth of the screen. */
  #topbar { padding: 3px 8px; gap: 6px; font-size: 12px; flex-wrap: nowrap; }
  #topbar button { min-height: 26px; padding: 2px 2px; border-width: 0 12px; font-size: 11px; }
  #topbar .brand { font-size: 12px; letter-spacing: 0.08em; }
  /* Landscape on a phone gives the bar very little height to spend. */
  #topbar img.brand { height: 19px; }
  /* Room is the scarce thing here; linking Discord is not a mid-game job. */
  #btn-discord, #whoami { display: none; }
  #view-game { height: calc(100vh - 34px); }

  .stage { position: static; overflow: visible; height: 100%; }
  .cabinet-fit { position: static; transform: none; height: 100%; }
  .cabinet {
    width: 100%; height: 100%;
    border-width: 7px 8px;
    border-image: url('/ui/frame.png') 138 149 136 156 stretch;
    filter: none;
  }
  .cabinet::before, .cabinet::after { inset: 0; }

  .felt {
    display: grid; gap: 5px; padding: 5px;
    grid-template-columns: 150px minmax(0, 1fr) 172px;
    grid-template-rows: auto minmax(0, 1fr) auto;
    border-radius: 4px;
  }
  /* On a phone the sign goes, but what it says does not: the turn line moves to
     a slim bar above the board, where it costs one line instead of a plank. */
  .sign {
    /* transform: none matters. The base rule centres the sign with
       translateX(-50%), which is right when it is absolutely positioned and
       wrong the moment it becomes a grid item — it was shifting it half its own
       width out of its column and under the plaque. Exactly 111px on a 223px
       sign, which is what gave it away. */
    position: static; transform: none; aspect-ratio: auto;
    grid-column: 2; grid-row: 1; align-self: start; justify-self: center; z-index: 6;
    /* Its own cell and no wider. Sized off the viewport it was wider than the
       board column, and being centred it overflowed both ways — the left half
       slid under the plaque and took the start of the name with it. */
    width: auto; max-width: 100%; min-width: 0;
    flex-direction: row; align-items: baseline; gap: 7px; justify-content: center;
    padding: 2px 9px; margin: 0;
    background: rgba(0,0,0,0.5); border: 1px solid var(--gilt-dim, #8a6a2c);
    border-radius: 99px; pointer-events: none;
  }
  /* `flex: 1 1 0`, not the default `0 1 auto`. On auto the item's basis is its
     own text width, and with two of them the pair measured wider than the column
     — so `justify-content: center` pushed the overflow out both sides and the
     name lost its first letters under the plaque. Basis zero means it takes the
     room that is left and ellipses the rest. */
  .sign #banner {
    margin: 0; font-size: 10.5px; color: var(--ink); text-shadow: 0 1px 2px #000;
    flex: 1 1 0; min-width: 0; max-width: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;
    -webkit-line-clamp: 1;
  }
  #me-seat    { grid-column: 1; grid-row: 1; position: static; }
  .rail.left  { grid-column: 1; grid-row: 2; position: static; width: auto;
                top: auto; bottom: auto; padding: 0; overflow-y: auto; }
  #board-wrap { grid-column: 2; grid-row: 1 / 3; position: relative; inset: auto;
                border-radius: 6px; }
  .rail.right { grid-column: 3; grid-row: 1 / 3; position: static; width: auto;
                top: auto; bottom: auto; }
  .tableau    { grid-column: 1 / 4; grid-row: 3; position: static; inset: auto;
                flex-direction: row; align-items: center; justify-content: center;
                gap: 10px; }
  .rail .grow { display: none; }

  /* Everything shrinks its ornament, not its text: the border art is what there
     is least room for and the labels are what have to stay readable. */
  .me-plaque { border-width: 11px 13px; gap: 6px; }
  .me-plaque .face { width: 40px; height: 40px; }
  .me-plaque .who { font-size: 12px; }
  .pip { width: 30px; height: 34px; padding-bottom: 3px; }
  .pip b { font-size: 12px; }
  .pip span { font-size: 6.5px; }
  .rival { border-width: 8px 11px; gap: 5px; }
  .rival .face { width: 28px; height: 28px; }
  .rival .who { font-size: 11px; }
  .mini i { font-size: 10px; padding: 0 4px; }
  #view-game .panel { border-width: 11px; }
  #view-game .panel h3 { font-size: 9px; margin-bottom: 2px; padding-bottom: 3px; }
  /* The rail keeps only the two doors; the feed itself opens over the board.
     And a door does not need a panel around it: framed as a tab strip inside a
     panel, the two buttons cost about a hundred pixels of the rail, which is
     three action buttons — and the actions are what you came to the rail for.
     They are two small buttons now, and the frame around them goes. */
  #view-game .talk {
    min-height: 0; flex: none;
    border-width: 0; border-image: none; background: none; filter: none;
  }
  .talk .feed, .talk .row { display: none; }
  .tabs { border-bottom: none; gap: 4px; }
  .tabs button[data-feed] {
    min-height: 26px; padding: 0 2px; font-size: 9px; letter-spacing: 0.08em;
    color: #f4e4c2; text-shadow: 0 1px 2px rgba(0,0,0,0.8);
    border-style: solid; border-width: 0 13px;
    border-image: url('/ui/button.png') 0 150 fill stretch;
    box-shadow: none; background: none;
  }
  .tabs button[data-feed].on { color: var(--gold); box-shadow: none; background: none;
                               filter: saturate(1.3) brightness(1.12); }

  /* The rail is a stacking context of its own (positioned, z-index 4), so the
     panel's z-index of 70 only ranks it against its siblings inside the rail —
     the plaque and the hand at 6 still painted over the top of it. The rail is
     what has to rise. */
  body.talk-open #view-game .rail.right { z-index: 70; }

  /* Scoped through #view-game deliberately. `#view-game .panel` sets
     position: relative and an id beats any number of classes, so
     `body.talk-open .talk` lost to it and the feed stayed in the rail. */
  body.talk-open #view-game .talk {
    position: fixed; inset: 0; z-index: 70;
    display: flex; flex-direction: column;
    border-width: 14px;
    background: #1b1008;
    filter: none;
  }
  body.talk-open #view-game .talk .feed:not([hidden]) {
    display: block; flex: 1; min-height: 0; overflow-y: auto;
    font-size: 15px; line-height: 1.45; padding: 8px 4px;
  }
  body.talk-open #view-game .talk .row:not([hidden]) { display: flex; gap: 8px; padding: 8px 4px 0; }
  body.talk-open #view-game .talk input { min-height: 44px; font-size: 15px; border-width: 9px 11px; }
  body.talk-open #view-game .talk button.small { min-height: 44px; font-size: 14px; border-width: 0 20px; }
  body.talk-open #view-game .tabs button { font-size: 12px; min-height: 44px; }
  body.talk-open #view-game #talk-close {
    display: inline-flex; flex: none; width: 46px; min-height: 44px;
    border: none; border-image: none; background: none;
    color: var(--gold); font-size: 17px; text-shadow: none;
  }
  .cost-row { font-size: 10.5px; padding: 1px 0; gap: 2px; }
  .cost-row .what { width: 46px; font-size: 9.5px; }
  .cost-chip { width: 14px; height: 14px; }
  /* Two columns, and no cap. A phone in landscape gives the rail about 145px for
     actions and a turn can offer seven — Road, Settlement, City, Buy dev card,
     Trade with bank, Offer trade, End turn — which is 227px in one column and
     will not fit however small the talk buttons get. In two it is four rows and
     it does. The labels were already short enough for it; only "Trade with bank"
     takes a second line, and it is the one you press least. */
  #view-game .actions {
    max-height: none;
    flex-direction: row; flex-wrap: wrap; align-content: flex-start; gap: 4px;
  }
  #view-game .actions button {
    width: calc(50% - 2px); min-height: 27px; font-size: 10px;
    border-width: 0 11px; padding: 2px 1px; line-height: 1.1;
  }
  .hand-label { display: none; }
  .tableau-row { gap: 10px; }
  #view-game .res-card { width: 30px; height: 39px; }
  #view-game .stack { padding-bottom: 11px; }
  #view-game .stack .count { width: 15px; height: 15px; font-size: 8.5px; border-width: 1px; }
  #view-game .dev-card { width: 42px; height: 55px; font-size: 7px; }
  /* In the board's own cell, at the foot of it. Pinned to the cabinet's corner
     they sat outside the felt over the left rail, clipped, and read as a pair of
     odd little buttons rather than as dice. */
  #dice { position: static; grid-column: 2; grid-row: 2;
          align-self: end; justify-self: center; transform: none;
          margin-bottom: 6px; padding: 3px 8px; gap: 5px; }
  #dice .die { --die: 24px; }
  #dice .total { font-size: 14px; }
  #dice .who { display: none; }
  #zoom-controls { top: 2px; right: 2px; gap: 0; }
  #zoom-controls button { width: 40px; height: 40px; min-height: 40px; font-size: 14px; }
  #zoom-controls button::before { width: 26px; height: 26px; }
  #btn-zoom-reset { font-size: 9.5px; }
}

/* ---- the lobby on a narrow screen -------------------------------------------
   A server-list row is six things in a line, which is fine at 1280 and ragged at
   375: the buttons wrapped one per line, each landing wherever the text before it
   ended. It becomes a small stack instead — name and status, then who is at the
   table, then the buttons together on one row of their own. */
@media (max-width: 720px) {
  /* Linking Discord is not a thing you do from a phone, and without it the bar
     fits on one line instead of pushing Log out off the edge. */
  #btn-discord, #whoami { display: none; }
  #topbar { padding: 8px 10px; gap: 8px; }
  #topbar .brand { font-size: 14px; }
  #topbar button { font-size: 12px; border-width: 0 18px; }

  #view-lobby { max-width: 100%; }
  .card { margin: 10px; border-width: 20px 22px; }

  .session-row {
    flex-wrap: wrap;
    row-gap: 6px;
    border-width: 14px 20px;
  }
  .session-row .name { min-width: 0; flex: 1 1 auto; font-size: 15px; }
  .session-row .badge { order: 2; }
  /* Full width, so it reads as a line rather than as a column that ran out. */
  .session-row .seats { order: 3; flex: 1 0 100%; -webkit-line-clamp: 3; font-size: 13px; }
  .session-row .when { order: 4; font-size: 12px; }
  .session-row .status { order: 5; font-size: 12px; }
  .session-row .spacer { order: 6; flex: 1 1 auto; }
  /* The buttons keep to the end of the last line, together and in a known order,
     instead of each one wrapping on its own. */
  .session-row button { order: 7; }
  .session-row button.danger { order: 8; }
}

/* ==== profile =================================================================
   Your record, and the picture you put to it. */
.profile { display: flex; flex-direction: column; gap: 14px; min-width: min(420px, 82vw); }
.profile-head { display: flex; gap: 14px; align-items: flex-start; }
.profile-head h2 { margin: 0 0 2px; color: var(--gold); }
.profile-head .row { margin-top: 8px; }
.small-note { font-size: 11.5px; margin: 6px 0 0; }

/* The same framed tile the board uses, so the picture you pick is shown in the
   thing it will actually appear in. */
.profile-face {
  position: relative; width: 108px; height: 108px; flex: none;
  background: url('/ui/portrait-frame.png') center / 100% 100% no-repeat;
}
.profile-face i {
  position: absolute; inset: 15.5%;
  background-size: cover; background-position: center top;
  background-color: rgba(0,0,0,0.35);
  border-radius: 1px;
}

.stat-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); gap: 6px;
}
.stat {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 8px 4px; gap: 1px;
  background: rgba(0,0,0,0.28); border: 1px solid rgba(216,164,74,0.22); border-radius: 6px;
}
.stat b { font-size: 19px; color: var(--gold); line-height: 1; font-family: var(--font-display); }
.stat span { font-size: 10px; letter-spacing: 0.05em; color: var(--ink-dim); text-align: center; }
.profile-foot { justify-content: flex-end; }

@media (max-width: 720px) {
  .profile { min-width: 0; width: 100%; }
  .profile-head { flex-direction: column; align-items: center; text-align: center; }
  .profile-head .row { justify-content: center; }
}

/* Dice statistics — the third tab beside table talk and the log.
   Derived from the game log rather than recorded separately: every roll has
   always gone in as { type: 'roll', total }, so this reads back a history that
   was already being kept, and is right about games played before it existed. */
.dice-stats { padding: 8px 4px 4px; font-size: 12px; }
.dice-head { margin-bottom: 8px; letter-spacing: 0.02em; }
.dice-head b { font-family: var(--font-display); font-size: 15px; color: var(--gold); }

.dice-row { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; }
.dice-n {
  width: 18px; text-align: right; flex: none;
  font-family: var(--font-display); color: var(--ink-dim);
}
/* The two numbers the whole board is built around are red on a real token, and
   they are the two anybody is actually tracking. */
.dice-row.hot .dice-n { color: #d06a4f; font-weight: 700; }

.dice-track {
  position: relative; flex: 1; min-width: 0; height: 11px;
  background: rgba(0, 0, 0, 0.34);
  border-radius: 2px; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
}
.dice-track i {
  display: block; height: 100%; border-radius: 2px;
  background: linear-gradient(180deg, #b99447, #7d6026);
}
.dice-row.hot .dice-track i { background: linear-gradient(180deg, #c5674a, #8d3f2a); }

/* Where the number should have got to by now. A tick, not a second bar: the
   question is "is 8 behind?", and a mark to fall short of answers that faster
   than two lengths to compare against each other. */
.dice-track u {
  position: absolute; top: -2px; bottom: -2px; width: 2px;
  margin-left: -1px; background: rgba(242, 236, 224, 0.75);
  border-radius: 1px;
}
.dice-count { width: 16px; text-align: right; flex: none; color: var(--ink-dim); }
.dice-note { margin: 9px 0 0; font-style: italic; }

/* Sign in with Google. Google's brand guidance wants their mark on a plain light
   or dark surface rather than restyled — so the button keeps the painted card's
   proportions and shadow, and leaves the mark itself alone. */
.google-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  width: min(100%, 260px); margin: 6px auto 2px; padding: 0 18px;
  height: 44px; box-sizing: border-box;
  background: #f6f2e8; color: #2a2118;
  border: 1px solid #6b5433; border-radius: 5px;
  font-family: var(--font-display); font-size: 14px; letter-spacing: 0.02em;
  text-decoration: none; cursor: pointer;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.google-btn:hover { background: #fffdf7; }
.google-btn.disabled { opacity: 0.45; pointer-events: none; }
/* Google's four-colour G, drawn rather than fetched — a login screen should not
   be waiting on a third-party image to render its only button. */
.google-btn .g {
  width: 18px; height: 18px; flex: none; border-radius: 50%;
  background:
    conic-gradient(from -45deg,
      #ea4335 0deg 90deg, #fbbc05 90deg 180deg,
      #34a853 180deg 270deg, #4285f4 270deg 360deg);
  -webkit-mask: radial-gradient(circle, transparent 0 34%, #000 35%);
  mask: radial-gradient(circle, transparent 0 34%, #000 35%);
}
#view-auth .small-note { font-size: 11px; margin: 6px 0 0; }

/* The login card used to be sized by the width of its username and password
   fields. With those gone it had nothing to push against and spread across the
   whole screen, so it says its own width now. */
#view-auth .card { width: min(92vw, 380px); text-align: center; }
#view-auth .muted { text-align: center; }
#view-auth .error:empty { display: none; }

/* "or" between signing in and not bothering. A rule either side rather than a
   bare word, so the two routes read as alternatives rather than a list. */
#view-auth .or {
  display: flex; align-items: center; gap: 10px;
  margin: 12px 0 10px; color: var(--ink-dim); font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.14em;
}
#view-auth .or::before, #view-auth .or::after {
  content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(180, 150, 96, 0.45), transparent);
}
.guest-btn { width: min(100%, 260px); margin: 0 auto; display: block; }

/* Choosing a name. The verdict sits under the field and changes as it is typed,
   so the answer arrives before the button is reached for. */
#pick-name { width: 100%; margin-top: 4px; }
.pick-status { min-height: 18px; margin: 7px 0 2px; font-size: 12px; }
.pick-status.free { color: #86b96a; }
.pick-status.taken { color: #d08a6a; }

/* Discarding. Your hand, as cards, in the dialogue that is covering your hand. */
.discard-hand {
  display: flex; flex-wrap: wrap; gap: 7px;
  justify-content: center; margin: 10px 0 4px;
}
.discard-card {
  width: 44px; height: 57px; cursor: pointer;
  transition: transform 0.12s ease, filter 0.12s ease, opacity 0.12s ease;
}
.discard-card:hover { transform: translateY(-3px); }
/* Chosen cards lift and gild, so the ones you are losing are the ones that stand
   out rather than the ones you are keeping. */
.discard-card.chosen {
  transform: translateY(-7px);
  box-shadow: 0 0 0 2px var(--gold), 0 6px 12px rgba(0, 0, 0, 0.6);
}
/* Once the count is met the rest go quiet — clicking a further card would do
   nothing, and a control that does nothing should not look live. */
.discard-card.spare { opacity: 0.45; cursor: default; }
.discard-card.spare:hover { transform: none; }

/* Private tables. */
.private-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--ink-dim); white-space: nowrap;
}
.badge.lock { color: var(--ink-dim); letter-spacing: 0.08em; }
.passcode-box .code {
  font-family: var(--font-display); font-size: 30px; letter-spacing: 0.14em;
  color: var(--gold); text-align: center; margin: 10px 0 6px;
  user-select: all;   /* one tap selects the whole thing on a phone */
}
.invited-list { margin-top: 10px; display: grid; gap: 4px; }
#join-code-form { margin-top: 8px; }

/* The legal footer. Present on every screen including the sign-in card, because
 * Google's reviewer and the CC BY-SA licences both need it reachable by somebody
 * who has not signed in. Quiet enough not to compete with the table. */
#site-footer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 5;
  display: flex; gap: 18px; justify-content: center;
  padding: 7px 12px;
  font-size: 12px; letter-spacing: 0.04em;
  background: linear-gradient(180deg, rgba(20, 12, 6, 0), rgba(20, 12, 6, 0.72) 55%);
  pointer-events: none;
}
#site-footer a { color: rgba(216, 164, 74, 0.62); text-decoration: none; pointer-events: auto; }
#site-footer a:hover { color: var(--gold); text-decoration: underline; }
