/* ===========================================================================
   quirk.city — UI skin
   Pure HTML/CSS overlaid on the WebGL canvas. Everything here is deliberately
   chunky: hard edges, no gradients where a dither would do, no border-radius
   larger than 0. The one exception is the CRT overlay, which is allowed to be
   a gradient because CRTs were analogue and so is this joke.
   =========================================================================== */

:root {
  /* Palette mirrors src/config.js — keep them in sync if you change either. */
  --soot:      #141824;
  --slate-d:   #252b3d;
  --slate:     #3d4459;
  --slate-l:   #5f6880;
  --grey:      #868fa6;
  --grey-l:    #b3bacb;
  --bone:      #eef1f7;
  --brown-d:   #6f4a33;
  --brown:     #a4673f;
  --brick:     #c45440;
  --brick-l:   #e3785c;
  --sodium:    #f9b866;
  --gold:      #ffe289;
  --cream:     #fff4cf;
  --mta:       #3f8ccc;
  --cyan:      #78d8f7;
  --green:     #57d693;
  --pink:      #ff79b8;
  --purple:    #c48cff;

  --ui-bg:     rgba(20, 24, 36, 0.88);
  --ui-edge:   #5f6880;
  --font:      'Press Start 2P', 'Courier New', monospace;

  --hud-h: 56px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
  background: var(--soot);
  color: var(--bone);
  font-family: var(--font);
  /* Pixel fonts look filthy when antialiased. */
  -webkit-font-smoothing: none;
  font-smooth: never;
  text-rendering: geometricPrecision;
  overscroll-behavior: none;
  /* `manipulation`, NOT `none`.
     Both kill the browser's double-tap zoom, which is what this was here for.
     But `none` also kills PINCH — and iOS zooms the page on its own anyway
     (focusing a small input is the usual way), so a page that had no pinch had
     no way back out of it either: double-tap, and you were stuck at 2x with
     the game still rendering at 1x underneath. `manipulation` leaves the pinch
     as the way out. The canvas takes `none` for itself below, so a pinch over
     the street is still the game's own zoom and never the browser's. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

body { user-select: none; -webkit-user-select: none; }

/* ---------------------------------------------------------------------------
   The canvas.
   main.js renders at a fraction of the display size; CSS blows it back up with
   nearest-neighbour. This is what makes the pixels real pixels rather than a
   post-process pretending.
   --------------------------------------------------------------------------- */
#scene {
  /* The street is dragged and pinched by the game itself — see the pointer
     handlers in main.js. The browser gets none of it. */
  touch-action: none;
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  cursor: grab;
  z-index: 0;
}
#scene.dragging { cursor: grabbing; }
#scene.pointing  { cursor: pointer; }

/* ---------------------------------------------------------------------------
   CRT overlay
   --------------------------------------------------------------------------- */
#crt {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 40;
  mix-blend-mode: multiply;
  /* Kept deliberately faint. The scanlines are meant to suggest a screen, not
     to compete with the art for the viewer's attention. */
  opacity: 0.5;
}
#crt.off { display: none; }

#crt-scan {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 1) 0px,
    rgba(255, 255, 255, 1) 2px,
    rgba(214, 220, 232, 1) 3px,
    rgba(214, 220, 232, 1) 4px
  );
  opacity: 0.35;
}
#crt-vig {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse at center,
    rgba(255, 255, 255, 1) 62%,
    rgba(238, 241, 247, 1) 86%,
    rgba(214, 219, 232, 1) 100%
  );
}

/* ---------------------------------------------------------------------------
   Floating price tags projected from 3D
   --------------------------------------------------------------------------- */
#tags {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 10;
  overflow: hidden;
}
.tag {
  position: absolute;
  transform: translate(-50%, -100%);
  background: var(--soot);
  border: 2px solid var(--slate-l);
  border-bottom-color: var(--soot);
  padding: 3px 5px 4px;
  font-size: 8px;
  line-height: 1.1;
  white-space: nowrap;
  color: var(--gold);
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55);
  will-change: transform;
}
.tag.mine   { border-color: var(--green); color: var(--green); }
.tag.rival  { border-color: var(--pink);  color: var(--pink); }
/* You rent a wall here but the building is somebody else's. Its own colour,
   because it is its own relationship: not yours, not a stranger's. */
.tag.tenant { border-color: var(--cyan);  color: var(--cyan); }

/* --- Whose is it: said in words, not only in colour ----------------------
   The border colours above were the whole of the answer, which fails twice
   over: green-against-pink is the commonest colour confusion there is, and
   even with perfect vision it means memorising a key. A two-word ribbon costs
   five pixels of height and needs no key at all.

   Written as ::before, the same device .first-place already uses, so they
   share one line-of-text slot at the top of the tag. That is also why the
   combinations below are spelled out rather than left to cascade: two rules
   both setting ::before would silently drop one of the two facts. */
.tag.mine::before,
.tag.tenant::before {
  display: block;
  font-size: 5px;
  letter-spacing: 0.5px;
  margin-bottom: 2px;
}
.tag.mine::before   { content: '\25B2 YOURS'; color: var(--green); }
.tag.tenant::before { content: '\25B8 YOUR BOARD'; color: var(--cyan); }
/* An empty lot. Gold, so a street of them reads as opportunity rather than
   as a row of buildings that happen to have nobody in them. */
.tag.for-sale { border-color: var(--gold); color: var(--gold); }
.tag.hot    { border-color: var(--sodium); color: var(--sodium); }
/* The one building at the very front of the city. Ribbon wins over
   mine/rival's border colour, since holding first place is the rarer fact. */
.tag.first-place {
  border-color: var(--gold);
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55), 0 0 0 1px var(--gold);
}
.tag.first-place::before {
  content: '\2605 FIRST PLACE';
  display: block;
  font-size: 5px;
  color: var(--gold);
  letter-spacing: 0.5px;
  margin-bottom: 2px;
}
/* Holding first place AND owning it is two facts about one building, and the
   tag has room for one ribbon. Combine rather than let the cascade pick. */
.tag.mine.first-place::before   { content: '\2605 FIRST PLACE \00B7 YOURS'; color: var(--gold); }
.tag.tenant.first-place::before { content: '\2605 FIRST PLACE \00B7 YOUR BOARD'; color: var(--gold); }
.tag .tag-owner {
  display: block;
  font-size: 6px;
  color: var(--grey-l);
  margin-top: 3px;
  max-width: 108px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Floating "+$1,234" rent-tick numbers */
.float-gain {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: 9px;
  color: var(--green);
  text-shadow: 2px 2px 0 var(--soot);
  animation: floatUp 1.6s steps(8) forwards;
  pointer-events: none;
}
@keyframes floatUp {
  0%   { opacity: 1; transform: translate(-50%, -50%); }
  100% { opacity: 0; transform: translate(-50%, -260%); }
}
/* Money leaving, rather than arriving. Green means a credit everywhere else in
   this HUD, and a green -$3 reads as a gain for exactly long enough to be
   confusing. */
.float-gain.spend { color: var(--brick-l); }

/* ---------------------------------------------------------------------------
   HUD
   --------------------------------------------------------------------------- */
#hud-top {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--hud-h);
  z-index: 30;
  display: flex;
  align-items: stretch;
  gap: 6px;
  padding: 6px;
  background: linear-gradient(to bottom, rgba(11,12,16,0.94), rgba(11,12,16,0.0));
  pointer-events: none;
}
#hud-top > * { pointer-events: auto; }

.hud-block {
  background: var(--ui-bg);
  border: 2px solid var(--ui-edge);
  padding: 6px 9px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
}

#hud-brand { border-color: var(--brick); padding: 5px 10px; }
.brand-1 { font-size: 8px; color: var(--sodium); letter-spacing: 1px; }
.brand-2 { font-size: 11px; color: var(--brick-l); margin-top: 4px; }

.stat { min-width: 92px; }
.stat-label { font-size: 6px; color: var(--grey); letter-spacing: 0.5px; }
.stat-value { font-size: 13px; color: var(--gold); margin-top: 5px; }
#stat-units .stat-value { color: var(--green); }
#stat-clock .stat-value { color: var(--cyan); }
#stat-clock { min-width: 78px; }

/* --- The forecast ---------------------------------------------------------
   Four values in the footprint of a stat block that was built for one. The
   condition and the temperature take the two rows every other block has; the
   wind and the UV share a third row underneath at label size, because they are
   the two nobody looks at until they are extreme. The block is only ~20px
   wider than the clock and the HUD is a flex row, so it costs the buttons on
   the right nothing. */
#stat-weather { min-width: 104px; }
/* Three rows inside a 56px bar that was designed for two, so the leading is
   pinned rather than left to the font: Press Start 2P's default line box is
   most of a row taller than its glyphs, and three of those overflow the block
   and clip the last one. */
#stat-weather .stat-label,
#stat-weather .stat-value,
#stat-weather .wx-sub { line-height: 1; }
#stat-weather .stat-label {
  display: flex;
  align-items: center;
  gap: 5px;
  color: var(--grey-l);
}
#stat-weather .stat-value { color: var(--cyan); margin-top: 5px; }
/* The icon is a 16x16 atlas tile blown up to 48 and scaled back down here.
   Without pixelated it is resampled into mud at this size. */
#wx-icon {
  width: 11px;
  height: 11px;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.wx-sub {
  display: flex;
  gap: 7px;
  margin-top: 5px;
  font-size: 6px;
  color: var(--grey);
  letter-spacing: 0.5px;
}
/* Only a UV worth reacting to gets a colour. A 2 in February is a number, not
   information. */
.wx-sub .wx-warn { color: var(--sodium); }

/* --- Altitude -------------------------------------------------------------
   The same three-row shape as the forecast, plus a bar. It appears only above
   the rooflines, which means it is the one block in this HUD that arrives
   mid-game — so it is sized to the widest thing it will ever say ("THE MOON")
   rather than to its contents, or the whole row of buttons to its right would
   shuffle sideways every time the word changed. */
#stat-flight { min-width: 104px; }
#stat-flight .stat-label,
#stat-flight .stat-value,
#stat-flight .wx-sub { line-height: 1; }
#stat-flight .stat-label { color: var(--grey-l); }
#stat-flight .stat-value { color: var(--cyan); margin-top: 5px; }
/* Lit only while the escape burn is actually running. See space.js. */
#stat-flight.is-burning { border-color: var(--sodium); }
#stat-flight.is-burning .stat-value { color: var(--sodium); }
#stat-flight.is-burning #val-flight-note { color: var(--gold); }
/* How much of the climb is done. Two pixels tall: it is a progress bar for
   something that takes eight seconds, and anything taller would be a gauge. */
.flight-bar {
  height: 2px;
  margin-top: 5px;
  background: var(--slate-d);
}
.flight-bar > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--sodium);
}

/* --- The crossing ---------------------------------------------------------
   Dead centre, over a screen that is entirely star streaks by the time this
   is legible. No panel behind it: there is nothing to separate it FROM, and a
   bordered box in the middle of interplanetary space reads as a dialog the
   player is supposed to dismiss. */
#crossing {
  position: fixed;
  left: 0;
  right: 0;
  top: 34%;
  z-index: 40;
  text-align: center;
  pointer-events: none;
  text-shadow: 2px 2px 0 rgba(0,0,0,0.85);
}
#crossing-kicker {
  font-size: 7px;
  letter-spacing: 3px;
  color: var(--sodium);
}
#crossing-title {
  font-size: 26px;
  letter-spacing: 2px;
  color: var(--cream);
  margin-top: 12px;
}
#crossing-sub {
  font-size: 7px;
  letter-spacing: 1px;
  color: var(--grey-l);
  margin-top: 14px;
}
@media (max-width: 720px) {
  #crossing-title { font-size: 16px; }
  /* Narrower rather than gone. A phone player has the drag-lift control and
     can leave the planet with it, so the one readout that says how far up they
     are is the last thing that should be dropped for space. */
  #stat-flight { min-width: 68px; }
  #stat-flight .flight-bar { display: none; }
}

