/*
 * The booth is a sonar console, not a web page: one monospace family, tabular numerals so a
 * figure that changes every second never reflows the row beside it, and a palette borrowed from
 * the thing being watched — abyss, phosphor, and the amber and coral of the game's own warnings.
 *
 * It is stared at for two days, so the boldness is in the palette and the density rather than in
 * motion. Nothing here loops.
 */

:root {
  --abyss: #03070c;
  --hull: #070f17;
  --deck: #0b1620;
  --deck-lit: #0f202c;
  --rule: #16303e;
  --rule-bright: #1f4557;

  --phosphor: #5fe3f0;
  --phosphor-dim: #2c7d8b;
  --kelp: #7fd88a;
  --amber: #ffb454;
  --coral: #ff5b5b;

  --bone: #cfe3ec;
  --ash: #698a99;
  --ghost: #3d5764;

  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "IBM Plex Mono", "Cascadia Mono", Menlo,
    Consolas, "DejaVu Sans Mono", monospace;

  --row: 26px;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

body {
  background:
    radial-gradient(120% 80% at 50% -20%, #0d2532 0%, transparent 60%),
    linear-gradient(180deg, var(--hull) 0%, var(--abyss) 70%);
  background-attachment: fixed;
  color: var(--bone);
  font: 400 13px/1.4 var(--mono);
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

/* Grain, as one inline SVG rather than a request. Two days of flat colour reads as a dead
   monitor; this is the difference between a screen and a surface. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

body > *:not(.grain) {
  position: relative;
  z-index: 1;
}

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- the instrument bar ---------------------------------------------------------------- */

.bar {
  display: flex;
  align-items: stretch;
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule-bright);
}

.bar > * {
  background: var(--deck);
  padding: 8px 16px;
}

.bar__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
}

.bar__mark {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 1.5px solid var(--phosphor);
  box-shadow: 0 0 0 3px #5fe3f01f;
  flex: none;
}

.bar__title {
  text-transform: uppercase;
  letter-spacing: 0.22em;
  font-size: 12px;
  color: var(--bone);
  white-space: nowrap;
}

.bar__sub {
  color: var(--ash);
  font-size: 11px;
  letter-spacing: 0.08em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.readout {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-width: 86px;
}

.readout__label {
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ash);
}

.readout__value {
  font-size: 15px;
  line-height: 1;
  color: var(--phosphor);
}

.readout--clock {
  min-width: 132px;
}

.readout--clock .readout__value {
  font-size: 27px;
  letter-spacing: -0.01em;
  text-shadow: 0 0 18px #5fe3f04d;
}

.readout--state .readout__value {
  font-size: 12px;
  text-transform: lowercase;
  letter-spacing: 0.1em;
  color: var(--bone);
}

.readout--state[data-state="Running"] .readout__value {
  color: var(--kelp);
}

.readout--state[data-state="Paused"] .readout__value {
  color: var(--amber);
}

/* The link light. A caster needs to know at a glance whether the numbers are the match or the
   last thing the match said before the feed went. */
.link {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 190px;
  max-width: 300px;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ash);
}

.link .link__text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.link__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ghost);
  flex: none;
}

.link[data-state="live"] {
  color: var(--kelp);
}

.link[data-state="live"] .link__dot {
  background: var(--kelp);
  box-shadow: 0 0 10px var(--kelp);
}

/* Amber rather than coral: all three come back on their own. A full booth (LMS-26) is waiting for
   a slot, and nothing the operator types will make one appear any sooner. */
.link[data-state="connecting"],
.link[data-state="retrying"],
.link[data-state="full"] {
  color: var(--amber);
}

.link[data-state="connecting"] .link__dot,
.link[data-state="retrying"] .link__dot,
.link[data-state="full"] .link__dot {
  background: var(--amber);
}

.link[data-state="refused"],
.link[data-state="mismatched"],
.link[data-state="blocked"] {
  color: var(--coral);
}

.link[data-state="refused"] .link__dot,
.link[data-state="mismatched"] .link__dot,
.link[data-state="blocked"] .link__dot {
  background: var(--coral);
  box-shadow: 0 0 10px var(--coral);
}

/* ---- the phase rule ------------------------------------------------------------------- */

