/*
  BOILERBROKERS - city page, top-down.

  THE ART is pixel, THE INTERFACE is not. A page where every control is blocky
  and set in a pixel font reads cheap and is hard to read on a phone. The street
  is the star; the type and layout stay clean so the street carries the page.

  Building names are HTML text, never baked into the artwork. Pixel lettering at
  this size cannot be generated. The first generated club sign came back
  reading "Colore". HTML text stays sharp at any pixel density.

  The lower third of the artwork is deliberately empty. Sign, button, facts and
  dialogue all live there. Text over a busy street is unreadable, and someone
  arriving from a raid post has about five seconds to work out what this is.
*/

:root {
  --ink: #2a2c33;
  --ink-dim: #6b6e77;
  /*
    EVERY COLOUR IS SAMPLED FROM city.png. NONE WERE CHOSEN FROM OUTSIDE IT.

    Two earlier versions failed the same way. A wooden board with a green button
    borrowed the language of a farming page; the dark steel plate with amber
    accents that replaced it borrowed the language of every other crypto page.
    Both were stuck on top of the city rather than grown out of it, and that
    always shows.

    Each colour below now has its origin in the artwork itself:

      --line   #34363d  the outline of EVERY object in this city, without one
                         exception
      --enamel  #f5f3e9  the kiosk awning
      --stone    #ded5d1  the bank stone and the cream building
      --red   #d5635d  the shop awnings
      --yellow  #fde403  the taxis, the loudest colour on that street

    The result is an interface that reads as signage hanging on that street
    rather than a panel floating above it. And because taxi yellow is used for
    exactly ONE thing, the call to action is the brightest object on screen
    without being enlarged or given a glow.
  */
  --line: #34363d;
  --line-deep: #141d33;
  --enamel: #f5f3e9;
  --stone: #ded5d1;
  --stone-shade: #a1909b;
  --red: #d5635d;
  --red-deep: #963441;
  --yellow: #fde403;
  --yellow-deep: #b99f02;
  --asphalt: #7f8385;
  /*
    Two pixel faces, and the split between them is deliberate. Press Start 2P is
    very wide with rigid line height: excellent for one big word, unreadable for
    a sentence. Silkscreen is far tighter and still legible at 11px.

    Long sentences inside the modal stay in an ordinary face. A pixel font
    across a four line paragraph wastes the time of somebody who only wants to
    know how to enter, and that is the only reason they opened the modal.
  */
  --display: "Press Start 2P", ui-monospace, monospace;
  --pixel: "Silkscreen", ui-monospace, monospace;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0; height: 100%; overflow: hidden;
  background: var(--asphalt);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

/* --------------------------------------------------------------------- bar */
/* NO BAR ACROSS THE TOP.
   A dark full width bar is the single most website-like element there is, and it
   cuts the city off exactly at the skyline, so what you see is a page with a
   picture under it rather than a place. With it gone the artwork reaches the top
   edge of the screen and all that remains are two objects hanging on that
   street. The container stays for simple layout but catches no clicks except on
   its contents. */
.bar {
  position: fixed; inset: 0 0 auto; z-index: 40;
  display: flex; align-items: flex-start; justify-content: space-between;
  padding: 0 16px;
  pointer-events: none;
}
.bar > * { pointer-events: auto; }

/* margin-left:auto rather than justify-content:flex-end on .bar. The bar used
   space-between when a sign hung on the left; with that gone, a single child
   under space-between falls to the LEFT, which is the opposite of what it looks
   like the rule should do. Pushing from the child itself survives whether or
   not anything is ever put back on the other side. */
.right {
  display: flex; align-items: center; gap: 10px;
  margin-top: 15px; margin-left: auto;
}
.icon {
  display: grid; place-items: center;
  width: 38px; height: 38px;
  border: 3px solid var(--line);
  background: var(--enamel); color: var(--ink);
  text-decoration: none; font-size: 15px;
  box-shadow: 5px 5px 0 rgba(20, 29, 51, .32);
}
.icon:hover { background: var(--yellow); }

/* Taxi yellow, and only here.
   It is the loudest colour in the city, so as long as nothing else uses it this
   button is the brightest object on screen without being enlarged, given a
   gradient, or wrapped in a glow. */
.cta {
  cursor: pointer;
  background: var(--yellow); color: var(--ink);
  border: 3px solid var(--line);
  border-radius: 0;
  font-family: var(--pixel); font-weight: 700; letter-spacing: .06em;
  box-shadow: 5px 5px 0 rgba(20, 29, 51, .32);
  transition: transform .08s ease, box-shadow .08s ease;
}
.cta:hover { background: #ffee4d; }
.cta:active { transform: translate(3px, 3px); box-shadow: 2px 2px 0 rgba(20, 29, 51, .32); }
.cta.small { font-size: 13px; padding: 10px 15px; }
.cta.big { font-size: 19px; padding: 17px 34px; }

/* Mint now, sitting in the panel before there is anywhere to send anyone.

   Shown rather than hidden, because people look for this button and its absence
   reads as a project that has not thought about its own sale. But visibly inert
   rather than a live-looking link: a mint button that goes somewhere unexpected
   is the exact shape of the scam these buyers have been trained to fear, and
   somebody caught once by a fake one will not press the real one later. */
.mintnow { display: block; margin: 22px 0 6px; text-align: center; text-decoration: none; }
.cta.off {
  background: var(--stone); color: var(--ink-dim);
  cursor: not-allowed; box-shadow: 5px 5px 0 rgba(20, 29, 51, .18);
}
.cta.off:hover { background: var(--stone); }
.cta.off:active { transform: none; box-shadow: 5px 5px 0 rgba(20, 29, 51, .18); }

/* ------------------------------------------------------------------- world */
.world { position: fixed; inset: 0; overflow: hidden; }
.stage { position: absolute; inset: 0; }

/*
  Square tiles fill a square frame. Every empty-corner problem the isometric
  version had came from a diamond that can never cover a rectangle; here the
  ground and the window are the same shape, so COVER just works.
*/
/*
  The zoom is pinned to the BAND OF ARTWORK that has to be visible, not to a
  number picked by eye: rows y 90..920 of 1440, being the row of buildings, the
  road, and enough open ground for the title plate. The scale then falls out on
  its own:

    band y 0..668, height 668 px of 1024
    city height = 1024/668              = 1.53 x window height
    city width  = 1.844 x city height   = 2.83 x window height
    shift up      0/1024                = 0% of city height

  The band was widened from 580 to 690 px because at 580 almost no front
  pavement was visible: nowhere for anyone to walk, and all five figures ended
  up below the fold.

  Every number here changed with the artwork: it is 1888x1024 now, not
  2560x1440, and its buildings sit far closer to the top edge - so close that the
  band starts at the very first row. The band is the row of shopfronts, the main
  road, the park block and the second road.

  To change the framing, change those two numbers and nothing else. The rest
  follows.
*/
.city {
  position: absolute; left: 50%; top: 0;
  transform: translate(-50%, 0);
  width: max(100vw, calc(100dvh * 2.83));
  aspect-ratio: 1888 / 1024;
}
.city img {
  display: block; width: 100%; height: 100%;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.layer { position: absolute; inset: 0; z-index: 3; }

/* Tree canopies are copied to their own layer and stacked ABOVE the people
   walking past. In a top down view leaves really are above the ground, so
   anyone walking under them should be covered by them. While the city was one
   flat image the figures were always drawn last and walked through the trees.

   The overlay is stamped from the map export's own tree objects, at the
   positions its manifest gives. It used to be separated by colour, on the basis
   that leaves are brighter than grass. That is not true of this artwork: its
   park lawn is the brightest green on the whole image and the crowns are
   darker, so the same rule would have lifted the lawn and left the trees, and
   the page would have shown people walking underneath the grass. */
.canopy {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
}

/* -------------------------------------------------------------- open doors */
/* The hit area IS the building, and nothing is drawn on it at all - no outline
   while pointing, no marker while idle. What tells you a door answers is the
   cursor and the plate that appears above it. */
.door {
  position: absolute; z-index: 2;
  padding: 0; margin: 0; border: 0; background: none;
  cursor: pointer; -webkit-appearance: none; appearance: none;
}
.door:focus { outline: none; }

/* The name rides above ITS OWN building. On a shared line it pointed at the row
   and not at any door in it. */
.door .tag {
  position: absolute; left: 50%; bottom: calc(100% + 10px);
  transform: translateX(-50%) translateY(4px);
  white-space: nowrap; pointer-events: none;
  opacity: 0; transition: opacity .1s steps(2), transform .1s steps(2);
}
.door:hover .tag, .door:focus-visible .tag {
  opacity: 1; transform: translateX(-50%) translateY(0);
}
.door .tag b {
  font-family: var(--pixel); font-size: clamp(10px, .8vw, 13px);
  padding: 4px 10px; background: var(--yellow); color: var(--ink);
  border: 2px solid var(--line); box-shadow: 3px 3px 0 rgba(20, 29, 51, .35);
}


/* --------------------------------------------------------------- the broker */
/* Traffic sits on the same layer as the people: above the road, below the tree
   canopies. A car that passed OVER a crown would be a car driving along the
   tops of the trees. */
.car {
  position: absolute; z-index: 1;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  image-rendering: pixelated;
  will-change: left;
}

.broker {
  position: absolute; z-index: 1;
  background-image: url("assets/broker.png");
  background-repeat: no-repeat;
  image-rendering: pixelated;
  will-change: transform, background-position;
}

/* ------------------------------------------------------------------- front */
.front {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 20;
  display: flex; flex-direction: column; align-items: center; gap: 20px;
  width: min(600px, 92vw);
  text-align: center;
}

/* The plate is built in CSS, not generated. Pixel lettering at this size always
   comes back as nonsense: the first generated club neon read "Colore". */
/* A riveted plate, not a wooden board. Square corners and a hard shadow: pixel
   art loses its edge under anything soft. */
.plate {
  position: relative;
  width: 100%;
  padding: 28px 32px 24px;
  background: var(--enamel);
  border: 4px solid var(--line);
  box-shadow: 9px 9px 0 rgba(20, 29, 51, .34);
}
.rivet {
  position: absolute; width: 8px; height: 8px; border-radius: 50%;
  background: var(--line);
}
.rivet.tl { left: 10px; top: 10px; }
.rivet.tr { right: 10px; top: 10px; }
.rivet.bl { left: 10px; bottom: 10px; }
.rivet.br { right: 10px; bottom: 10px; }

.plate h1 {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(15px, 2.6vw, 32px);
  letter-spacing: .02em; line-height: 1.15;
  color: var(--ink);
}
.plate .sub {
  margin: 16px 0 0;
  font-family: var(--pixel);
  font-size: clamp(10px, 1.3vw, 14px);
  letter-spacing: .02em; color: #6b6e77;
}


/* ------------------------------------------------------------------- sheet */
.scrim {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(20, 29, 51, .72); backdrop-filter: blur(3px);
}
.scrim[hidden] { display: none; }

/*
  A PIXEL FRAME, not a rounded card.

  One flat border reads as a web dialog. Three rings of alternating dark and
  enamel, each 4px, read as the chunky bevel a 1980s interface was drawn with -
  and they cost nothing: box-shadow spread rings do it without a border image,
  so it stays crisp at any zoom and there is no asset to keep in step.

  The corners are square on purpose. A radius is the one detail that gives away
  a pixel interface instantly, because no pixel grid can draw a smooth curve at
  this size.
*/
.sheet {
  position: relative; width: min(440px, 100%);
  max-height: calc(100dvh - 40px); overflow-y: auto;
  background: var(--enamel); border: 0; border-radius: 0;
  padding: 26px 24px 22px;
  box-shadow:
    0 0 0 4px var(--line),
    0 0 0 8px var(--enamel),
    0 0 0 12px var(--line),
    12px 12px 0 rgba(20, 29, 51, .45);
}

/*
  The pop is stepped, not eased.

  A smooth ease is what every modal on the web does, and next to artwork drawn
  on a 32px grid it looks borrowed from somewhere else. steps(5) snaps the panel
  in over five frames, which is how a sprite would have done it.
*/
@keyframes doorPop {
  from { transform: translateY(10px) scale(.92); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
@keyframes scrimIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.scrim:not([hidden]) { animation: scrimIn .12s steps(3) both; }
.scrim:not([hidden]) .sheet:not([hidden]) { animation: doorPop .16s steps(5) both; }

/* Anyone who has asked their machine to stop moving things gets the panel
   without the pop. The information is the point; the animation is not. */
@media (prefers-reduced-motion: reduce) {
  .scrim:not([hidden]),
  .scrim:not([hidden]) .sheet:not([hidden]) { animation: none; }
  .door .tag { transition: none; }
}

.close {
  position: absolute; top: 10px; right: 12px;
  background: none; border: 0; cursor: pointer;
  color: var(--ink); font-size: 24px; line-height: 1; padding: 4px 8px;
}
.close:hover { color: var(--ink); }

.kicker { margin: 0; font-family: var(--pixel); font-size: 11px; letter-spacing: .17em; text-transform: uppercase; color: var(--red-deep); }
.sheet h2 { margin: 8px 0 10px; font-family: var(--display); font-size: 15px; line-height: 1.4; }
.lead { margin: 0 0 18px; font-size: 13px; line-height: 1.55; color: var(--ink-dim); }
.lead strong { color: var(--ink); }

.steps { list-style: none; margin: 0 0 20px; padding: 0; display: grid; gap: 8px; }
.steps li {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; border: 2px solid var(--line); border-radius: 0;
  background: rgba(52, 54, 61, .07); font-size: 13px;
}
.steps .num {
  flex: 0 0 20px; height: 20px; display: grid; place-items: center;
  border-radius: 50%; background: var(--line); color: var(--enamel);
  font-size: 10px; font-weight: 700;
}
.steps .what { flex: 1 1 auto; color: var(--ink-dim); }
.steps .what b { color: var(--ink); font-weight: 700; }
.steps .go {
  flex: 0 0 auto; cursor: pointer;
  font-family: var(--pixel);
  font-size: 12px; font-weight: 700; letter-spacing: .02em;
  color: var(--ink); text-decoration: none;
  border: 2px solid var(--line); border-radius: 0; padding: 5px 10px;
  background: var(--enamel);
}
.steps .go:hover { background: var(--yellow); }
/* A step that is waiting reads as not-yet, not as broken: the row dims and its
   button shows the seconds left rather than its label, so the count is where
   the eye already is. */
.steps-note { margin: 0 0 10px; }

.steps li.waiting { opacity: .5; }
.steps li.waiting .go {
  cursor: not-allowed; background: var(--stone);
  min-width: 62px; text-align: center;
}
.steps li.waiting .go:hover { background: var(--stone); }
.steps li.done { border-color: var(--red-deep); }
.steps li.done .num { background: var(--yellow); color: var(--ink); }
.steps li.done .go { opacity: .5; }

.label {
  display: block; margin: 0 0 6px;
  font-family: var(--pixel);
  font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-dim);
}
/* The "@" stands in its own cell on the left rather than floating on top of the
   input.

   The old version placed it position:absolute and pushed the text across with
   padding-left: 26px, and that padding lost. The input[type="text"] rule below
   it uses the shorthand padding with exactly the same specificity, so the one
   written later won and padding-left reverted to 12px. The result was "@" and
   the word "username" fighting for the same spot.

   With flex there is no number that can override another: the "@" cell takes its
   own width and the input fills what is left. */
.field {
  display: flex; align-items: stretch;
  background: #fff;
  border: 2px solid var(--line); border-radius: 0;
  margin-bottom: 14px; overflow: hidden;
}
.field .prefix {
  display: flex; align-items: center; padding: 0 11px;
  font: 500 14px/1 var(--mono); color: var(--ink-dim);
  background: rgba(52, 54, 61, .09);
  border-right: 2px solid var(--line);
  user-select: none;
}
.field input[type="text"] {
  flex: 1; min-width: 0; margin: 0;
  background: none; border: 0; border-radius: 0;
}
.field input[type="text"]:focus { outline: none; }
.field:focus-within { outline: 3px solid var(--yellow); outline-offset: 0; }

input[type="text"] {
  width: 100%; background: #fff;
  border: 2px solid var(--line); border-radius: 0; color: var(--ink);
  font: 500 14px/1 var(--mono); padding: 12px; margin-bottom: 14px;
}
input[type="text"]:focus { outline: 3px solid var(--yellow); outline-offset: 0; }
input[type="text"]::placeholder { color: #9a9ca3; }

.error { margin: -6px 0 12px; font-size: 12px; color: var(--red-deep); font-weight: 700; }
/* Success and failure share one slot, told apart by colour. Two separate boxes
   would mean one of them is always empty and the layout jumps every time the
   state changes. */
.error.ok { color: #1f6b34; }
.error[hidden] { display: none; }

.submit {
  width: 100%; cursor: pointer;
  background: var(--yellow); color: var(--ink);
  border: 3px solid var(--line); border-radius: 0;
  font: 700 14px/1 var(--pixel); letter-spacing: .05em;
  padding: 15px;
  box-shadow: 5px 5px 0 rgba(20, 29, 51, .3);
}
.submit:disabled {
  background: var(--stone); color: #7e8189;
  box-shadow: none; cursor: not-allowed;
}
.submit:not(:disabled):hover { background: #ffee4d; }
.submit:not(:disabled):active { transform: translate(3px, 3px); box-shadow: 2px 2px 0 rgba(20, 29, 51, .3); }

.fineprint { margin: 12px 0 0; font-size: 11px; line-height: 1.5; color: var(--ink-dim); text-align: center; }

/* ------------------------------------------------------------------- small */
@media (max-width: 760px) {
  .front { top: 50%; gap: 14px; }
  .city { width: max(250vw, calc(100dvh * 2.3)); transform: translate(-50%, 0); }
}

/* A short table of facts inside a panel. Not the docs' <table>: at 440px wide a
   real table forces its columns to fight, and the term wraps to two lines while
   the value sits alone. A definition list stacks cleanly at any width. */
.spec { margin: 0 0 18px; display: grid; gap: 6px; }
.spec > div {
  display: flex; gap: 12px; align-items: baseline;
  padding: 7px 10px; background: var(--stone);
  border: 2px solid var(--line);
}
.spec dt {
  flex: 0 0 auto; min-width: 116px;
  font-family: var(--pixel); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--red-deep);
}
.spec dd { margin: 0; flex: 1 1 auto; font-size: 13px; color: var(--ink); }