/* --- The course dial ------------------------------------------------------
   Sits just under the top HUD and above everything the sky is doing, because
   what it is labelling is IN the sky: the disc the burn is pointed at is drawn
   up there too, and the strip is the caption for it rather than a menu in
   front of it. No panel, for the same reason #crossing has none. */
#dial {
  position: fixed;
  left: 0;
  right: 0;
  top: 92px;
  z-index: 38;
  text-align: center;
  pointer-events: none;
  text-shadow: 2px 2px 0 rgba(0,0,0,0.85);
}
#dial-kicker {
  font-size: 6px;
  letter-spacing: 2px;
  color: var(--grey);
}
#dial-list {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  justify-content: center;
  gap: 18px;
  flex-wrap: wrap;
}
#dial-list li {
  font-size: 8px;
  letter-spacing: 1px;
  color: var(--slate-l);
}
/* The selected one, and nothing subtle about it: this is a choice being made
   with a key that normally does something else, so it has to be obvious that
   the key is doing this now. */
#dial-list li.is-aimed {
  color: var(--sodium);
}
#dial-list li.is-aimed::before { content: 'B8 '; }
@media (max-width: 720px) {
  #dial { top: 74px; }
  #dial-list { gap: 10px; }
  #dial-list li { font-size: 6px; }
}

.stat.bump { animation: bump 0.32s steps(4); }
@keyframes bump {
  0%   { transform: translateY(0); }
  40%  { transform: translateY(-5px); }
  100% { transform: translateY(0); }
}

.hud-buttons { flex-direction: row; gap: 5px; margin-left: auto; align-items: center; }
.icon-btn {
  font-family: var(--font);
  font-size: 8px;
  color: var(--grey);
  background: var(--slate-d);
  border: 2px solid var(--slate-l);
  padding: 7px 8px;
  cursor: pointer;
  min-width: 34px;
  min-height: 32px;
}
.icon-btn:hover { color: var(--bone); border-color: var(--grey-l); }
.icon-btn.is-on { color: var(--gold); border-color: var(--gold); }
.icon-btn:active { transform: translate(1px, 1px); }

/* Badges */
#badges {
  position: fixed;
  top: calc(var(--hud-h) + 4px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  gap: 6px;
  pointer-events: none;
}
.badge {
  font-size: 7px;
  padding: 5px 8px;
  border: 2px solid var(--slate-l);
  background: var(--ui-bg);
  color: var(--grey-l);
  letter-spacing: 0.5px;
}
.badge-live {
  border-color: var(--brick-l);
  color: var(--brick-l);
  animation: blink 1.1s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0.35; } }

/* ---------------------------------------------------------------------------
   Panels (leaderboard)
   --------------------------------------------------------------------------- */
.panel {
  position: fixed;
  left: 6px;
  top: calc(var(--hud-h) + 8px);
  width: 218px;
  max-height: min(48vh, 380px);
  z-index: 25;
  background: var(--ui-bg);
  border: 2px solid var(--ui-edge);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
  display: flex;
  flex-direction: column;
}
.panel-head {
  font-size: 7px;
  color: var(--sodium);
  padding: 7px 8px;
  border-bottom: 2px solid var(--slate);
  display: flex;
  justify-content: space-between;
  align-items: center;
  letter-spacing: 0.5px;
}
.panel-toggle {
  font-family: var(--font);
  background: none;
  border: none;
  color: var(--grey);
  cursor: pointer;
  font-size: 10px;
  padding: 0 3px;
  line-height: 1;
}
.panel-body {
  /* So the panel can be shrunk by the rail it sits in — see #left-rail > *. */
  min-height: 0;
  margin: 0;
  padding: 5px;
  list-style: none;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--slate-l) transparent;
}
.panel.collapsed .panel-body { display: none; }

.lb-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 5px 4px;
  font-size: 7px;
  border-bottom: 1px solid rgba(74, 78, 99, 0.35);
}
.lb-row:last-child { border-bottom: none; }
.lb-rank { color: var(--grey); width: 16px; flex: none; }
.lb-name {
  flex: 1;
  color: var(--bone);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lb-pts { color: var(--gold); flex: none; }
.lb-units { color: var(--grey); font-size: 6px; flex: none; width: 26px; text-align: right; }
.lb-row.me { background: rgba(47, 191, 113, 0.14); }
.lb-row.me .lb-name { color: var(--green); }
.lb-row.moved { animation: flashRow 0.6s steps(3); }
@keyframes flashRow { 0%, 60% { background: rgba(255, 215, 94, 0.22); } }

/* ---------------------------------------------------------------------------
   Power-up rack
   --------------------------------------------------------------------------- */
/* The right-hand column: achievements, The Paper, then whatever power-ups are
   running. Everything in here was previously fixed to the same corner at the
   same top offset and simply drew over the top of everything else. */
#right-rail {
  position: fixed;
  right: 6px;
  top: calc(var(--hud-h) + 8px);
  z-index: 26;
  width: 250px;
  /* dvh, not vh: on iOS `100vh` is the height the page would have with the
     browser chrome hidden, so a column bounded by it runs off the bottom of
     what is actually on screen and lands on the dock down there. */
  max-height: calc(100dvh - var(--hud-h) - 70px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  pointer-events: none;
}
#right-rail > * { pointer-events: auto; min-height: 0; }   /* see #left-rail */

#powerup-rack {
  position: static;
  display: flex;
  flex-direction: column;
  gap: 5px;
  align-items: flex-end;
  pointer-events: none;
}
.pu-chip {
  background: var(--ui-bg);
  border: 2px solid var(--purple);
  padding: 6px 8px;
  font-size: 7px;
  color: var(--purple);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
  min-width: 148px;
}
/* A chip that has a subject to travel to. The ones that do something to the
   whole street have nowhere to send you and deliberately look inert. */
.pu-chip.is-live {
  cursor: pointer;
  transition: transform 0.08s steps(2), box-shadow 0.08s steps(2);
}
.pu-chip.is-live:hover {
  transform: translate(-1px, -1px);
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.5);
}
.pu-chip.is-live:active { transform: translate(1px, 1px); box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5); }
/* Locked on: the same orange the tracking outline and the card use. */
.pu-chip.following {
  border-color: var(--sodium);
  color: var(--sodium);
}
.pu-chip .pu-name { display: block; }
.pu-chip .pu-bar {
  display: block;
  height: 4px;
  margin-top: 5px;
  background: var(--slate);
  border: 1px solid var(--slate-l);
}
.pu-chip .pu-fill {
  display: block;
  height: 100%;
  background: var(--purple);
  transform-origin: left center;
}
.pu-chip.bad  { border-color: var(--brick-l); color: var(--brick-l); }
.pu-chip.bad  .pu-fill { background: var(--brick-l); }
.pu-chip.good { border-color: var(--green); color: var(--green); }
.pu-chip.good .pu-fill { background: var(--green); }

/* ---------------------------------------------------------------------------
   Toasts
   --------------------------------------------------------------------------- */
/* One bottom-right column holding the toasts and the live-event cards.
   They were two separately positioned fixed elements before, both anchored to
   the bottom right, which meant a couple of toasts sat straight on top of the
   BREAKING card and you could not read either. Stacked in one flex column the
   toasts push up from the cards and nothing overlaps at any count.

   Empty on a phone — see ui._layOutForPhone, and the mobile blocks below. */
#corner-stack {
  position: fixed;
  right: 6px;
  bottom: 34px;
  z-index: 35;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  max-width: min(340px, 76vw);
  pointer-events: none;
}

#toasts {
  display: flex;
  flex-direction: column-reverse;
  gap: 5px;
  align-items: flex-end;
  width: 100%;
  pointer-events: none;
}
.toast {
  background: var(--ui-bg);
  border: 2px solid var(--slate-l);
  border-left-width: 5px;
  padding: 8px 10px;
  font-size: 7px;
  line-height: 1.55;
  color: var(--bone);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
  animation: toastIn 0.22s steps(3);
  max-width: 100%;
}
.toast.out { animation: toastOut 0.3s steps(3) forwards; }
.toast.win  { border-left-color: var(--green);  color: var(--green); }
.toast.lose { border-left-color: var(--brick-l); color: var(--brick-l); }
.toast.info { border-left-color: var(--cyan); }
.toast.cash { border-left-color: var(--gold); color: var(--gold); }
.toast.chaos { border-left-color: var(--purple); color: var(--purple); }
@keyframes toastIn  { from { opacity: 0; transform: translateX(28px); } }
@keyframes toastOut { to   { opacity: 0; transform: translateX(28px); } }

/* ---------------------------------------------------------------------------
   News ticker
   --------------------------------------------------------------------------- */
#ticker {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  height: 26px;
  z-index: 28;
  background: var(--soot);
  border-top: 2px solid var(--brick);
  overflow: hidden;
  display: flex;
  align-items: center;
}
#ticker-track {
  white-space: nowrap;
  will-change: transform;
  font-size: 8px;
  color: var(--sodium);
  padding-left: 100%;
  /* Duration is set from the measured width in ui.js (_sizeTicker) so the
     ticker holds a constant reading speed as the headline roster grows. This
     value is only the pre-measurement fallback. */
  animation: scrollTicker 480s linear infinite;
}
@keyframes scrollTicker {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}

/* ---------------------------------------------------------------------------
   Modals — shared
   --------------------------------------------------------------------------- */
/* The account and help sheets still take over the screen. They are asides, and
   there is nothing behind them worth watching. */
#mock-backdrop, #help-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: rgba(11, 12, 16, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px;
  overflow-y: auto;
}

/* The LISTING does not. Same reasoning as the build panel next door: everything
   in it changes something you are meant to be looking at — the billboard, the
   colour, the sign — and a dialog in the middle of the screen hides the one
   building it is about. So this is a positioning root that does not block, and
   the listing itself is a panel parked on the right. */
#modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;   /* the street underneath stays live */
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
}
.hidden { display: none !important; }

#mock-sheet, #help {
  background: var(--slate-d);
  border: 3px solid var(--slate-l);
  box-shadow: 0 0 0 3px var(--soot), 6px 6px 0 rgba(0,0,0,0.6);
  width: min(660px, 100%);
  max-height: calc(100dvh - 28px);
  overflow-y: auto;
  position: relative;
  animation: modalIn 0.16s steps(3);
}

#modal {
  pointer-events: auto;
  position: fixed;
  right: 6px;
  top: calc(var(--hud-h) + 8px);
  width: min(420px, calc(100vw - 12px));
  max-height: calc(100dvh - var(--hud-h) - 16px);
  overflow-y: auto;
  background: var(--ui-bg);
  border: 2px solid var(--ui-edge);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
  animation: modalIn 0.16s steps(3);
}

/* One column, at panel width. The floor plan and the specs sat side by side
   because there were 660 pixels to fill; there are not any more. */
/* WHO owns it. The first block under the header, because it is the first
   question anybody has about a building on this street. */
#m-ownerbox {
  padding: 12px 14px;
  border-bottom: 2px solid var(--slate);
}
.own-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.own-row + .own-row { margin-top: 8px; }
.own-label {
  font-size: 6px;
  letter-spacing: 0.5px;
  color: var(--grey);
  flex: none;
}
.own-name {
  font-size: 9px;
  color: var(--bone);
  text-align: right;
  line-height: 1.5;
  word-break: break-word;
}
.own-name.me { color: var(--green); }
.own-views { font-size: 6px; color: var(--grey-l); text-align: right; line-height: 1.6; }

/* Every block in the panel carries its own gutter, so nothing sits against
   the edge and the sections read as separate things. */
#m-buy-wrap { padding: 12px 14px; border-bottom: 2px solid var(--slate); margin-bottom: 0; }
@keyframes modalIn { from { transform: translateY(14px); opacity: 0; } }

/* STICKY, not absolute.
   Both of these panels scroll their own content, and an absolutely positioned
   child of a scroll box scrolls away with it. On a desktop that is a nuisance
   — Esc still closes, and so does nothing else being in the way. On a phone it
   left the bottom sheet with no way out at all: no Escape key, and the
   backdrop stays deliberately click-through so the street underneath can still
   be dragged while you read the listing.
   Floated rather than laid out on a row of its own, so it keeps the top-right
   corner it has always had — and because a float in the same formatting
   context pushes the header's own text lines clear of it for free. */