/* The countdown is the server's (ADR-0015), and it arrives once a second. Transitioning the width
   over that second is what turns twelve steps into a bar that moves. */
.phasebar {
  height: 3px;
  background: #0a1a24;
}

.phasebar > span {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--phosphor-dim), var(--phosphor));
  box-shadow: 0 0 12px #5fe3f066;
  transition: width 1s linear;
}

/* ---- the zone strip ------------------------------------------------------------------- */

.zone {
  display: flex;
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.zone__cell {
  flex: 1;
  background: var(--hull);
  padding: 6px 16px;
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}

.zone__cell--right {
  flex: 0 0 auto;
  min-width: 150px;
}

.zone__label {
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ash);
  flex: none;
}

.zone__value {
  color: var(--bone);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.zone__cell--right .zone__value[data-danger="true"] {
  color: var(--coral);
}

/* ---- the roster ----------------------------------------------------------------------- */

/* Two panes: the roster takes what is left and the feed is a fixed column beside it, each
   scrolling on its own. The bar and the zone strip above stay put, which is the whole reason the
   document itself does not scroll. */
main {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 380px;
}

.pane {
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
}

.pane--side {
  border-left: 1px solid var(--rule);
  background: #050c12;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* The map is the thing a stream cuts to, so it takes the top of the column at its natural square
   and the feed takes whatever is left under it. */
.map {
  position: relative;
  flex: none;
  aspect-ratio: 1;
  border-bottom: 1px solid var(--rule-bright);

  background-color: #02070b;
  overflow: hidden;
}

/* The repository's own bathymetry render, from the game's own terrain octrees — the same image the
   players' in-game minimap draws (docs/config/README.md). Nothing third-party.

   Flipped, and that is the whole reason it is a pseudo-element rather than a background on .map:
   make-minimap walks `z = j * metres - span / 2` down the rows, so the file's row 0 is the *south*
   edge, while mapview.js projects north up to match the minimap on every player's screen. The
   transform follows the players' HUD and the image is turned to suit it, not the other way round. */
.map::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("/map/terrain.png");
  background-size: 100% 100%;
  transform: scaleY(-1);
}

.map canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* North, said rather than assumed. A map that is the wrong way up looks perfectly plausible, so the
   one thing worth labelling is which way up it is. */
.map__north {
  position: absolute;
  top: 5px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 9px;
  letter-spacing: 0.3em;
  color: var(--phosphor-dim);
  text-shadow: 0 1px 3px #000;
  pointer-events: none;
}

.map__scale {
  position: absolute;
  right: 7px;
  bottom: 5px;
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ash);
  text-shadow: 0 1px 3px #000;
  pointer-events: none;
}

.feed {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

/* Under about 1200px the feed stops being a column and becomes what is under the table, because a
   240px roster is not a roster. */
@media (max-width: 1200px) {
  body {
    overflow: auto;
  }

  main {
    grid-template-columns: minmax(0, 1fr);
  }

  .pane,
  .pane--side {
    overflow: visible;
  }

  .pane--side {
    border-left: none;
    border-top: 1px solid var(--rule);
  }

  /* Stacked, the map keeps its square and is centred instead of widened. A non-square panel would
     stretch the terrain and — worse — turn the zone's rings into ellipses, because a radius is one
     fraction of a square world (WorldMap.Fraction). */
  .map {
    max-height: 46vh;
    max-width: 46vh;
    margin: 0 auto;
    width: 100%;
  }
}

.roster {
  width: 100%;
  border-collapse: collapse;
}

.roster thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--deck);
  border-bottom: 1px solid var(--rule-bright);
  padding: 6px 10px;
  text-align: left;
  font-weight: 400;
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ash);
  white-space: nowrap;
}

.roster td {
  padding: 0 10px;
  height: var(--row);
  border-bottom: 1px solid #0d1c26;
  white-space: nowrap;
  color: var(--bone);
}

.col-num,
.col-pos {
  text-align: right;
}

.roster td.col-num,
.roster td.col-pos {
  text-align: right;
}

.col-meter {
  width: 96px;
}

.col-pos {
  width: 120px;
}

.col-pack {
  width: 68px;
  text-align: right;
}