#m-close, #build-close {
  position: sticky;
  float: right;
  top: 8px;
  /* The root is `touch-action: manipulation`; this says so again because a
     close button is the last control that should lose a tap to a gesture
     the browser thought was starting. */
  touch-action: manipulation;
  font-family: var(--font);
  font-size: 9px;
  background: var(--slate);
  color: var(--grey-l);
  border: 2px solid var(--slate-l);
  width: 28px; height: 28px;
  cursor: pointer;
  /* Over the owner box, which comes later in the document and would otherwise
     paint its own background straight over the top of this. */
  z-index: 4;
}
#m-close { margin: 8px 8px 0 0; }
/* #build carries its own 16/14 padding, which is already the inset — and
   matching `top` to it means the button does not hop the first time the panel
   is scrolled. */
#build-close { top: 16px; margin: 0 0 0 8px; }
#m-close:hover, #build-close:hover { color: var(--brick-l); border-color: var(--brick-l); }



#m-floorplan {
  width: 112px;
  height: 112px;
  image-rendering: pixelated;
  border: 2px solid var(--slate-l);
  background: var(--soot);
  display: block;
}
#m-specs {
  margin-top: 10px;
  font-size: 6px;
  line-height: 1.9;
  color: var(--grey-l);
}
#m-specs b { color: var(--bone); }

#m-right { flex: 1; min-width: 0; }

#m-bidbox {
  background: var(--soot);
  border: 2px solid var(--slate);
  padding: 10px;
  margin-bottom: 12px;
}
.bid-row { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.bid-row + .bid-row { margin-top: 9px; }
.bid-label { font-size: 6px; color: var(--grey); }
.bid-value { font-size: 15px; color: var(--sodium); }
.bid-owner {
  font-size: 7px;
  color: var(--bone);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 60%;
}
.bid-owner.me { color: var(--green); }

.sub-head { font-size: 6px; color: var(--sodium); margin-bottom: 7px; letter-spacing: 0.5px; }
#m-amenities { margin: 0; padding: 0; list-style: none; }
#m-amenities li {
  font-size: 7px;
  line-height: 1.75;
  color: var(--grey-l);
  padding: 4px 0 4px 14px;
  position: relative;
}
#m-amenities li::before {
  content: '>';
  position: absolute;
  left: 0;
  color: var(--brick-l);
}

/* ---------------------------------------------------------------------------
   The billboard
   --------------------------------------------------------------------------- */
/* An empty lot, priced by how close it sits to somebody famous. */
#m-buy-wrap { margin-bottom: 12px; }
#m-buy-price {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
}
#m-buy-amount { font-size: 22px; color: var(--gold); }
#m-buy-why { font-size: 6px; color: var(--grey-l); line-height: 1.6; }
#m-buy-error {
  font-size: 6px;
  line-height: 1.7;
  color: var(--pink);
  margin: 8px 0 0;
}

#m-billboard-wrap { padding: 0 16px 14px; }
#m-billboard-view { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
#m-billboard-canvas {
  /* The whole modal width. At 240px a cell was under 8px across, which is a
     fiddly target for a mouse and an impossible one for a fingertip.
     height:auto rather than a fixed size or a hardcoded aspect-ratio, so the
     box follows the canvas element's own dimensions — the board has already
     changed shape once, and this way the CSS does not have to be found and
     changed again when it does. */
  display: block;
  width: 100%;
  height: auto;
  margin: 0 auto;
  background: var(--soot);
  border: 2px solid var(--slate);
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  touch-action: none; /* the owner draws by dragging across it; don't let the page scroll instead */
}
#m-billboard-empty { font-size: 6px; color: var(--grey); margin: 0; text-align: center; }
.billboard-link {
  display: inline-block;
  max-width: 100%;
  font-size: 7px;
  color: var(--cyan);
  word-break: break-all;
  text-decoration: underline;
}
.billboard-link::before { content: '\1F517  '; } /* a link, shown as itself — never a caption hiding the real URL */

#m-billboard-edit { margin-top: 10px; }
.billboard-hint { font-size: 6px; color: var(--grey); line-height: 1.7; margin: 0 0 8px; }

/* How the drawing gets made. Five ways in, one grid out. */
#m-board-modes { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; }
.board-mode {
  font-family: var(--font);
  font-size: 6px;
  letter-spacing: 0.5px;
  padding: 7px 9px;
  background: var(--soot);
  border: 2px solid var(--slate);
  color: var(--grey-l);
  cursor: pointer;
}
.board-mode:hover { border-color: var(--slate-l); color: var(--bone); }
.board-mode.is-picked { border-color: var(--sodium); color: var(--sodium); }

#m-board-source { margin-bottom: 10px; }
.board-src { display: flex; flex-direction: column; gap: 6px; }
.board-src input[type="text"] {
  font-family: var(--font);
  font-size: 7px;
  padding: 9px 8px;
  background: var(--soot);
  border: 2px solid var(--slate);
  color: var(--bone);
  width: 100%;
}
.board-src input[type="text"]:focus { border-color: var(--sodium); outline: none; }
.board-src input[type="file"] { font-family: var(--font); font-size: 6px; color: var(--grey-l); }
.board-src .text-btn { align-self: flex-start; }
.board-src .billboard-hint { margin: 0; }
#m-billboard-palette { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
#m-billboard-palette .build-swatch { padding: 5px; min-height: 0; }
#m-billboard-palette .build-swatch .chip { margin-right: 0; width: 12px; height: 12px; }
#m-billboard-edit-actions { margin-bottom: 8px; }
#m-billboard-linkinput {
  display: block;
  width: 100%;
  font-family: var(--font);
  font-size: 7px;
  padding: 9px 8px;
  background: var(--soot);
  border: 2px solid var(--slate);
  color: var(--bone);
}
#m-billboard-linkinput:focus { border-color: var(--slate-l); outline: none; }
#m-billboard-error {
  font-size: 7px;
  line-height: 1.7;
  color: var(--brick-l);
  margin: 10px 0 0;
  padding: 9px;
  border: 2px solid var(--brick);
  background: rgba(168, 50, 43, 0.14);
}

#m-foot {
  padding: 12px 14px 16px;
  border-top: 2px solid var(--slate);
  background: var(--soot);
}

#m-raise { display: flex; gap: 8px; align-items: stretch; }
.step-btn {
  font-family: var(--font);
  font-size: 13px;
  width: 44px;
  background: var(--slate);
  color: var(--bone);
  border: 2px solid var(--slate-l);
  cursor: pointer;
  flex: none;
}
.step-btn:hover { border-color: var(--gold); color: var(--gold); }
.step-btn:active { transform: translate(1px, 1px); }

#m-amount-wrap {
  flex: 1;
  display: flex;
  align-items: center;
  background: var(--slate-d);
  border: 2px solid var(--slate-l);
  padding: 0 10px;
  min-width: 0;
}
.dollar { font-size: 13px; color: var(--grey); margin-right: 5px; }
#m-amount {
  flex: 1;
  min-width: 0;
  font-family: var(--font);
  font-size: 15px;
  color: var(--gold);
  background: none;
  border: none;
  outline: none;
  padding: 12px 0;
  width: 100%;
}

#m-min-note { font-size: 6px; color: var(--grey); margin-top: 9px; }
#m-min-note b { color: var(--grey-l); }

.big-btn {
  display: block;
  width: 100%;
  font-family: var(--font);
  font-size: 11px;
  padding: 15px 10px;
  margin-top: 12px;
  background: var(--brick);
  color: var(--cream);
  border: 2px solid var(--brick-l);
  border-bottom-width: 5px;
  cursor: pointer;
  letter-spacing: 0.5px;
  line-height: 1.5;
}
.big-btn:hover:not(:disabled) { background: var(--brick-l); }
.big-btn:active:not(:disabled) { transform: translateY(2px); border-bottom-width: 2px; }
.big-btn:disabled { background: var(--slate); border-color: var(--slate-l); color: var(--grey); cursor: not-allowed; }

.text-btn {
  display: block;
  width: 100%;
  font-family: var(--font);
  font-size: 7px;
  padding: 12px;
  margin-top: 8px;
  background: none;
  border: none;
  color: var(--grey);
  cursor: pointer;
  text-decoration: underline;
}
.text-btn:hover { color: var(--bone); }

#m-fee-note {
  font-size: 6px;
  line-height: 1.8;
  color: var(--grey);
  margin: 11px 0 0;
  text-align: center;
}
#m-fee-note b { color: var(--gold); }
#m-fee-note.is-live b { color: var(--brick-l); }

#m-error {
  font-size: 7px;
  line-height: 1.7;
  color: var(--brick-l);
  margin: 10px 0 0;
  padding: 9px;
  border: 2px solid var(--brick);
  background: rgba(168, 50, 43, 0.14);
}

/* ---------------------------------------------------------------------------
   Fake checkout
   --------------------------------------------------------------------------- */
#mock-sheet { width: min(400px, 100%); padding: 20px; text-align: center; }
#mock-brand { font-size: 20px; color: var(--purple); letter-spacing: -1px; }
#mock-sub { font-size: 6px; color: var(--grey); margin-top: 9px; letter-spacing: 1px; }
#mock-line {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  font-size: 7px;
  color: var(--grey-l);
  padding: 14px 0;
  margin-top: 16px;
  border-top: 2px solid var(--slate);
  border-bottom: 2px solid var(--slate);
  text-align: left;
}
#mock-line b { font-size: 13px; color: var(--gold); }
#mock-fields { margin: 16px 0; text-align: left; }
.mock-field + .mock-field { margin-top: 11px; }
.mock-field label { display: block; font-size: 6px; color: var(--grey); margin-bottom: 6px; }
.mock-input {
  font-size: 8px;
  color: var(--grey-l);
  background: var(--soot);
  border: 2px solid var(--slate);
  padding: 11px 9px;
}
#mock-legal { font-size: 6px; color: var(--slate-l); margin-top: 14px; line-height: 1.7; }
#mock-sheet.paying #mock-pay { pointer-events: none; }

/* ---------------------------------------------------------------------------
   Build a building

   Deliberately NOT a covering modal. The whole point of this panel is that
   the construction site you clicked keeps standing in the 3D view while you
   configure it — the live preview IS the world behind this panel, morphing
   into whatever shape/colour is picked. A full-screen dimmed backdrop would
   hide the one thing this panel exists to show. So #build-backdrop is just a
   non-blocking positioning root, and #build is a right-anchored panel, not a
   centred dialog — same visual language as the leaderboard/portfolio panels,
   just parked on the empty side of the screen.
   --------------------------------------------------------------------------- */
#build-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none; /* the 3D view underneath stays fully interactive */
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  padding: 0;
}
#build {
  pointer-events: auto;
  position: fixed;
  right: 6px;
  top: calc(var(--hud-h) + 8px);
  width: min(300px, calc(100vw - 12px));
  max-height: calc(100dvh - var(--hud-h) - 16px);
  padding: 16px 14px;
  overflow-y: auto;
  background: var(--ui-bg);
  border: 2px solid var(--ui-edge);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
  animation: modalIn 0.16s steps(3);
}
#build-title { font-size: 11px; color: var(--brick-l); letter-spacing: 0.5px; padding-right: 26px; }
#build-count { font-size: 6px; color: var(--grey); margin: 6px 0 14px; line-height: 1.7; }

.build-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 16px;
}
.build-swatch {
  font-family: var(--font);
  font-size: 6px;
  padding: 8px 10px;
  min-height: 30px;
  background: var(--soot);
  color: var(--grey-l);
  border: 2px solid var(--slate);
  cursor: pointer;
  letter-spacing: 0.3px;
}
.build-swatch:hover { border-color: var(--slate-l); color: var(--bone); }
.build-swatch.is-picked { border-color: var(--gold); color: var(--gold); background: rgba(255,215,94,0.1); }

/* Colour swatches show the actual palette colour as a small chip beside the
   label, rather than tinting the whole button — a PINK button reading PINK
   in pink text on a dark ground is hard to read; a chip stays legible either
   way. */
.build-swatch .chip {
  display: inline-block;
  width: 9px; height: 9px;
  margin-right: 5px;
  vertical-align: -1px;
  border: 1px solid rgba(0,0,0,0.4);
}

#build-placements { display: flex; flex-direction: column; gap: 12px; margin-top: 4px; }
.build-option { padding: 12px; border: 2px solid var(--slate); background: var(--soot); }
.build-option-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 8px;
  color: var(--bone);
  letter-spacing: 0.5px;
}
.build-option-price { font-size: 12px; color: var(--gold); }
.build-option-note { font-size: 6px; color: var(--grey); line-height: 1.7; margin: 6px 0 0; }
.build-option .big-btn { margin-top: 10px; }
.build-head-btn { background: var(--slate-d); border-color: var(--gold); color: var(--gold); }
.build-head-btn:hover:not(:disabled) { background: rgba(255,215,94,0.14); }

#build-error {
  font-size: 7px;
  line-height: 1.7;
  color: var(--brick-l);
  margin: 12px 0 0;
  padding: 9px;
  border: 2px solid var(--brick);
  background: rgba(168, 50, 43, 0.14);
}

/* ---------------------------------------------------------------------------
   Help
   --------------------------------------------------------------------------- */
/* The panel is taller than a phone now, so it scrolls inside itself rather
   than pushing the START button off the bottom of the backdrop. */
#help { width: min(560px, 100%); padding: 24px 22px; max-height: 88vh; overflow-y: auto; }
#help h1 { font-size: 20px; color: var(--brick-l); margin: 0; line-height: 1.4; }
#help .tag { font-size: 7px; color: var(--sodium); margin: 12px 0 20px; line-height: 1.8; }
.help-h {
  font-size: 7px;
  color: var(--sodium);
  letter-spacing: 1px;
  margin: 20px 0 6px;
  padding-bottom: 6px;
  border-bottom: 2px solid var(--slate);
}
.help-h:first-of-type { margin-top: 0; }
.help-list { margin: 0; padding: 0 0 0 4px; list-style: none; }
.help-list li {
  font-size: 7px;
  line-height: 2;
  color: var(--grey-l);
  padding: 7px 0 7px 16px;
  position: relative;
}
.help-list li::before { content: '>'; position: absolute; left: 0; color: var(--brick-l); }
.help-list b { color: var(--bone); }
.help-list i { color: var(--gold); font-style: normal; }
/* The attribution Uppbeat's licence asks for, in the wording it asks for.
   Quieter than the rules above it and deliberately not shortened — the URL
   IS the credit, so it is printed rather than hidden behind a word. */
.credit {
  font-size: 6px;
  line-height: 2.2;
  color: var(--grey);
  margin: 10px 0 0;
  padding-left: 4px;
  word-break: break-all;
}
.credit a { color: var(--cyan); text-decoration: none; border-bottom: 1px solid var(--slate); }
.credit a:hover { color: var(--bone); border-bottom-color: var(--cyan); }

#help-money {
  font-size: 6px;
  line-height: 1.9;
  color: var(--grey);
  margin-top: 18px;
  padding: 11px;
  border: 2px solid var(--slate);
  background: var(--soot);
}
#help-money b { color: var(--gold); }
#help-money.is-live { border-color: var(--brick); }
#help-money.is-live b { color: var(--brick-l); }

/* ---------------------------------------------------------------------------
   Listing economics — the projection panel inside the modal.
   --------------------------------------------------------------------------- */
#m-econ {
  margin-top: 12px;
  padding: 10px;
  border: 2px solid var(--slate);
  background: var(--soot);
}
.occ-wrap { display: flex; align-items: center; gap: 7px; margin: 8px 0 10px; }
.occ-bar {
  flex: 1;
  height: 9px;
  background: var(--slate-d);
  border: 2px solid var(--slate);
  /* Chunky steps rather than a smooth slide — it is a pixel game. */
  overflow: hidden;
}
.occ-fill {
  height: 100%;
  width: 100%;
  background: var(--green);
  transition: width 0.12s steps(6), background-color 0.12s steps(2);
}
.occ-fill.warn { background: var(--sodium); }
.occ-fill.bad  { background: var(--brick); }
.occ-pct { font-size: 6px; color: var(--grey-l); flex: none; min-width: 52px; text-align: right; }

.econ-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  font-size: 6px;
  line-height: 2.2;
  color: var(--grey);
}
.econ-row b { color: var(--grey-l); font-size: 8px; }
.econ-row.econ-net {
  margin-top: 6px;
  padding-top: 8px;
  border-top: 2px solid var(--slate);
  color: var(--grey-l);
}
.econ-row.econ-net b { font-size: 11px; color: var(--gold); }
.econ-row.econ-net b.is-loss { color: var(--brick-l); }

.ghost-btn {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 8px 6px;
  font-family: var(--font);
  font-size: 6px;
  line-height: 1.8;
  letter-spacing: 0.5px;
  color: var(--grey);
  background: transparent;
  border: 2px dashed var(--slate-l);
  cursor: pointer;
}
.ghost-btn b { color: var(--gold); font-size: 8px; }
.ghost-btn:hover:not(:disabled) { color: var(--bone); border-color: var(--grey-l); }
.ghost-btn:disabled { opacity: 0.45; cursor: not-allowed; }
#m-reno-btn { border-style: solid; border-color: var(--green-d); margin-top: 8px; }
#m-reno-btn:hover:not(:disabled) { border-color: var(--green); }

/* ---------------------------------------------------------------------------
   Boot screen
   --------------------------------------------------------------------------- */
#boot {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--soot);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.4s steps(4);
}
#boot.done { opacity: 0; pointer-events: none; }
#boot-inner { text-align: center; padding: 20px; max-width: 460px; }
#boot-title { font-size: 17px; color: var(--brick-l); line-height: 1.5; }
#boot-msg { font-size: 7px; color: var(--grey); margin-top: 18px; line-height: 1.8; }
#boot-bar {
  width: 200px;
  max-width: 100%;
  height: 12px;
  margin: 18px auto 0;
  border: 2px solid var(--slate-l);
  background: var(--slate-d);
}
#boot-fill { height: 100%; width: 0%; background: var(--sodium); transition: width 0.2s steps(4); }
#boot-hint {
  display: none;
  font-size: 6px;
  color: var(--brick-l);
  margin-top: 16px;
  line-height: 1.9;
}

/* ---------------------------------------------------------------------------
   Mobile
   --------------------------------------------------------------------------- */
@media (max-width: 860px) {
  :root { --hud-h: 50px; }

  #hud-brand { display: none; }
  .stat { min-width: 0; padding: 5px 7px; }
  .stat-value { font-size: 11px; }
  #stat-clock { display: none; }
  /* The forecast stays — it is the only thing in the HUD that tells you what
     is happening to the picture — but the wind and the UV go, because on a
     phone they are two more numbers on a line already fighting for room. */
  #stat-weather { min-width: 0; }
  .wx-sub { display: none; }

  .panel {
    width: 172px;
    max-height: 34vh;
    top: calc(var(--hud-h) + 6px);
  }
  .lb-row { font-size: 6px; }

  .pu-chip { min-width: 0; font-size: 6px; padding: 5px 7px; }
  .pu-chip .pu-name { max-width: 108px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  #corner-stack { max-width: 68vw; }
  .toast { font-size: 6px; padding: 7px 8px; }

  /* On a phone there is no empty side to park a panel on, so the listing goes
     back to being a bottom sheet — but a SHORT one, so the building it is
     about stays on screen above it. */
  #modal-backdrop { align-items: flex-end; justify-content: center; padding: 0; }
  #modal {
    position: relative;
    right: auto;
    top: auto;
    width: 100%;
    max-height: 62dvh;
    border-width: 3px 0 0;
    box-shadow: none;
    animation: sheetIn 0.2s steps(3);
  }
  @keyframes sheetIn { from { transform: translateY(100%); } }

  #m-left {
    width: 100%;
    display: flex;
    gap: 12px;
    align-items: flex-start;
  }
  #m-floorplan { width: 88px; height: 88px; }
  #m-specs { margin-top: 0; flex: 1; }
  .bid-value { font-size: 13px; }

  /* Bigger touch targets. */
  .step-btn { width: 52px; font-size: 15px; }
  #m-amount { font-size: 15px; padding: 14px 0; }
  .big-btn { padding: 17px 10px; }

  #help { padding: 20px 16px; }
  #help h1 { font-size: 15px; }

  #tags .tag { font-size: 7px; padding: 2px 4px 3px; }
  .tag .tag-owner { display: none; }
}

@media (max-width: 400px) {
  #badges { font-size: 6px; }
  .badge { font-size: 6px; padding: 4px 6px; }
}

/* Landscape phones: the HUD eats the whole screen otherwise. */
@media (max-height: 460px) and (orientation: landscape) {
  .panel { max-height: 40vh; width: 156px; }
  #ticker { display: none; }
  #corner-stack { bottom: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  #ticker-track { animation: none; padding-left: 8px; }
  .badge-live { animation: none; }
  *, *::before, *::after { animation-duration: 0.01ms !important; }
}

/* ===========================================================================
   SaaS chrome: plans, account, presence.
   Added after the game shipped, so it lives in its own block rather than
   threaded through the original rules.
   =========================================================================== */

#stat-online .stat-value { color: var(--green); }
#stat-online.stale .stat-value { color: var(--grey); }

/* --- Plan badge in the HUD ------------------------------------------------ */
.plan-btn {
  font-family: var(--font);
  font-size: 7px;
  padding: 8px 9px;
  min-height: 32px;
  border: 2px solid var(--slate-l);
  background: var(--slate-d);
  color: var(--grey-l);
  cursor: pointer;
  letter-spacing: 0.5px;
  white-space: nowrap;
}
.plan-btn:active { transform: translate(1px, 1px); }
.plan-btn.plan-tenant   { color: var(--grey-l); border-color: var(--slate-l); }
.plan-btn.plan-broker   { color: var(--cyan);   border-color: var(--cyan); }
.plan-btn.plan-slumlord { color: var(--gold);   border-color: var(--gold); background: rgba(255,215,94,0.08); }

.badge-offline { border-color: var(--sodium); color: var(--sodium); }

/* --- Leaderboard plan markers -------------------------------------------- */
.lb-row.plan-broker   .lb-name { color: var(--cyan); }
.lb-row.plan-slumlord .lb-name { color: var(--gold); }
.lb-row.plan-slumlord .lb-rank { color: var(--gold); }
.lb-row .lb-bot { color: var(--slate-l); font-size: 6px; }

/* --- Listing interest ---------------------------------------------------- */
.bid-views { font-size: 7px; color: var(--grey-l); text-align: right; }
.bid-views b { color: var(--sodium); }

/* --- Shared modal furniture ---------------------------------------------- */
#account-backdrop, #plans-backdrop {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(11, 12, 16, 0.86);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px;
  overflow-y: auto;
}
#account, #plans {
  background: var(--slate-d);
  border: 3px solid var(--slate-l);
  box-shadow: 0 0 0 3px var(--soot), 6px 6px 0 rgba(0,0,0,0.6);
  width: min(640px, 100%);
  max-height: calc(100dvh - 28px);
  overflow-y: auto;
  position: relative;
  padding: 22px 20px;
  animation: modalIn 0.16s steps(3);
}
#plans { width: min(880px, 100%); }

/* Sticky and floated, for the same reason #m-close is — every dialog that
   wears one of these scrolls its own body, and an absolutely positioned child
   of a scroll box goes up and off with the content. */
.modal-close {
  position: sticky;
  float: right;
  top: 8px;
  margin-left: 8px;
  touch-action: manipulation;
  font-family: var(--font);
  font-size: 9px;
  background: var(--slate);
  color: var(--grey-l);
  border: 2px solid var(--slate-l);
  width: 28px; height: 28px;
  cursor: pointer;
  z-index: 4;
}
.modal-close:hover { color: var(--brick-l); border-color: var(--brick-l); }

#account h2, #plans h2 {
  font-size: 13px;
  color: var(--sodium);
  margin: 0 0 16px;
  line-height: 1.5;
}