.roster tbody tr {
  background: linear-gradient(90deg, #0a141c 0%, #08111800 40%);
}

.roster tbody tr:hover {
  background: var(--deck-lit);
}

/* Alive first, and told apart by more than order: a name in phosphor is in the match. */
.name {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  letter-spacing: 0.02em;
}

.name__pip {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--kelp);
  flex: none;
}

.row--out .name__pip {
  background: var(--coral);
  box-shadow: 0 0 8px var(--coral);
}

/* The row a caster is about to talk about. A left rule rather than a fill, because a filled row at
   25 rows is a wall of colour and this has to stay readable next to it. On the first cell rather
   than on the row: a box-shadow on a <tr> under border-collapse is drawn by some browsers and not
   by others. */
.roster tbody .row--out td[data-cell="name"] {
  box-shadow: inset 3px 0 0 var(--coral);
}

.row--dead {
  color: var(--ghost);
  background: repeating-linear-gradient(
    -45deg,
    #060d13,
    #060d13 6px,
    #08111a 6px,
    #08111a 12px
  ) !important;
}

.row--dead .name {
  text-decoration: line-through;
  text-decoration-color: #2a3f4b;
}

.row--dead .name__pip {
  background: var(--ghost);
  box-shadow: none;
}

/* A vital is a number the eye reads and a bar the eye scans. Both, in one cell, on one line. */
.meter {
  display: flex;
  align-items: center;
  gap: 8px;
}

.meter__figure {
  width: 24px;
  text-align: right;
  flex: none;
}

.meter__track {
  flex: 1;
  height: 4px;
  background: #0e2029;
  overflow: hidden;
}

.meter__fill {
  display: block;
  height: 100%;
  background: var(--phosphor-dim);
  transition: width 1s linear;
}

[data-level="ok"] .meter__fill {
  background: var(--phosphor);
}

[data-level="low"] .meter__fill {
  background: var(--amber);
}

[data-level="critical"] .meter__fill {
  background: var(--coral);
}

/* Specific enough to beat `.roster td`, which sets the resting colour of every cell. Without the
   `.roster` here a critical oxygen figure stayed bone, which is the one number on the row that has
   to be read across a booth. */
.roster td[data-level="low"],
.roster [data-level="low"] .meter__figure {
  color: var(--amber);
}

.roster td[data-level="critical"],
.roster [data-level="critical"] .meter__figure {
  color: var(--coral);
}

.roster td.absent {
  color: var(--ghost);
}

.roster td.zone-cell[data-inside="false"] {
  color: var(--coral);
}

/* Last, so a row the match is finished with is grey whatever its last numbers were. */
.roster tbody .row--dead [data-level] .meter__fill {
  background: #24343d;
}

.roster tbody .row--dead td,
.roster tbody .row--dead td[data-level],
.roster tbody .row--dead td.absent,
.roster tbody .row--dead [data-level] .meter__figure {
  color: var(--ghost);
}

/* The pack: a count that opens the stacks under the row. Twenty-five inventories cannot all be on
   screen, and the one being asked about always can. */
.pack {
  appearance: none;
  background: none;
  border: 1px solid var(--rule-bright);
  color: var(--bone);
  font: inherit;
  font-variant-numeric: tabular-nums;
  padding: 1px 7px;
  cursor: pointer;
}

.pack:hover {
  border-color: var(--phosphor);
  color: var(--phosphor);
}

.pack[aria-expanded="true"] {
  border-color: var(--phosphor);
  background: #0e2b33;
  color: var(--phosphor);
}

.pack:focus-visible {
  outline: 1px solid var(--phosphor);
  outline-offset: 2px;
}

.stacks {
  display: none;
}

.stacks--open {
  display: table-row;
}

.stacks td {
  height: auto;
  padding: 8px 10px 10px 29px;
  background: #060e14;
  border-bottom: 1px solid var(--rule);
}

.stacks__list {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin: 0;
  padding: 0;
  list-style: none;
  white-space: normal;
}

.stacks__item {
  border: 1px solid #143544;
  padding: 1px 8px;
  font-size: 12px;
  color: var(--bone);
}

.stacks__item b {
  color: var(--phosphor);
  font-weight: 400;
}

.stacks__none {
  color: var(--ghost);
}