/* --- Account ------------------------------------------------------------- */
.acc-row { margin-bottom: 20px; }
.acc-row > label {
  display: block;
  font-size: 6px;
  color: var(--grey);
  margin-bottom: 8px;
  letter-spacing: 0.5px;
}
.acc-inline { display: flex; gap: 7px; align-items: stretch; flex-wrap: wrap; }
.acc-inline input {
  flex: 1;
  min-width: 0;
  font-family: var(--font);
  font-size: 8px;
  color: var(--bone);
  background: var(--soot);
  border: 2px solid var(--slate-l);
  padding: 11px 9px;
  outline: none;
}
.acc-inline input:focus { border-color: var(--gold); }
.acc-inline input:read-only { color: var(--grey); }

.small-btn {
  touch-action: manipulation;
  font-family: var(--font);
  font-size: 7px;
  padding: 11px 12px;
  background: var(--slate);
  color: var(--bone);
  border: 2px solid var(--slate-l);
  cursor: pointer;
  white-space: nowrap;
}
.small-btn:hover:not(:disabled) { border-color: var(--gold); color: var(--gold); }
.small-btn:active { transform: translate(1px, 1px); }
.small-btn:disabled { color: var(--grey); cursor: not-allowed; }
.small-btn.primary { background: var(--brick); border-color: var(--brick-l); color: var(--cream); }
.small-btn.primary:hover { background: var(--brick-l); color: var(--cream); }

.acc-hint { font-size: 6px; line-height: 1.8; color: var(--grey); margin: 9px 0 0; }
.acc-hint.locked { color: var(--sodium); }

/* --- Sign in ------------------------------------------------------------- */
/* Column, not a row: the provider names run long ("SIGN IN WITH GOOGLE") and
   two of them side by side wrap mid-word inside the modal's width. */
#acc-auth { display: flex; flex-direction: column; gap: 7px; align-items: stretch; }
.acc-auth-btn { text-align: center; }
/* A connected account reads as a fact with an escape hatch, not as a button:
   the handle takes the space and UNLINK sits at the end of it. */
.acc-ident {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 9px;
  background: var(--soot);
  border: 2px solid var(--slate-l);
}
.acc-ident-who {
  flex: 1;
  min-width: 0;
  font-size: 7px;
  line-height: 1.7;
  color: var(--bone);
  /* A display name is whatever its owner typed, up to and including no
     spaces at all. It gets an ellipsis rather than the modal. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.acc-ident-who b { color: var(--sodium); font-weight: normal; }

.acc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: 8px;
  margin-bottom: 20px;
}
.acc-stat {
  background: var(--soot);
  border: 2px solid var(--slate);
  padding: 10px 9px;
}
.acc-stat .k { display: block; font-size: 6px; color: var(--grey); }
.acc-stat .v { display: block; font-size: 11px; color: var(--gold); margin-top: 7px; }

#acc-plan {
  background: var(--soot);
  border: 2px solid var(--slate);
  padding: 11px;
  font-size: 7px;
  line-height: 1.8;
  color: var(--grey-l);
  margin-bottom: 9px;
}
#acc-plan b { color: var(--bone); }

#acc-error, #plans-note {
  font-size: 7px;
  line-height: 1.7;
  color: var(--brick-l);
  margin: 12px 0 0;
  padding: 9px;
  border: 2px solid var(--brick);
  background: rgba(168, 50, 43, 0.14);
}
#plans-note { color: var(--grey); border-color: var(--slate); background: var(--soot); }

/* --- Plans --------------------------------------------------------------- */
.plans-sub { font-size: 7px; color: var(--grey); margin: -8px 0 20px; line-height: 1.8; }

#plans-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 12px;
}
.plan-card {
  background: var(--soot);
  border: 2px solid var(--slate);
  padding: 16px 14px;
  display: flex;
  flex-direction: column;
}
.plan-card.current { border-color: var(--green); }
.plan-card.featured { border-color: var(--gold); }

.plan-card h3 { font-size: 11px; margin: 0; color: var(--bone); line-height: 1.5; }
.plan-card .plan-price { font-size: 17px; color: var(--gold); margin: 14px 0 4px; }
.plan-card .plan-price small { font-size: 7px; color: var(--grey); }
.plan-card .plan-tag { font-size: 6px; color: var(--sodium); line-height: 1.8; margin: 10px 0 14px; min-height: 32px; }
.plan-card ul { margin: 0 0 16px; padding: 0; list-style: none; flex: 1; }
.plan-card li {
  font-size: 6px;
  line-height: 1.9;
  color: var(--grey-l);
  padding: 5px 0 5px 13px;
  position: relative;
}
.plan-card li::before { content: '+'; position: absolute; left: 0; color: var(--green); }
.plan-card .plan-cta {
  width: 100%;
  font-family: var(--font);
  font-size: 8px;
  padding: 13px 8px;
  background: var(--brick);
  color: var(--cream);
  border: 2px solid var(--brick-l);
  border-bottom-width: 4px;
  cursor: pointer;
  line-height: 1.5;
}
.plan-card .plan-cta:hover:not(:disabled) { background: var(--brick-l); }
.plan-card .plan-cta:active:not(:disabled) { transform: translateY(2px); border-bottom-width: 2px; }
.plan-card .plan-cta:disabled { background: var(--slate); border-color: var(--slate-l); color: var(--grey); cursor: default; }

@media (max-width: 860px) {
  #stat-online { display: none; }
  #btn-plan { font-size: 6px; padding: 8px 6px; }
  #account, #plans { padding: 18px 14px; }
  #plans-grid { grid-template-columns: 1fr; }
  .acc-inline { flex-direction: column; }

  /* Touch targets. 44px is the floor for anything a thumb has to hit. */
  .small-btn { padding: 15px 12px; min-height: 44px; }
  .acc-inline input { padding: 14px 10px; min-height: 44px; font-size: 9px; }
  .modal-close { width: 44px; height: 44px; }
}


/* ---------------------------------------------------------------------------
   Portfolio panel
   With unlimited units on the top plan you need somewhere to see what you
   actually hold, and a way to get back to it on an endless street.
   --------------------------------------------------------------------------- */
#portfolio {
  top: auto;
  bottom: 34px;
  left: 6px;
  width: 218px;
  max-height: min(38vh, 300px);
}
#portfolio .panel-head b { color: var(--green); }

#pf-summary {
  font-size: 6px;
  line-height: 1.9;
  color: var(--grey);
  padding: 8px;
  border-bottom: 2px solid var(--slate);
}
#pf-summary b { color: var(--gold); }
#portfolio.collapsed #pf-summary { display: none; }

.pf-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 6px 4px;
  font-size: 6px;
  border-bottom: 1px solid rgba(74, 78, 99, 0.35);
  cursor: pointer;
}
.pf-row:last-child { border-bottom: none; }
.pf-row:hover { background: rgba(47, 191, 113, 0.12); }
.pf-name {
  flex: 1;
  color: var(--bone);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pf-bid { color: var(--gold); flex: none; }
.pf-empty { font-size: 6px; color: var(--grey); padding: 10px 6px; line-height: 1.9; }

@media (max-width: 860px) {
  /* Moved into #left-rail at boot (ui._layOutForPhone) — bottom-left is the
     chat dock's corner on a phone, and this panel was parked on top of the
     chat toggle. In the rail it is a third board under the other two, laid out
     by the same column and unable to overlap either. */
  /* `#left-rail .panel` already takes care of the layout; this is only the
     third panel's share of a column that was sized for two. */
  #left-rail #portfolio { max-height: 26vh; }
  .pf-row { padding: 9px 4px; }
}
@media (max-height: 460px) and (orientation: landscape) {
  #portfolio { display: none; }
}

/* The line the modal shows when a unit is held by one of the cameo landlords
   in cameos.js. Quiet, italic, and set off from the numbers above it — it is
   flavour, not information you can act on. */
.cameo-note {
  margin: -2px 0 10px;
  padding: 7px 9px;
  border-left: 3px solid var(--pink, #ff79b8);
  background: rgba(255, 121, 184, 0.08);
  color: var(--bone, #eef1f7);
  font-size: 11px;
  line-height: 1.45;
  font-style: italic;
  opacity: 0.92;
}
.cameo-note.hidden { display: none; }

/* The eight-dollar checkmark, on the leaderboard rows belonging to the cameo
   landlords in cameos.js. Sized to sit inside the row without pushing the
   points column around. */
.lb-check {
  flex: 0 0 auto;
  margin-left: 4px;
  color: var(--mta, #3f8ccc);
  font-size: 10px;
  line-height: 1;
}

/* ---------------------------------------------------------------------------
   SIGNS: the tooltip that follows the cursor over a readable sign, and the card
   behind the ones belonging to a cameo (see src/cameos.js).

   Both show the SAME PIXELS as the sign in the world: the marks are cropped
   straight out of the texture atlas at runtime by textures.js#tileDataURL, so
   there is still not one image file in this project.
   --------------------------------------------------------------------------- */

.sign-tip {
  position: fixed;
  z-index: 40;
  display: flex;
  gap: 9px;
  align-items: flex-start;
  max-width: 290px;
  padding: 8px 10px;
  background: rgba(20, 24, 36, 0.95);
  border: 2px solid var(--slate-l);
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  pointer-events: none;      /* it is a label, never a target */
  transform: translate(14px, 14px);
}
.sign-tip-icon {
  width: 32px;
  height: 32px;
  flex: none;
  image-rendering: pixelated;
}
.sign-tip-body { min-width: 0; }
.sign-tip-name {
  font-size: 9px;
  color: var(--gold);
  letter-spacing: 0.5px;
}
.sign-tip-handle {
  font-size: 8px;
  color: var(--cyan);
  margin-top: 3px;
}
.sign-tip-handle:empty { display: none; }
.sign-tip-blurb {
  font-size: 8px;
  line-height: 1.55;
  color: var(--grey-l);
  margin-top: 5px;
}
.sign-tip-more {
  font-size: 7px;
  color: var(--slate-l);
  margin-top: 6px;
}

#cameo-backdrop {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(11, 12, 16, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px;
  overflow-y: auto;
}
#cameo {
  position: relative;
  background: var(--slate-d);
  border: 3px solid var(--slate-l);
  box-shadow: 0 0 0 3px var(--soot), 6px 6px 0 rgba(0, 0, 0, 0.6);
  width: min(460px, 100%);
  max-height: calc(100dvh - 28px);
  overflow-y: auto;
  padding: 18px;
}
.cameo-head {
  display: flex;
  gap: 14px;
  align-items: center;
  margin-bottom: 14px;
}
.cameo-icon {
  width: 64px;
  height: 64px;
  flex: none;
  image-rendering: pixelated;
  background: var(--soot);
  border: 2px solid var(--slate);
  padding: 4px;
}
.cameo-shop {
  font-size: 13px;
  color: var(--gold);
  margin: 0 0 6px;
  letter-spacing: 0.5px;
}
.cameo-handle {
  font-size: 9px;
  color: var(--cyan);
  text-decoration: none;
  border-bottom: 1px dotted var(--cyan);
}
.cameo-handle:hover { color: var(--pink); border-color: var(--pink); }
.cameo-blurb {
  font-size: 9px;
  line-height: 1.7;
  color: var(--bone);
  margin: 0 0 16px;
  padding-left: 10px;
  border-left: 3px solid var(--pink);
}
.cameo-facts {
  margin: 0;
  font-size: 8px;
  line-height: 1.6;
}
.cameo-facts dt {
  color: var(--slate-l);
  letter-spacing: 0.6px;
  margin-top: 11px;
}
.cameo-facts dd {
  margin: 3px 0 0;
  color: var(--grey-l);
}
.cameo-foot {
  margin: 18px 0 0;
  font-size: 7px;
  line-height: 1.6;
  color: var(--slate-l);
}

/* ---------------------------------------------------------------------------
   PORTRAITS. Generated per handle by src/avatars.js, on the same 16x16 grid
   and out of the same 24 colours as everything else in the world, then blown
   up with image-rendering: pixelated. They are characters, not likenesses.
   --------------------------------------------------------------------------- */

.sign-tip-who {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-top: 4px;
}
.sign-tip-face {
  width: 16px;
  height: 16px;
  flex: none;
  image-rendering: pixelated;
}
.sign-tip-handle { margin-top: 0; }

.cameo-face {
  width: 80px;
  height: 80px;
  flex: none;
  image-rendering: pixelated;
  border: 2px solid var(--slate);
}
#cameo .cameo-icon {
  width: 44px;
  height: 44px;
  align-self: flex-end;
  margin-bottom: 4px;
}

.lb-face {
  width: 14px;
  height: 14px;
  flex: none;
  image-rendering: pixelated;
  margin-right: 2px;
}

.toast.with-face {
  display: flex;
  gap: 8px;
  align-items: flex-start;
}
.toast-face {
  width: 24px;
  height: 24px;
  flex: none;
  image-rendering: pixelated;
}

.cameo-note {
  display: flex;
  gap: 7px;
  align-items: flex-start;
}
.cameo-note-face {
  width: 20px;
  height: 20px;
  flex: none;
  image-rendering: pixelated;
}

/* Handle and homepage, side by side under the shop name on the cameo card. */
.cameo-links {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  align-items: baseline;
}
.cameo-site {
  font-size: 8px;
  color: var(--green);
  text-decoration: none;
  border-bottom: 1px dotted var(--green);
}
.cameo-site:hover { color: var(--gold); border-color: var(--gold); }

/* ===========================================================================
   ACHIEVEMENTS
   Sits on the right rail so it does not fight the leaderboard and portfolio,
   both of which own the left. Collapsed by default: thirty-six rows of joke is
   a thing you go and look at, not a thing that covers the street.
   ========================================================================= */
/* Both right-hand panels are laid out by #right-rail, not by their own fixed
   coordinates — see the note there. */
#ach-panel,
#news-panel {
  position: static;
  left: auto;
  right: auto;
  top: auto;
  width: 100%;
}
#ach-panel { max-height: min(46vh, 380px); }
#ach-panel .panel-head { color: var(--gold); }

.ach-row {
  display: flex;
  gap: 7px;
  align-items: flex-start;
  padding: 6px 5px;
  border-bottom: 1px solid rgba(95, 104, 128, 0.25);
}
.ach-row:last-child { border-bottom: none; }

/* The tick box. Empty and grey until earned, then gold and filled — readable
   at a glance without needing colour alone to carry it. */
.ach-mark {
  flex: none;
  width: 11px;
  height: 11px;
  margin-top: 1px;
  border: 1px solid var(--slate-l);
  color: var(--soot);
  font-size: 8px;
  line-height: 10px;
  text-align: center;
}
.ach-row.got .ach-mark { background: var(--gold); border-color: var(--gold); color: var(--soot); }

.ach-text { min-width: 0; flex: 1; }
/* Both are <span>, so they need to be told to stack — otherwise the name and
   the joke run together into one unreadable line. */
.ach-name {
  display: block;
  font-size: 7px;
  letter-spacing: 0.5px;
  color: var(--grey);
  overflow-wrap: anywhere;
}
.ach-row.got .ach-name { color: var(--gold); }
.ach-blurb {
  display: block;
  font-size: 6px;
  line-height: 1.55;
  color: var(--slate-l);
  margin-top: 3px;
  overflow-wrap: anywhere;
}
.ach-row.got .ach-blurb { color: var(--grey-l); }
.ach-row.locked .ach-blurb { font-style: italic; }

/* The unlock toast. Gold rail down the left so it reads as a different KIND of
   message from the rent/outbid toasts it shares the stack with. */
.toast.ach {
  border-color: var(--gold);
  border-left-width: 5px;
}
.toast.ach .toast-kicker {
  display: block;
  font-size: 6px;
  color: var(--sodium);
  letter-spacing: 1px;
  margin-bottom: 4px;
}

@media (max-width: 860px) {
  #right-rail { width: 200px; }
  #ach-panel { max-height: 36vh; }
  #news-panel { max-height: 30vh; }
}
@media (max-height: 460px) and (orientation: landscape) {
  #ach-panel { display: none; }
  #right-rail { width: 200px; }
}

/* Loose props advertise that they can be picked up. */
#scene.grabbable { cursor: grab; }
#scene.holding { cursor: grabbing; }

/* ===========================================================================
   THE ROOF — the owner's one permanent decision about a building
   ========================================================================= */
#m-roof-wrap { margin-top: 10px; }
#m-roof-view {
  font-size: 7px;
  line-height: 1.7;
  color: var(--grey);
  margin: 0 0 8px;
}
#m-roof-view b { color: var(--gold); }
#m-roof-picker { margin-bottom: 8px; }
#m-roof-picker .build-swatch { padding: 6px 7px; }
#m-roof-picker .build-swatch .chip {
  width: 14px;
  height: 14px;
  margin-right: 6px;
  vertical-align: middle;
  image-rendering: pixelated;
}
#m-roof-blurb {
  font-size: 6px;
  line-height: 1.8;
  color: var(--slate-l);
  margin: 0 0 8px;
  min-height: 11px;
}
#m-roof-error {
  font-size: 6px;
  line-height: 1.7;
  color: var(--brick-l);
  margin: 6px 0 0;
}
#m-roof-error.hidden { display: none; }

/* Vertical enlargement. Blue rather than gold, because it is not a
   renovation — it is a different kind of decision with a different kind of
   consequence, and the panel should not let the two blur together. */
#m-floor-btn {
  border-style: solid;
  border-color: var(--mta);
  margin-top: 8px;
}
#m-floor-btn:hover:not(:disabled) { border-color: var(--cyan); }

/* ===========================================================================
   THE PAPER — the archive of everything that already scrolled past
   Toasts live for four seconds and the ticker never stops moving, so anything
   you were not looking at when it happened was simply gone. This is the same
   stream, kept.
   ========================================================================= */
#news-panel { max-height: min(46vh, 380px); }
#news-panel .panel-head { color: var(--brick-l); }

.news-unread {
  display: inline-block;
  margin-left: 5px;
  padding: 1px 4px;
  background: var(--brick);
  color: var(--bone);
  font-size: 6px;
}

.news-row {
  display: block;
  padding: 6px 5px;
  border-bottom: 1px solid rgba(95, 104, 128, 0.25);
  font-size: 6px;
  line-height: 1.7;
  color: var(--bone);
}
.news-row:last-child { border-bottom: none; }
.news-when {
  display: block;
  color: var(--grey);
  letter-spacing: 0.5px;
  margin-bottom: 3px;
}
/* Same colour language the toasts use, so a loss reads as a loss in both. */
.news-row.win .news-when { color: var(--green); }
.news-row.lose .news-when { color: var(--brick-l); }
.news-row.chaos .news-when { color: var(--pink); }
.news-row.info .news-when { color: var(--slate-l); }
.news-empty { display: block; padding: 8px 5px; font-size: 6px; color: var(--grey); }

/* ===========================================================================
   LIVE EVENTS — breaking-news cards, bottom right
   Things worth walking to: the Rat King, somebody in a suit going past
   overhead. Each card is a button; clicking it flies the camera there.
   ========================================================================= */
#live-events {
  display: flex;
  flex-direction: column;
  gap: 5px;
  align-items: flex-end;
  pointer-events: none;
}

.live-card {
  pointer-events: auto;
  display: block;
  width: 232px;
  padding: 0;
  font-family: var(--font);
  text-align: left;
  cursor: pointer;
  /* NO background on the card itself. The feed below is a hole through to the
     game canvas, and a background here would sit behind it — which is exactly
     what made the "video" a rectangle of dark panel colour the first time. The
     opaque panel is painted on the header and the lower third instead. */
  background: none;
  border: 2px solid var(--brick);
  /* The red rail is the "breaking news" tell, and it is the one part that
     stays put while the rest of the card animates in. */
  border-left-width: 5px;
  color: var(--bone);
  animation: liveIn 0.22s steps(3);
}
.live-card:hover { border-color: var(--sodium); }
.live-card:active { transform: translate(1px, 1px); }

/* The camera is locked onto this one.
   The same colour as the tracking box drawn around the subject out in the
   world (world.js, showReticle) — that pairing is the only thing telling the
   player that the rectangle out there and this card are the same fact. */
.live-card.following {
  border-color: var(--brick-l);
  animation: liveLock 1.1s steps(2) infinite;
}
.live-card.following .live-kicker::after {
  content: ' ● LOCKED';
  color: var(--brick-l);
}
/* Locked on, the picture-in-picture is a small copy of what is already filling
   the screen — and it is the tall part of the card, so it is also most of what
   was sitting on top of the thing you went to look at. Collapse it. The card
   keeps its headline and its release control and stops being a wall. */
.live-card.following .live-feed {
  height: 0;
  min-height: 0;
  border-width: 0;
  overflow: hidden;
}
@keyframes liveLock {
  0%, 100% { box-shadow: 0 0 0 2px rgba(227, 120, 92, 0.55); }
  50% { box-shadow: 0 0 0 2px rgba(227, 120, 92, 0.12); }
}

.live-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 5px 7px 4px;
  background: var(--ui-bg);
}

/* ---- The picture ---------------------------------------------------------
   Deliberately EMPTY and transparent. What shows through it is the game's own
   canvas, which sits behind the whole HUD: Game.renderFeed() scissors a second
   render of the scene into exactly this rectangle every frame, through a
   second camera parked over whatever the card is reporting on. A CSS
   impression of a video would have been cheaper and would have looked it. */
.live-feed {
  position: relative;
  display: block;
  height: 96px;
  border-top: 1px solid var(--brick);
  border-bottom: 1px solid var(--brick);
  background: transparent;
  overflow: hidden;
}
/* Scanlines and a corner bracket over the top, so it reads as a feed rather
   than as a hole cut in the panel. */
.live-scan {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0.22) 0 1px,
      rgba(0, 0, 0, 0) 1px 3px
    );
  animation: liveFlicker 2.7s steps(2) infinite;
}
.live-scan::before,
.live-scan::after {
  content: '';
  position: absolute;
  width: 9px;
  height: 9px;
  border: 1px solid var(--sodium);
  opacity: 0.75;
}
.live-scan::before { top: 3px; left: 3px; border-right: 0; border-bottom: 0; }
.live-scan::after { bottom: 3px; right: 3px; border-left: 0; border-top: 0; }

.live-lower { display: block; padding: 5px 7px 6px; background: var(--ui-bg); }

.live-kicker {
  font-size: 6px;
  letter-spacing: 1px;
  color: var(--brick-l);
}
/* The dot that makes it read as a live feed rather than a notification. */
.live-kicker::before {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 5px;
  vertical-align: middle;
  background: var(--brick-l);
  animation: livePulse 1.1s steps(2) infinite;
}
.live-clock { font-size: 6px; color: var(--grey); letter-spacing: 1px; }
.live-title { display: block; font-size: 7px; line-height: 1.5; color: var(--gold); }
.live-sub { display: block; font-size: 6px; line-height: 1.5; color: var(--sodium); margin-top: 4px; }

@keyframes liveIn { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; } }
/* Going off the board. The card slides back the way it came in and takes its
   height with it, so the ones below it move up rather than jumping up — a card
   that is simply removed makes the whole stack snap, which is the one moment
   the player is definitely looking at the board. `forwards` holds the end
   state for the few frames between the animation finishing and the element
   actually being removed. */
.live-card.out {
  animation: liveOut 0.3s steps(4) forwards;
  pointer-events: none;
}
@keyframes liveOut {
  from { opacity: 1; transform: translateX(0); max-height: 240px; }
  to { opacity: 0; transform: translateX(14px); max-height: 0; }
}
@keyframes livePulse { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.15; } }
@keyframes liveFlicker { 0%, 94% { opacity: 0.8; } 95%, 100% { opacity: 1; } }