.empty {
  margin: 0;
  padding: 26px 16px;
  color: var(--ash);
  max-width: 62ch;
}

.empty[hidden] {
  display: none;
}

/* ---- the token gate ------------------------------------------------------------------- */

.gate::backdrop {
  background: #02060ae0;
}

.gate {
  border: 1px solid var(--rule-bright);
  background: var(--deck);
  color: var(--bone);
  font: inherit;
  max-width: 46ch;
  padding: 26px 28px;
  box-shadow: 0 30px 80px #000c;
}

.gate h1 {
  margin: 0 0 12px;
  font-size: 13px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--phosphor);
}

.gate p {
  margin: 0 0 16px;
  color: var(--ash);
  line-height: 1.65;
}

.gate code {
  color: var(--bone);
}

.gate input {
  width: 100%;
  background: var(--abyss);
  border: 1px solid var(--rule-bright);
  color: var(--phosphor);
  font: inherit;
  padding: 9px 11px;
  letter-spacing: 0.1em;
}

.gate input:focus-visible {
  outline: none;
  border-color: var(--phosphor);
}

.gate__why {
  margin: 10px 0 0;
  color: var(--coral);
  min-height: 1.4em;
}

.gate__why:empty {
  margin: 0;
}

.gate button {
  margin-top: 16px;
  appearance: none;
  background: var(--phosphor);
  border: none;
  color: var(--abyss);
  font: inherit;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  padding: 9px 22px;
  cursor: pointer;
}

.gate button:hover {
  background: #8bf0fb;
}

::selection {
  background: var(--phosphor);
  color: var(--abyss);
}

::-webkit-scrollbar {
  width: 11px;
  height: 11px;
}

::-webkit-scrollbar-track {
  background: var(--abyss);
}

::-webkit-scrollbar-thumb {
  background: #163342;
  border: 3px solid var(--abyss);
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
  }
}

/* ---- the feed ------------------------------------------------------------------------- */

.feed__head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 14px;
  background: var(--deck);
  border-bottom: 1px solid var(--rule-bright);
}

.feed__title,
.feed__count {
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.feed__title {
  color: var(--ash);
}

.feed__count {
  color: var(--phosphor-dim);
}

.feed__list {
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  flex: 1;
  min-height: 0;
}

.feed__empty {
  margin: 0;
  padding: 18px 14px;
  color: var(--ash);
  line-height: 1.6;
}

.feed__empty[hidden] {
  display: none;
}

/* Newest first, so the thing that just happened is on screen without anybody scrolling for it. */
.line {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  gap: 10px;
  padding: 4px 14px;
  border-bottom: 1px solid #0b1721;
}

.line__at {
  color: var(--ghost);
  font-size: 11px;
  text-align: right;
  padding-top: 1px;
}

.line__text {
  color: var(--bone);
  overflow-wrap: anywhere;
}

/* Which match a line belongs to, said once above the run of them rather than on every line: a push
   can carry the end of one match while its snapshot already shows the next. */
.line__match {
  grid-column: 1 / -1;
  color: var(--phosphor-dim);
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  padding: 8px 0 4px;
  border-top: 1px solid var(--rule);
  margin-top: 2px;
}

.line:first-child .line__match {
  border-top: none;
  margin-top: 0;
  padding-top: 2px;
}

.line__match[hidden] {
  display: none;
}

/* An elimination is the line the stream cuts to, so it is the only one with a fill behind it. */
.line[data-tone="out"] {
  background: linear-gradient(90deg, #2a0f14 0%, #0a0f16 70%);
  box-shadow: inset 3px 0 0 var(--coral);
}

.line[data-tone="out"] .line__text {
  color: #ffd9d9;
}

.line[data-tone="phase"] {
  box-shadow: inset 3px 0 0 var(--phosphor-dim);
}

.line[data-tone="phase"] .line__text {
  color: var(--phosphor);
}

.line[data-tone="zone"] .line__text {
  color: var(--amber);
}

.line[data-tone="ring"] .line__text {
  color: var(--kelp);
}

.line[data-tone="quiet"] .line__text {
  color: var(--ash);
}

.line[data-tone="match"] .line__text {
  color: var(--bone);
}