@media (max-width: 860px) {
  #corner-stack { left: 6px; right: 6px; bottom: 30px; max-width: none; align-items: stretch; }
  #live-events { align-items: stretch; }
  /* Still in the corner stack: a window dragged narrow after boot, where
     ui._layOutForPhone's one-shot IS_MOBILE read has already said desktop. */
  #corner-stack .live-card { width: auto; }
  #corner-stack .live-feed { height: 88px; }

  /* On a phone the cards are moved into the right rail at boot — see
     ui._layOutForPhone. Rail width, and a feed cut to little more than half
     height: it has to read as a live picture without being the screen. */
  #right-rail .live-card { width: auto; border-left-width: 4px; }
  #right-rail .live-head { padding: 3px 6px 2px; }
  #right-rail .live-lower { padding: 3px 6px 5px; }
  #right-rail .live-feed { height: 52px; }

  /* The toasts are moved into the rail at boot too (ui._layOutForPhone).
     #corner-stack goes full-bleed on a phone, so down there every toast was a
     band across the whole screen; up here one is a note in the margin. Rail
     width, so they line up with the panels above them rather than reading as a
     fourth thing. */
  #right-rail #toasts { align-items: stretch; }
  #right-rail .toast {
    font-size: 6px;
    line-height: 1.6;
    padding: 6px 7px;
    border-left-width: 4px;
  }
  #right-rail .toast-kicker { font-size: 5px; margin-bottom: 3px; }
}

/* ===========================================================================
   THE CART — the first flow, and the people running one
   ===========================================================================
   Three things live down here: the left rail that stacks the two boards, the
   prompt that appears when somebody hungry is within reach of your bike, and
   the chat dock. Everything is anchored above the ticker (26px) so nothing
   ever sits on the headline crawl.
   =========================================================================== */

/* The left column. Exactly the device #right-rail uses: two panels that would
   otherwise be independently fixed to the same corner and draw over each other
   the moment either grows a row. */
#left-rail {
  position: fixed;
  left: 6px;
  top: calc(var(--hud-h) + 8px);
  z-index: 25;
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 218px;
  max-height: calc(100dvh - var(--hud-h) - 150px);   /* dvh — see #right-rail */
  pointer-events: none;
}
/* min-height: 0 is what makes the rail's own max-height mean anything. A flex
   item will not shrink below its content by default, so three panels whose
   max-heights add up to more than the column just overflowed it — and the one
   on the end came down on the chat dock. Shrunk instead, and .panel-body was
   already a scroller, so what gives is the list rather than the layout. */
#left-rail > * { pointer-events: auto; min-height: 0; }
/* Static inside the rail — the flex column owns the placement now. */
#left-rail .panel {
  position: static;
  width: 100%;
  max-height: min(34vh, 270px);
}
#vendor-board .panel-head { color: var(--gold); }

.vb-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 5px 4px;
  font-size: 7px;
  border-bottom: 1px solid rgba(74, 78, 99, 0.35);
}
.vb-row:last-child { border-bottom: none; }
.vb-rank { color: var(--grey); width: 16px; flex: none; }
.vb-name {
  flex: 1;
  color: var(--bone);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vb-sold { color: var(--gold); flex: none; }
.vb-earned { color: var(--grey); font-size: 6px; flex: none; text-align: right; }
.vb-row.me { background: rgba(255, 226, 137, 0.14); }
.vb-row.me .vb-name { color: var(--gold); }
.vb-row.moved { animation: flashRow 0.6s steps(3); }
.vb-empty { color: var(--grey); font-size: 6px; line-height: 1.7; padding: 6px 4px; }

/* ---------------------------------------------------------------------------
   The sell prompt
   Only ever on screen while Vendor.target is set, which is the one moment the
   button means anything.
   --------------------------------------------------------------------------- */
/* In the dock's flex column, not fixed to the viewport: the log under it
   grows and shrinks with the conversation, and anything anchored to a fixed
   offset ends up drawn straight through it. */
#buy-prompt, #sky-prompt { align-self: center; }
#buy-prompt.hidden, #sky-prompt.hidden { display: none; }

/* On a desktop the cart prompt is centred on the SCREEN, not on the dock.
   It lives in #chat-dock so that on a phone it can sit directly over the input
   — but the dock is a 430px column pinned to the bottom left, so "centred"
   inside it put the prompt a third of the way across a wide window, over the
   buildings rather than under them. Lifted out of the flow instead and parked
   above the key legend, which owns the strip at 32px. */
@media (min-width: 861px) {
  #buy-prompt, #sky-prompt {
    position: fixed;
    left: 50%;
    bottom: 58px;
    transform: translateX(-50%);
  }
}
/* The roof prompt is the cart prompt in a different colour: gold is a thing
   for sale, and this one costs nothing and sells nothing. */
#sky-btn { background: var(--cyan); }
#sky-btn:hover { background: var(--bone); }
#sky-sub { color: var(--teal-d); }

#buy-btn, #sky-btn {
  touch-action: manipulation;
  font-family: var(--font);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 10px 16px;
  font-size: 9px;
  color: var(--soot);
  background: var(--gold);
  border: 2px solid var(--cream);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.55);
  cursor: pointer;
  animation: sellPop 0.18s steps(3);
}
#buy-btn:hover { background: var(--cream); }
#buy-btn:active, #sky-btn:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 rgba(0,0,0,0.55); }
#buy-btn:disabled { background: var(--slate); color: var(--grey); cursor: default; }
#buy-sub, #sky-sub { font-size: 6px; color: var(--brown-d); letter-spacing: 1px; }
@keyframes sellPop { from { opacity: 0; transform: translateY(6px) scale(0.96); } }

/* ---------------------------------------------------------------------------
   Turbo

   Hidden outright on anything with a keyboard: there, turbo is SHIFT and a
   button would be screen bought for nothing. The media query at the bottom of
   this block is what puts it on a phone.
   --------------------------------------------------------------------------- */
#btn-turbo {
  display: none;
  position: fixed;
  right: 10px;
  /* Above the chat dock's row and clear of the bottom edge, on the side the
     chat dock does not use. */
  bottom: 76px;
  z-index: 34;
  width: 74px;
  height: 74px;
  font-family: var(--font);
  font-size: 8px;
  letter-spacing: 1px;
  color: var(--soot);
  background: var(--sodium);
  border: 3px solid var(--cream);
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55);
  /* A held control, so it must never select text, summon a magnifier, or fire
     the 300ms tap delay. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: pointer;
}
#btn-turbo.is-on {
  background: var(--brick-l);
  color: var(--cream);
  /* Pressed in, not just recoloured: the shadow is the only cue that survives
     a thumb sitting on top of the button. */
  transform: translate(3px, 3px);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0.55);
}

@media (max-width: 860px) {
  #btn-turbo { display: block; }
}

/* ---------------------------------------------------------------------------
   Controls legend

   Bottom CENTRE, in the one strip of screen nothing else claims: the chat
   dock owns the bottom left, the toasts and event cards own the bottom right,
   and the ticker owns the 26px below. Hidden under 1024px, where those two
   meet in the middle and there is no gap left to sit in — and hidden on a
   phone besides, which has no keyboard to caption.
   --------------------------------------------------------------------------- */
#controls {
  position: fixed;
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 27;
  display: flex;
  gap: 12px;
  align-items: center;
  /* A reminder, not a control. Every key here also has a button. */
  pointer-events: none;
  user-select: none;
  white-space: nowrap;
}
.ctl {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 6px;
  letter-spacing: 1px;
  /* Same white as the caps. The labels sit on bare street rather than on a
     panel, so grey read as dirt on the road at this size. */
  color: var(--bone);
  /* And a hard one-pixel drop behind them, because the road underneath is not
     one colour: a white word over a lit window is a white word over white. */
  text-shadow: 1px 1px 0 var(--soot);
}
.ctl b {
  display: inline-block;
  min-width: 13px;
  padding: 3px 4px;
  text-align: center;
  font-weight: normal;
  font-size: 6px;
  color: var(--bone);
  background: var(--ui-bg);
  border: 1px solid var(--slate-l);
  /* The cap has its own background; the shadow inherited from .ctl would just
     smear the letter against it. */
  text-shadow: none;
  /* Inherited from .ctl, where it spaces the labels; inside a cap it just
     pushes a single letter off centre. */
  letter-spacing: 0;
}
/* The caps of one group read as one key row, so they touch and share an edge.
   -5px is the 4px flex gap cancelled, and then one more for the shared
   border — keep the two numbers together if either moves. */
.ctl b + b { margin-left: -5px; }
.ctl b.wide { min-width: auto; }
/* Held. Set from ui.js on keydown; see _wireControls(). */
.ctl b.is-down {
  color: var(--soot);
  background: var(--gold);
  border-color: var(--cream);
}

@media (max-width: 1024px) {
  #controls { display: none; }
}

/* ---------------------------------------------------------------------------
   Chat
   --------------------------------------------------------------------------- */
#chat-dock {
  position: fixed;
  /* Bottom LEFT, not centred. Centred it sat directly under the cart prompt
     and over the middle of the street, which is where the buildings you are
     trying to look at are. Aligned to the same 6px gutter the panels use. */
  left: 6px;
  bottom: 32px;
  z-index: 33;
  width: min(430px, 82vw);
  /* The two heights the toggle is placed against — see #chat-toggle.
     --row-h is the WHOLE chat row, which is two lines now: the faces and the
     input, plus this element's own padding and border. */
  --row-h: 84px;
  --toggle-h: 30px;
  display: flex;
  flex-direction: column;
  gap: 5px;
  pointer-events: none;
}
#chat-dock > * { pointer-events: auto; }

/* Explicit order, because the column is not in document order. Bottom up: the
   input, the cart prompt directly over it — it is the button you are reaching
   for while a cart is in range, so it wants to be nearer the thumb than the
   log is — then the log. */
#chat-log { order: 1; }
#buy-prompt { order: 2; }
#chat-row { order: 3; }

/* The toggle is OUT of the column, and this is the whole reason why.
   The dock is anchored by its bottom edge and grows upwards, so a toggle in
   the flow rides up and down by the height of everything under it — the log
   filling, the input appearing, a cart prompt arriving. A button you press
   twice in a row must not move between the presses. Reserving a slot tall
   enough for all of that worked and put the button a third of the way up an
   empty screen, which is worse.
   So it is placed instead: one fixed step above the input, whose height is the
   only thing it depends on and is the one thing in the dock that never
   changes. #chat-row keeps its space when the chat is hidden (visibility, not
   display) so the step is the same either way, and carries a margin-top the
   width of this button so nothing in the column is ever laid out underneath
   it. */
#chat-toggle {
  position: absolute;
  left: 0;
  bottom: calc(var(--row-h) + 5px);
  min-height: var(--toggle-h);
}
#chat-row {
  height: var(--row-h);
  margin-top: calc(var(--toggle-h) + 5px);
}

/* Off by default — see state.chatOn and ui.showChat. The toggle and the cart
   prompt are NOT part of what hides: one is the way back in, and the other has
   nothing to do with chat. */
#chat-dock.chat-off #chat-log { display: none; }
/* Hidden, not removed: the toggle is placed one step above this row, and the
   step has to be the same step whether the chat is up or not. */
#chat-dock.chat-off #chat-row { visibility: hidden; }

#chat-toggle {
  align-self: flex-start;
  /* The root is `touch-action: none` so the street can be dragged. That is the
     wrong value for a button — `manipulation` is the one that says "this is a
     tap", and it is what stops a thumb that rolls two pixels from being read
     as the start of a gesture and losing the press. */
  touch-action: manipulation;
  font-family: var(--font);
  font-size: 6px;
  letter-spacing: 1px;
  padding: 6px 9px;
  background: var(--ui-bg);
  color: var(--grey-l);
  border: 2px solid var(--slate-l);
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
  cursor: pointer;
}
#chat-toggle:hover { color: var(--bone); border-color: var(--grey); }
#chat-toggle.is-on { color: var(--gold); border-color: var(--gold); }
#chat-toggle:active { transform: translate(1px, 1px); }

/* The last few lines, so a message is not lost the moment its bubble fades.
   Reversed column so new lines push up from the input rather than the list
   jumping every time one arrives. */
#chat-log {
  display: flex;
  flex-direction: column-reverse;
  gap: 3px;
  max-height: 96px;
  overflow: hidden;
  pointer-events: none;
}
.chat-line {
  font-size: 6px;
  line-height: 1.7;
  padding: 4px 7px;
  background: var(--ui-bg);
  border-left: 3px solid var(--slate-l);
  color: var(--grey-l);
  align-self: flex-start;
  max-width: 100%;
  animation: toastIn 0.2s steps(3);
}
.chat-line b { color: var(--sodium); }
.chat-line.mine { border-left-color: var(--gold); }
.chat-line.mine b { color: var(--gold); }
.chat-line.system { border-left-color: var(--cyan); color: var(--cyan); }

/* Two lines: the six faces across the top, the line you type on underneath.
   One line meant the input was whatever was left after six buttons and a SAY —
   about eleven characters on a phone, so you could not read back what you had
   just written. Wrapped rather than nested, so the markup stays one row and
   #emote-row simply claims a full line of it. The two lines split the row's
   height between them (align-content: stretch), which is why --row-h below is
   the only number that has to change. */
#chat-row {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 4px;
  background: var(--ui-bg);
  border: 2px solid var(--ui-edge);
  padding: 4px;
  box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
}
#emote-row { flex: 0 0 100%; display: flex; gap: 2px; }
.emote-btn {
  touch-action: manipulation;
  font-family: var(--font);
  font-size: 11px;
  line-height: 1;
  padding: 3px 4px;
  background: var(--slate-d);
  border: 1px solid var(--slate);
  color: var(--bone);
  cursor: pointer;
}
.emote-btn:hover { background: var(--slate); }
.emote-btn:active { transform: translate(1px, 1px); }
#chat-input {
  flex: 1;
  min-width: 0;
  font-family: var(--font);
  font-size: 7px;
  padding: 4px 6px;
  background: var(--soot);
  border: 1px solid var(--slate);
  color: var(--bone);
  outline: none;
}
#chat-input:focus { border-color: var(--sodium); }
#chat-input::placeholder { color: var(--slate-l); }

/* ---------------------------------------------------------------------------
   Name plates over the bikes
   Projected from 3D every frame by src/rider.js, exactly like the price tags.
   --------------------------------------------------------------------------- */
#rider-layer {
  position: fixed;
  inset: 0;
  z-index: 22;
  pointer-events: none;
  overflow: hidden;
}
.rider-plate {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  white-space: nowrap;
}
.rp-name {
  font-size: 6px;
  line-height: 1;
  padding: 3px 5px;
  color: var(--soot);
  /* The rider's jacket colour, set per plate. Same hash on every client, so
     the bike and the label always agree about who this is. */
  background: var(--jacket, var(--sodium));
  border: 1px solid rgba(0,0,0,0.5);
}
.rp-bubble {
  font-size: 7px;
  line-height: 1.5;
  max-width: 190px;
  padding: 5px 7px;
  white-space: normal;
  text-align: center;
  color: var(--soot);
  background: var(--cream);
  border: 2px solid var(--soot);
  box-shadow: 2px 2px 0 rgba(0,0,0,0.45);
}
.rp-bubble.emote {
  font-size: 15px;
  padding: 3px 6px;
  line-height: 1.2;
}
.rider-plate.mine .rp-bubble { background: var(--gold); }

/* The arrow under YOUR plate, pointing down at your own bike.
   Drawn with borders rather than a glyph: the UI font is a pixel face and has
   no guarantee of carrying an arrow character, and a missing glyph renders as
   a box or nothing at all. */
.rp-arrow {
  display: none;
  width: 0;
  height: 0;
  margin-top: -2px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--pink);
}
/* Yours is the only plate that names itself. No plate behind it — it is a
   pointer at your own bike, not a label somebody else has to read, and a
   filled box follows the camera around the whole time. The hard shadow is
   what keeps pink legible over a lit window or a pale wall. */
.rider-plate.mine .rp-name {
  background: none;
  border: 0;
  padding: 0;
  color: var(--pink);
  letter-spacing: 1px;
  text-shadow: 1px 1px 0 var(--soot), -1px 1px 0 var(--soot), 1px -1px 0 var(--soot), -1px -1px 0 var(--soot);
}

@media (max-width: 860px) {
  #left-rail { width: 190px; max-height: calc(100dvh - var(--hud-h) - 190px); }
  #chat-dock { width: calc(100vw - 12px); left: 6px; bottom: 30px; }
  #chat-log { max-height: 62px; }
  /* 104 = 8 padding + 4 border + two 44px lines + the 4px between them. */
  #chat-dock { --row-h: 104px; --toggle-h: 44px; }

  /* 16px, and not a pixel less: iOS zooms the whole page in when you focus a
     text field smaller than that, and there is no gesture on this page that
     zooms it back out. It was 6px — the size of the placeholder rather than of
     anything anybody typed — so it was unreadable AND it was the trap. */
  #chat-input { font-size: 16px; padding: 0 8px; }
  .chat-line { font-size: 7px; padding: 5px 8px; }
  /* 44px is the floor for anything a thumb has to hit. The emote faces were
     29px tall and the toggle 36px, which is the difference between a button
     and a button you keep missing. */
  .emote-btn { font-size: 15px; flex: 1; min-width: 0; }
  /* Height comes from --toggle-h above, so the button and the gap the toggle
     is placed into can never disagree. */
  #chat-toggle { font-size: 8px; padding: 0 18px; }

  /* The X is a thumb target down here, and the sheet header keeps clear of a
     button half again as wide as the desktop one. */
  #m-close, #build-close { width: 44px; height: 44px; font-size: 12px; }


  /* The cart prompt goes LEFT, not floating in the middle of the street:
     centred in a full-width dock it sat out over the road with the buildings
     behind it. Where it sits in the column is set by `order` further up. */
  #buy-prompt { align-self: flex-start; }
}

/* ---------------------------------------------------------------------------
   The receipt
   The share card that comes up on the last bite. Modelled on the account
   modal, narrower, and with the hot dog itself at the top — it is a receipt
   for lunch, not a settings screen.
   --------------------------------------------------------------------------- */
#share-backdrop {
  position: fixed;
  inset: 0;
  z-index: 56;
  background: rgba(11, 12, 16, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px;
}
#share-backdrop.hidden { display: none; }

#share-card {
  position: relative;
  width: min(400px, 94vw);
  background: var(--ui-bg);
  border: 2px solid var(--sodium);
  box-shadow: 5px 5px 0 rgba(0,0,0,0.6);
  padding: 20px 18px 16px;
  text-align: center;
  animation: sharePop 0.24s steps(4);
}
@keyframes sharePop { from { opacity: 0; transform: translateY(10px) scale(0.95); } }

#share-dog,
#share-dog-flat {
  display: block;
  margin: 0 auto 10px;
  image-rendering: pixelated;
}
/* Twice the backing resolution of its CSS box, so the voxels stay crisp on the
   nearest-neighbour upscale rather than resolving into mush. */
#share-dog { width: 136px; height: 80px; }
#share-dog-flat { width: 96px; height: 42px; }
#share-dog.hidden, #share-dog-flat.hidden { display: none; }
#share-card h2 {
  font-size: 13px;
  color: var(--gold);
  margin: 0 0 14px;
  line-height: 1.5;
}

#share-who {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: 12px;
}
#share-face {
  width: 40px;
  height: 40px;
  image-rendering: pixelated;
  border: 2px solid var(--slate-l);
  flex: none;
}
#share-face.hidden { display: none; }
#share-line {
  margin: 0;
  font-size: 7px;
  line-height: 1.8;
  color: var(--bone);
  text-align: left;
}
#share-line b { color: var(--cyan); }

/* The exact text that will be posted, shown before anything is posted. Nobody
   should have to find out what they tweeted by reading their own timeline. */
#share-tweet {
  margin: 0 0 14px;
  padding: 9px 10px;
  font-size: 6px;
  line-height: 1.9;
  color: var(--grey-l);
  background: var(--soot);
  border-left: 3px solid var(--cyan);
  text-align: left;
}

#share-x { width: 100%; }
#share-skip { margin-top: 8px; }

@media (max-width: 860px) {
  #share-card { padding: 16px 12px 12px; }
  #share-card h2 { font-size: 11px; }
}

/* --- The owner's handle, as a link -----------------------------------------
   The handle is the whole point of a cameo building — it says whose this is —
   so it is the thing you click. Underlined on hover only: a permanent
   underline in a 9px pixel face turns the descenders to mush. */
.own-name .x-link {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted var(--slate-l);
}
.own-name .x-link:hover {
  color: var(--cyan);
  border-bottom-color: var(--cyan);
}
.own-name.me .x-link { color: var(--green); }

/* --- The file picker -------------------------------------------------------
   A native <input type="file"> renders as "Choose File / No file chosen" in
   the OS font, which is the one piece of chrome in the whole game that looks
   like a form from 1998. The input itself stays in the DOM (it has to — a file
   dialog can only be opened by a real click on a real input) but is made
   invisible and stretched over its own label, so the pixel button IS the hit
   target rather than something that forwards a synthetic click. */
.board-file {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.board-file input[type="file"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.board-file .file-btn {
  font-family: var(--font);
  font-size: 6px;
  letter-spacing: 0.5px;
  padding: 9px 12px;
  background: var(--soot);
  border: 2px solid var(--slate);
  color: var(--grey-l);
  pointer-events: none;   /* the input above it takes the click */
}
.board-file:hover .file-btn { border-color: var(--sodium); color: var(--sodium); }
.board-file .file-name {
  font-size: 6px;
  color: var(--grey);
  pointer-events: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 55%;
}

/* --- Space to let ----------------------------------------------------------
   One row per storey. The row has to answer three questions at a glance —
   which floor, whose it is, what you can do about it — so it is a three-column
   grid rather than a sentence: the floor label and the action stay in the same
   place down the whole list, and only the middle column varies. */
#m-slots-wrap { padding: 12px 14px; border-bottom: 2px solid var(--slate); }
.slots-intro { font-size: 6px; color: var(--grey); line-height: 1.7; margin: 0 0 8px; }

.slot-row {
  display: grid;
  grid-template-columns: 46px 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  border-top: 1px solid var(--slate);
}
.slot-row:first-of-type { border-top: 0; }

.slot-floor { font-size: 6px; color: var(--grey); letter-spacing: 0.5px; }
.slot-what { font-size: 7px; color: var(--bone); line-height: 1.6; word-break: break-word; }
.slot-what .who { color: var(--sodium); }
.slot-what .rent { color: var(--green); }
.slot-what a { color: var(--cyan); text-decoration: none; border-bottom: 1px dotted var(--slate-l); }
.slot-what a:hover { border-bottom-color: var(--cyan); }
/* The one the player holds. Marked, because on a tall building it is otherwise
   one identical row among seven. */
.slot-row.is-mine { background: rgba(120, 200, 255, 0.06); }
.slot-row.is-mine .slot-floor { color: var(--cyan); }

.slot-actions { display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.slot-btn {
  font-family: var(--font);
  font-size: 6px;
  letter-spacing: 0.5px;
  padding: 7px 9px;
  background: var(--soot);
  border: 2px solid var(--slate);
  color: var(--grey-l);
  cursor: pointer;
  white-space: nowrap;
}
.slot-btn:hover { border-color: var(--slate-l); color: var(--bone); }
.slot-btn.primary { border-color: var(--sodium); color: var(--sodium); }
.slot-btn.primary:hover { background: var(--sodium); color: var(--soot); }
.slot-btn:disabled { opacity: 0.4; cursor: default; }

/* The owner's price box. Narrow on purpose — it takes cents-as-dollars and
   nothing else, and a wide field invites a sentence. */
.slot-price {
  font-family: var(--font);
  font-size: 6px;
  width: 54px;
  padding: 6px 5px;
  background: var(--soot);
  border: 2px solid var(--slate);
  color: var(--bone);
  text-align: right;
}

/* The row you arrived at by clicking its board on the street. A signpost, not
   a state — it fades out, because a permanent mark would leave two rows both
   claiming to be the one you picked. */
.slot-row.is-focus {
  outline: 2px solid var(--sodium);
  outline-offset: -2px;
  animation: slotFound 2.2s ease-out;
}
@keyframes slotFound {
  0%, 45% { background: rgba(240, 160, 60, 0.16); }
  100% { background: transparent; }
}
