/* The client's look, following the desktop original: a near-black blue
   ground, green accents, and a console that fills the window. */

:root {
  color-scheme: dark;

  --bg: #0b0f14;
  --panel: #161b22;
  --titlebar: #1a2129;
  --rule: #232b35;

  --fg: #d8d8d8;
  --dim: #7a8290;
  --accent: #3dcf44;
  --gold: #d8a629;
  --error: #ff5f56;
  /* The prompt is the client's `lblue`, which its RGB packing puts at cyan. */
  --prompt: #00ffff;
  /* The original's `iBlue`, which is what it prints an emote in. Its
     constants are packed BGR, so `&HE99A9C` is this. */
  --chat-emote: #9c9ae9;

  --gutter: 4px;
  --prespace: 40px;
}

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  display: grid;
  /* Title bar, the consoles (the prompt is inside them), status bar. The
     windows are `position: fixed` and take no row of their own: they float
     over the consoles, which is what makes them windows. */
  grid-template-rows: auto 1fr auto;
  /* `minmax(0, 1fr)` rather than the default `auto`: the window is the whole
     of the page, so a row wide enough to overflow it has to be made to fit
     rather than pushing the others off the right edge -- which, with the
     `overflow: hidden` below, is off the screen for good. The title bar is
     the row that would: its account is right-aligned, and on a phone it was
     the part that went. */
  grid-template-columns: minmax(0, 1fr);
  background: var(--bg);
  color: var(--fg);
  font-family: Verdana, "DejaVu Sans", system-ui, sans-serif;
  font-size: 13px;
  overflow: hidden;
}

/* ---- title bar ------------------------------------------------------- */

#titlebar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.6rem;
  background: var(--titlebar);
  border-bottom: 1px solid var(--rule);
  font-weight: 700;
}

#status-dot {
  width: 9px;
  height: 9px;
  flex: 0 0 auto;
  border-radius: 1px;
}
#status-dot.offline {
  background: var(--error);
}
#status-dot.connecting {
  background: var(--gold);
}
#status-dot.online {
  background: var(--accent);
}

#brand {
  flex: 0 0 auto;
  padding-right: 0.6rem;
  border-right: 1px solid var(--rule);
  color: var(--accent);
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* A phone has room for the account and not much else, so the brand goes
   first -- the page's own title says the same thing. */
@media (max-width: 34rem) {
  #brand {
    display: none;
  }
}

/* The connection state is short and stays whole; the name beside it is the
   long one -- twenty characters, at the server's limit -- and is what gives
   way when the bar has not room for both. */
#status-text {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* The round trip, which is a number that changes every few seconds: it sits
   in the console face so the digits do not shift the things beside it as it
   goes from 9 ms to 140, and it is given the width of the longest reading
   worth reading so that it does not shift them either. */
#ping {
  flex: 0 0 auto;
  min-width: 4.5em;
  padding-left: 0.5rem;
  border-left: 1px solid var(--rule);
  font-family: "Courier New", "Liberation Mono", monospace;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--dim);
}
#ping.good {
  color: var(--accent);
}
#ping.fair {
  color: var(--gold);
}
#ping.poor,
#ping.lost {
  color: var(--error);
}

/* A phone loses it along with the brand: the account and the sign-in state
   are what the bar is for, and a latency reading is not worth the row. */
@media (max-width: 34rem) {
  #ping {
    display: none;
  }
}

/* ---- the account ------------------------------------------------------ */

/* Signed out this is one button; signed in it is a name and a way out. The
   menu hangs from it, so the box is what the menu is measured against. */
#account {
  position: relative;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
  /* Weighted so that a bar too narrow for everything takes it out of the
     name -- which ellipsizes -- before the connection state, which is short
     and is the one thing the bar is for. */
  flex-shrink: 100;
}

#account-label {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--dim);
  font-weight: 400;
}
#account-label b {
  color: var(--gold);
}

/* Both wear the status bar's dress: furniture, not something to press by
   accident. */
#logout,
#account-toggle {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
  padding: 0.1rem 0.45rem;
  font-size: 11px;
  background: transparent;
  border-color: transparent;
  color: var(--dim);
}
/* `display` above would otherwise outrank the attribute, leaving whichever
   of the two the bar is not showing laid out beside the one it is. */
#logout[hidden],
#account-toggle[hidden] {
  display: none;
}

#logout:hover,
#account-toggle:hover,
#account-toggle[aria-expanded="true"] {
  color: var(--gold);
  border-color: var(--rule);
}

/* Three bars, drawn rather than typed: U+2630 is missing from half the faces
   this page falls back to. `currentColor` carries the hover with it. */
.hamburger {
  width: 12px;
  height: 9px;
  background:
    linear-gradient(currentColor 0 0) 0 0 / 100% 1.5px no-repeat,
    linear-gradient(currentColor 0 0) 0 50% / 100% 1.5px no-repeat,
    linear-gradient(currentColor 0 0) 0 100% / 100% 1.5px no-repeat;
}

#account-menu {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  /* Above the comm log it hangs over. */
  z-index: 10;
  width: 15rem;
  display: grid;
  gap: 0.5rem;
  padding: 0.6rem;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 3px;
  box-shadow: 0 8px 20px rgb(0 0 0 / 50%);
}
/* `display` above would otherwise outrank the attribute. */
#account-menu[hidden] {
  display: none;
}

#login {
  display: grid;
  gap: 0.4rem;
  font-weight: 400;
}

#remember-label {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--dim);
  cursor: pointer;
  white-space: nowrap;
}

#account-links {
  display: grid;
  gap: 0.3rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--rule);
  font-weight: 400;
}
#account-links a {
  color: var(--dim);
  text-decoration: none;
}
#account-links a:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* The rule below dresses text fields; a checkbox wants none of it. */
#login input[type="checkbox"] {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  width: auto;
  accent-color: var(--accent);
  cursor: pointer;
}

input:not([type="checkbox"]),
button {
  background: #0e1319;
  color: var(--fg);
  border: 1px solid var(--rule);
  padding: 0.25rem 0.4rem;
  font: inherit;
  font-weight: 400;
  border-radius: 2px;
}

input::placeholder {
  color: var(--dim);
}

button {
  cursor: pointer;
  font-weight: 700;
}
button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* ---- the windows ------------------------------------------------------ */

/* Everything that is not a console is a window: the communications log, the
   file explorer, chat, mail, the editor and the library. They float over the
   consoles rather than taking room from them, and `window.ts` sets where
   each one sits -- so the only geometry here is the frame itself.

   The padding is that frame, and it is load-bearing: it is what makes the
   outermost few pixels the window's own box rather than whatever is inside,
   which is how a grab at the edge is told from a grab at a scrollbar sitting
   against it. `--win-frame` and `FRAME` in `window.ts` are the same number. */
.window {
  --win-frame: 6px;

  position: fixed;
  /* The browser centres a `<dialog>` by itself, and these are placed by
     hand. */
  margin: 0;
  inset: auto;
  z-index: 30;

  display: grid;
  /* Title bar, body, and whatever the window puts along the bottom. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-height: 0;
  padding: var(--win-frame);
  /* The frame is chrome and the contents are not, so they are different
     grounds -- which is also what makes the border a player can grab
     visible before they grab it. */
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 3px;
  box-shadow: 0 0.5rem 1.5rem #0009;
  color: var(--fg);
  font-size: 12px;
  /* A `<dialog>` is a max-content box by default, which would fight the
     width it is given. */
  max-width: none;
  max-height: none;
  overflow: hidden;
}

/* `display` above would otherwise outrank both of the ways a window is put
   away -- the attribute on a panel, and `close()` on a dialog. */
.window[hidden],
dialog.window:not([open]) {
  display: none;
}

/* The one in front is the one being used, and its frame says so. Everything
   else is a window that happens to be open. */
.window.front {
  border-color: #38424f;
  box-shadow: 0 0.5rem 2rem #000b;
}

/* A dialog takes the focus when it is shown, and a ring around the whole
   window is not what that should look like: what is worth seeing is which
   control inside it has the keyboard. */
.window:focus {
  outline: none;
}

/* Everything below the title bar sits on the window's ground rather than its
   frame. `:where` so that a pane with a ground of its own -- the icons, the
   status strips -- still wins on specificity alone. */
.window > :where(:not(.win-bar)) {
  background: var(--bg);
}

/* No backdrop: these are shown rather than modal, so there is nothing behind
   them to dim -- the rest of the desktop is still live. */
.window::backdrop {
  background: transparent;
}

/* A terminal is the window the client's own keys act on -- `Ctrl+B` stops
   what is running in it, and a file opened from an explorer runs there -- so
   which one that is has to be visible even when the window in front is an
   editor. `.front` is the top of the stack; this is the terminal last used. */
.terminal.active > .win-bar .win-title {
  color: var(--gold);
}

/* ---- a window's title bar ---------------------------------------------- */

.win-bar {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.25rem 0.3rem 0.25rem 0.5rem;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 2px 2px 0 0;
}

.window.front .win-bar {
  background: #1f2733;
}

.win-title {
  color: var(--accent);
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.win-spacer {
  flex: 1 1 auto;
  min-width: 0;
}

.win-close {
  font-size: 11px;
  padding: 0.1rem 0.4rem;
}

/* The title bar is the handle. Nothing in it should select as it is
   dragged, and the cursor says so before the drag starts. */
.win-drag {
  cursor: move;
  user-select: none;
  touch-action: none;
}
.win-drag.dragging {
  cursor: grabbing;
}

/* ---- communications log ------------------------------------------------ */

.comm-body {
  overflow-y: auto;
  padding: 0.3rem 0.6rem;
  font-size: 11px;
  line-height: 1.45;
}

.comm-line {
  display: flex;
  gap: 0.6rem;
}

.comm-time {
  flex: 0 0 4.5rem;
  color: var(--gold);
  font-weight: 700;
  text-align: right;
}

.comm-text {
  color: var(--gold);
  font-weight: 700;
}

/* The log is two rows rather than three -- there is nothing along its
   bottom -- so it says so. */
#comm {
  grid-template-rows: auto minmax(0, 1fr);
}

/* ---- the file explorer ------------------------------------------------- */

/* The path being shown, beside the title, so the window says where it is
   without the tree having to be read. */
.tree-path {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--dim);
  font-size: 11px;
}

/* The folders, the divider, and the files. The divider is dragged, so the
   left column is a property rather than a length. */
.tree-panes {
  /* A proportion once the window has been dragged narrow enough that a
     fixed width would leave the icons nowhere to go. */
  --tree-pane: min(13rem, 45%);

  display: grid;
  grid-template-columns: minmax(0, var(--tree-pane)) auto minmax(0, 1fr);
  min-height: 0;
}

.tree-body {
  overflow: auto;
  padding: 0.25rem 0;
  font-size: 11px;
}

/* Wide enough to grab, drawn as a rule. */
.tree-split {
  width: 5px;
  cursor: col-resize;
  background: var(--rule);
  touch-action: none;
}
.tree-split:hover,
.tree-split.dragging {
  background: var(--accent);
}

.tree-icons {
  overflow: auto;
  min-height: 0;
  padding: 0.4rem;
  display: grid;
  /* As many as fit, which is what makes the pane worth widening. */
  grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
  grid-auto-rows: min-content;
  gap: 0.3rem;
  align-content: start;
  background: #0d1218;
}
.tree-icons:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: -2px;
}

.tree-empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: 0.6rem;
  color: var(--dim);
  font-size: 11px;
}

.tree-status {
  margin: 0;
  padding: 0.25rem 0.6rem;
  color: var(--dim);
  font-size: 10px;
  /* One line: the strip is furniture, and a long complaint belongs in the
     communications log, which is where the window puts one. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tree-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One folder. The indent is the depth rather than nested padding, so a name
   is never squeezed to nothing by how deep it sits. */
.node {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.1rem 0.4rem 0.1rem calc(0.3rem + var(--depth, 0) * 0.85rem);
  cursor: default;
  white-space: nowrap;
  /* A row is dragged onto a console, so it must not be selected by the
     press that starts the drag. */
  user-select: none;
}
.node:hover {
  background: #1e2530;
}
.node.selected {
  background: #24303c;
}
.node:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
}

/* The folding arrow, which keeps its room even on a folder with nothing
   under it so that names line up down the column. */
.twist {
  flex: 0 0 0.8rem;
  color: var(--dim);
  font-size: 9px;
  text-align: center;
  cursor: pointer;
}

.folder-icon {
  flex: 0 0 auto;
  color: var(--gold);
  font-size: 9px;
}

.node .name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--gold);
  font-weight: 700;
}

/* ---- one file, as an icon ---------------------------------------------- */

.tile {
  position: relative;
  display: grid;
  grid-template-rows: auto auto auto;
  justify-items: center;
  gap: 0.1rem;
  padding: 0.35rem 0.2rem 0.3rem;
  border: 1px solid transparent;
  border-radius: 2px;
  cursor: default;
  /* A tile is dragged onto a console, so it must not be selected by the
     press that starts the drag. */
  user-select: none;
}
.tile:hover {
  background: #1e2530;
}
.tile.selected {
  background: #24303c;
  border-color: var(--rule);
}

.tile-icon {
  font-size: 20px;
  line-height: 1.1;
  color: var(--dim);
}
/* What the game itself tells apart: a script is run, a song is played. */
.tile.script .tile-icon {
  color: var(--accent);
}
.tile.song .tile-icon {
  color: var(--chat-emote);
}
.tile.text .tile-icon {
  color: var(--fg);
}
.tile.image .tile-icon,
.tile.pack .tile-icon {
  color: var(--gold);
}

.tile-name {
  max-width: 100%;
  /* Two lines' worth whether the name needs them or not, so a long name does
     not stand its tile taller than the rest of the row. */
  min-height: 2.4em;
  font-size: 11px;
  text-align: center;
  /* Two lines and then an ellipsis: a name is what the tile is for, and one
     line is not enough for the ones the game ships. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.tile-size {
  color: var(--dim);
  font-size: 9px;
}

/* The download button, which is furniture until the tile is pointed at. */
.tile .get {
  position: absolute;
  top: 1px;
  right: 1px;
  padding: 0 0.2rem;
  font-size: 11px;
  line-height: 1.3;
  background: transparent;
  border-color: transparent;
  color: var(--dim);
  visibility: hidden;
}
.tile:hover .get,
.tile:focus-within .get {
  visibility: visible;
}
.tile .get:hover {
  color: var(--accent);
  border-color: var(--rule);
}

/* The folder a drop would land in, and the console a path would be typed
   into. Both are the same promise, so both are drawn the same way. */
.node.drop-target,
.tree-icons.drop-target {
  background: #1d3324;
  outline: 1px dashed var(--accent);
  outline-offset: -1px;
}

/* ---- the desktop ----------------------------------------------------- */

/* The ground the windows float over. It holds nothing but the notice below,
   so its whole job is to take the row between the title bar and the status
   bar and be the colour of the client. */
#desktop {
  display: grid;
  place-items: center;
  /* `min-width: 0` as well as the height, so that a wide window cannot hold
     the row open and push the status bar off. */
  min-width: 0;
  min-height: 0;
}

/* Shown only while there is no terminal at all, which is a desktop with
   nothing on it and no obvious way on. */
#desktop-hint {
  margin: 0;
  color: var(--dim);
  font-size: 12px;
}
#desktop-hint b {
  color: var(--accent);
}

/* ---- console --------------------------------------------------------- */

/* A path from the tree is about to be typed here. */
.console.drop-target {
  outline: 1px dashed var(--accent);
  outline-offset: -2px;
}

/* The body of a terminal window: the log, with the prompt as its last line.
   It is what scrolls, and its width is what scripts measure against -- so
   resizing the window changes what a script laid out before the drag, which
   is what resizing a terminal has always done. */
.console {
  overflow-y: auto;
  /* The whole area is the terminal: a click in it takes the caret, so the
     cursor says so over the blank space below the last line too. */
  cursor: text;
  padding: 0.4rem 0;
  /* Script output is whitespace-significant. */
  white-space: pre-wrap;
  word-break: break-word;
  min-height: 0;
}

/* Spacing measured off the client: lines sit 19px apart, output is indented
   by `PreSpaceWidth` (600 twips, which is 40px at 96dpi) past a 4px gutter,
   and a `noprespace` line keeps only the gutter. Bold is the console's
   default weight, so anything not carrying its own run styling inherits it. */
.line {
  /* 19px between lines of the default 10pt text. Unitless so a line carrying
     a larger run -- the startup banner is 48pt -- grows to hold it. */
  min-height: 19px;
  line-height: 1.425;
  /* The console's own face, which a line without run styling of its own --
     a blank one, an echo, the prompt -- is drawn in. */
  font-family: Verdana, "DejaVu Sans", system-ui, sans-serif;
  font-size: 10pt;
  font-weight: 700;
  padding: 0 0.6rem 0 calc(var(--gutter) + var(--prespace));
}

/* `noprespace` removes the console's usual left indent. */
.line.no-prespace {
  padding-left: var(--gutter);
}

.align-center {
  text-align: center;
}
.align-right {
  text-align: right;
}

.line.system {
  color: var(--dim);
}
/* An echoed line: what was typed reads as ordinary text, and only the prompt
   in front of it is coloured. */
.line.echo {
  color: #ffffff;
}
.prompt-echo {
  color: var(--prompt);
  font-weight: 700;
  white-space: pre-wrap;
}
.prompt-echo.script {
  color: inherit;
  font-weight: inherit;
}
.line.error {
  color: var(--error);
}
/* Ctrl+B, which the client reports in the same orange it always has. */
.line.stopped {
  color: #ff9a3d;
}

@keyframes blink {
  50% {
    opacity: 0.15;
  }
}
.flash {
  animation: blink 1s step-start infinite;
}
.flash-fast {
  animation: blink 0.35s step-start infinite;
}
.flash-slow {
  animation: blink 2s step-start infinite;
}

@media (prefers-reduced-motion: reduce) {
  .flash,
  .flash-fast,
  .flash-slow {
    animation: none;
  }
}

/* ---- input ----------------------------------------------------------- */

/* The last line of the log, not a bar beneath it. It inherits `.line`
   padding, so the prompt sits in the same column as everything above it. */
.entry {
  display: flex;
  align-items: baseline;
}

/* A script's own prompt is part of its output and is coloured like it; the
   shell's `/home>` keeps the accent. */
.prompt {
  color: var(--prompt);
  font-weight: 700;
  white-space: pre-wrap;
}
.prompt.script {
  color: inherit;
  font-weight: inherit;
}
.prompt:empty {
  display: none;
}

/* Scoped to the entry so it outweighs the rule that dresses the sign-in
   fields: the console's own input is part of the log, not a form control. */
.entry .input {
  flex: 1;
  min-width: 0;
  border: none;
  background: transparent;
  color: inherit;
  font: inherit;
  padding: 0;
}
.entry .input:focus {
  outline: none;
}
/* Hidden rather than dimmed while a script is running: a terminal shows no
   caret when it is not listening. */
.entry.idle {
  display: none;
}

/* ---- mail ------------------------------------------------------------ */

.mail-spacer {
  flex: 1;
}

.mail-button {
  font-size: 11px;
  padding: 0.15rem 0.5rem;
}
.mail-button:disabled {
  color: var(--dim);
  cursor: default;
  border-color: transparent;
}

/* The list, and the message under it when one is open. */
.mail-body {
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  min-height: 0;
}
.mail-body:has(.mail-read) {
  grid-template-rows: minmax(6rem, 2fr) minmax(0, 3fr);
}

.mail-list {
  overflow-y: auto;
  min-height: 0;
}

.mail-empty {
  margin: 0;
  padding: 0.6rem;
  color: var(--dim);
}

/* A row, laid out as the original's three columns. */
.mail-row {
  display: grid;
  grid-template-columns: 12rem 1fr 9rem;
  gap: 0.6rem;
  width: 100%;
  padding: 0.2rem 0.6rem;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--fg);
  font-weight: 400;
  text-align: left;
}
.mail-row:hover {
  background: var(--panel);
  color: var(--fg);
}
/* Unread is bold, which is the only mark the original gives it. */
.mail-row.unread {
  font-weight: 700;
  color: #ffffff;
}
.mail-row.current {
  background: var(--panel);
  box-shadow: inset 2px 0 0 var(--accent);
}

.mail-from {
  color: var(--gold);
}
.mail-subject,
.mail-date {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mail-date {
  color: var(--dim);
  text-align: right;
}

.mail-read {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  min-height: 0;
  border-top: 1px solid var(--rule);
  background: var(--panel);
}

.mail-read-head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.3rem 0.6rem;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--rule);
}
.mail-read-subject {
  font-weight: 700;
  color: #ffffff;
}
.mail-read-meta {
  grid-row: 2;
  color: var(--dim);
}
.mail-read-head .mail-button {
  grid-row: 1 / span 2;
  grid-column: 2;
}

.mail-read-body {
  margin: 0;
  padding: 0.6rem;
  overflow: auto;
  min-height: 0;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: "Courier New", "Liberation Mono", monospace;
  font-size: 12px;
}

.mail-compose {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  gap: 0.4rem;
  padding: 0.6rem;
  min-height: 0;
}

.mail-field {
  display: grid;
  grid-template-columns: 5rem 1fr;
  align-items: center;
  gap: 0.6rem;
  color: var(--dim);
}
.mail-field-body {
  align-items: start;
  min-height: 0;
}
.mail-field textarea {
  height: 100%;
  resize: none;
  background: #0e1319;
  color: var(--fg);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 0.3rem 0.4rem;
  font-family: "Courier New", "Liberation Mono", monospace;
  font-size: 12px;
}

.mail-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.35rem;
}

.mail-status {
  padding: 0.25rem 0.6rem;
  border-top: 1px solid var(--rule);
  background: var(--titlebar);
  color: var(--dim);
  font-size: 11px;
  min-height: 1.5rem;
}

/* ---- the editor ------------------------------------------------------ */

.ed-title {
  color: var(--gold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ed-spacer {
  flex: 1;
}

.ed-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 17rem;
  min-height: 0;
}

/* The gutter and the code, which scroll together. */
.ed-pane {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  min-height: 0;
  background: #0a0e13;
}

/* Everything that has to line up says so once, here: the two layers of the
   editor and the line numbers are drawn in the same metrics, because the
   highlight sits exactly behind the text it colours. */
.ed-numbers,
.ed-highlight,
.ed-input {
  margin: 0;
  border: 0;
  font-family: "Courier New", "Liberation Mono", monospace;
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: 0;
  tab-size: 4;
  white-space: pre;
}

.ed-gutter {
  overflow: hidden;
  border-right: 1px solid var(--rule);
  background: #0d1219;
}
.ed-numbers {
  padding: 0.5rem 0.5rem 0.5rem 0.7rem;
  color: #4a5462;
  text-align: right;
  user-select: none;
}

.ed-code {
  position: relative;
  overflow: hidden;
  min-width: 0;
}
.ed-highlight {
  position: absolute;
  top: 0;
  left: 0;
  min-width: 100%;
  width: max-content;
  padding: 0.5rem 0.7rem;
  color: var(--fg);
  pointer-events: none;
}
.ed-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0.5rem 0.7rem;
  resize: none;
  overflow: auto;
  background: transparent;
  /* The text itself is drawn by the layer underneath; only the caret and
     the selection belong to the textarea. */
  color: transparent;
  caret-color: var(--accent);
  border-radius: 0;
}
.ed-input:focus {
  outline: none;
}
.ed-input::selection {
  background: #2b4a6b;
}

/* The colours. Comments recede, strings and the game's own markup stand
   apart, and a name the interpreter knows is lit differently from one it
   does not -- which is how a typo shows itself. */
.tok-comment {
  color: #5f6c7b;
  font-style: italic;
}
.tok-string {
  color: #c8a55b;
}
.tok-markup {
  color: #d8622f;
  font-weight: 700;
}
.tok-number {
  color: #b48ce0;
}
.tok-keyword {
  color: #4aa3ff;
  font-weight: 700;
}
.tok-builtin {
  color: #43c9b0;
}
.tok-constant {
  color: #b48ce0;
}
.tok-api {
  color: var(--accent);
}
.tok-member {
  color: #9fd4ff;
}
.tok-op {
  color: #8e99a8;
}
.tok-continuation {
  color: var(--gold);
  font-weight: 700;
}

/* The API reference, which is the original's command list brought forward. */
.ed-ref {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-height: 0;
  border-left: 1px solid var(--rule);
  background: var(--panel);
}
.ed-search {
  margin: 0.4rem;
  width: calc(100% - 0.8rem);
}
.ed-list {
  overflow-y: auto;
  min-height: 0;
}
.ed-ref-row {
  display: block;
  width: 100%;
  padding: 0.15rem 0.5rem;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--fg);
  font-weight: 400;
  text-align: left;
}
.ed-ref-row:hover {
  background: #1f2731;
}
.ed-ref-name {
  color: var(--accent);
}
.ed-ref-args {
  display: block;
  color: var(--dim);
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ed-ref-foot {
  margin: 0;
  padding: 0.3rem 0.5rem;
  border-top: 1px solid var(--rule);
  color: var(--dim);
  font-size: 11px;
}

.ed-status {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.25rem 0.6rem;
  border-top: 1px solid var(--rule);
  background: var(--titlebar);
  color: var(--dim);
  font-size: 11px;
  min-height: 1.6rem;
}
.ed-hint {
  color: var(--accent);
  font-family: "Courier New", "Liberation Mono", monospace;
}
.ed-position,
.ed-state {
  white-space: nowrap;
}

/* ---- the file library ------------------------------------------------- */

.lib-body {
  display: grid;
  grid-template-columns: 12rem minmax(0, 1fr);
  min-height: 0;
}

.lib-categories {
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  min-height: 0;
  border-right: 1px solid var(--rule);
  background: var(--panel);
}
.lib-category {
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--fg);
  font-weight: 400;
  text-align: left;
  padding: 0.25rem 0.6rem;
}
.lib-category:hover {
  background: #1f2731;
}
.lib-category.current {
  color: var(--gold);
  box-shadow: inset 2px 0 0 var(--accent);
}

.lib-table {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  min-height: 0;
}
.lib-table:has(.lib-detail) {
  grid-template-rows: auto minmax(0, 1fr) auto;
}

.lib-rows {
  overflow-y: auto;
  min-height: 0;
}

.lib-row {
  display: grid;
  grid-template-columns: minmax(0, 3fr) 5rem 6rem 8rem minmax(0, 2fr) 7rem;
  gap: 0.4rem;
  width: 100%;
  padding: 0.2rem 0.6rem;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--fg);
  font-weight: 400;
  text-align: left;
}
.lib-row:hover {
  background: var(--panel);
}
.lib-row.current {
  background: var(--panel);
  box-shadow: inset 2px 0 0 var(--accent);
}
.lib-head {
  border-bottom: 1px solid var(--rule);
  background: var(--titlebar);
  padding: 0;
}
.lib-cell {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lib-sort {
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--dim);
  text-align: left;
  padding: 0.2rem 0;
  font-size: 11px;
}
.lib-sort.sorted {
  color: var(--gold);
}
.lib-arrow {
  font-size: 9px;
}

/* Two columns rather than six: an upload is an id and a rendered line. */
.lib-mine {
  grid-template-columns: 5rem minmax(0, 1fr);
}

.lib-detail {
  border-top: 1px solid var(--rule);
  background: var(--panel);
  padding: 0.5rem 0.6rem;
}
.lib-detail-title {
  font-weight: 700;
  color: #ffffff;
}
.lib-detail-meta {
  color: var(--dim);
}
.lib-detail-body {
  margin: 0.4rem 0;
  white-space: pre-wrap;
}

.lib-panel,
.lib-form {
  display: grid;
  align-content: start;
  gap: 0.4rem;
  padding: 0.6rem;
  min-height: 0;
}
.lib-panel {
  grid-template-rows: auto auto minmax(0, 1fr) auto;
}
.lib-heading {
  margin: 0;
  font-size: 13px;
  color: var(--gold);
}
.lib-note {
  margin: 0;
  color: var(--dim);
}

.lib-channel {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--dim);
}
.lib-channel input {
  width: 6rem;
}

.lib-text {
  min-height: 0;
  height: 100%;
  resize: none;
  background: #0e1319;
  color: var(--fg);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 0.4rem;
  font-family: "Courier New", "Liberation Mono", monospace;
  font-size: 12px;
}

.mail-button.current {
  color: var(--gold);
  border-color: var(--accent);
}

/* ---- status bar ------------------------------------------------------ */

#statusbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.25rem 0.6rem;
  background: var(--titlebar);
  border-top: 1px solid var(--rule);
  font-size: 11px;
  color: var(--dim);
}

#hint b {
  color: var(--accent);
}

/* On a phone the switches are what the bar is for, and there is not room for
   both them and the hint. The startup banner says the same thing anyway. */
@media (max-width: 34rem) {
  #hint {
    display: none;
  }
}

/* The apps. The library has no command in the original either: it is opened
   from the console's own furniture, which here is the status bar, and the
   rest keep it company. Furniture rather than something to press by
   accident, which is the dress the account's buttons wear too. */
#statusbar button {
  padding: 0.05rem 0.4rem;
  font-size: 11px;
  background: transparent;
  border-color: transparent;
  color: var(--dim);
}
#statusbar button:hover,
#statusbar button.open {
  color: var(--gold);
  border-color: var(--rule);
}

/* A notice arriving while its window is closed, which is what the
   communications log now is to begin with. The button is the only thing left
   pointing at it, so it goes on pointing until the log has been looked at
   rather than blinking once and stopping: nothing here is urgent enough to
   interrupt, and nothing here should be missed either.

   A pulse rather than a static colour because the bar already has one in it
   -- `:hover` and an open menu are both gold -- and because movement is what
   a glance catches. Where the player has asked for less of it, the lit half
   of the pulse is simply held. */
#statusbar button.attention {
  color: var(--gold);
  animation: attention 1.8s ease-in-out infinite;
}
@keyframes attention {
  50% {
    background: var(--gold);
    border-color: var(--gold);
    color: var(--bg);
  }
}
@media (prefers-reduced-motion: reduce) {
  #statusbar button.attention {
    animation: none;
    background: var(--gold);
    border-color: var(--gold);
    color: var(--bg);
  }
}

/* ---- the app menu ---------------------------------------------------- */

/* What an app with windows out drops up over the status bar: which of them,
   or another. Fixed and hanging from the body, placed by `taskbar.ts`
   against the button it belongs to -- the status bar is one line tall, and a
   menu laid out inside it would be too. */
#app-menu {
  position: fixed;
  /* A floor. `taskbar.ts` sets the real one when it places the menu, since
     the windows' stack climbs and no number here would stay above it. */
  z-index: 100;
  min-width: 11rem;
  max-width: min(22rem, calc(100vw - 1rem));
  display: grid;
  padding: 0.25rem;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 3px;
  box-shadow: 0 -8px 20px rgb(0 0 0 / 50%);
  font-size: 11px;
}
/* `display` above would otherwise outrank the attribute. */
#app-menu[hidden] {
  display: none;
}

.app-menu-item {
  padding: 0.25rem 0.5rem;
  text-align: left;
  background: transparent;
  border-color: transparent;
  color: var(--dim);
  /* A window is named for where it is looking, and a deep path is longer
     than the menu: it ellipsizes rather than widening or wrapping. */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.app-menu-item:hover,
.app-menu-item:focus-visible {
  color: var(--gold);
  border-color: var(--rule);
}

/* Whichever window is already in front, so the menu says where a press would
   take you before it takes you. */
.app-menu-item.front {
  color: var(--accent);
}

/* Opening another is a different kind of thing from picking one of these,
   and is ruled off from them. */
.app-menu-item.fresh {
  margin-top: 0.25rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--rule);
  border-radius: 0;
}
.app-menu-item.fresh:hover,
.app-menu-item.fresh:focus-visible {
  border-color: transparent;
  border-top-color: var(--rule);
}

/* ---- chat ------------------------------------------------------------ */

/* A window now, where the original raises its ChatBox over the console with
   `ZOrder 0`. Being a window is the difference worth having: the room can be
   watched beside a script rather than instead of it. */
#chat {
  grid-template-rows: auto minmax(0, 1fr) auto;
}

.chat-title {
  color: var(--gold);
}

.chat-log {
  overflow-y: auto;
  min-height: 0;
  padding: 0.4rem 0.6rem;
  /* A message keeps its own spacing, and a long one wraps rather than
     widening the pane. */
  white-space: pre-wrap;
  word-break: break-word;
  line-height: 1.5;
}

/* The original prints an ordinary line white and an emote in `iBlue`. Your
   own lines are the one addition: it had no way to pick them out, and in a
   room where everyone is named by their account it is worth having. */
.chat-line {
  display: flex;
  gap: 0.6rem;
  color: #ffffff;
}
.chat-line.emote {
  color: var(--chat-emote);
}
.chat-line.own {
  color: var(--accent);
}

/* The time, in a column of its own for the same reason the communications
   log puts one there: a fixed width right against the text lines every
   message up, and a wrapped one hangs under itself rather than under the
   clock. Dim, because it is the least of what a line says. */
.chat-time {
  flex: 0 0 4.5rem;
  color: var(--dim);
  text-align: right;
  /* It is furniture, not something said: the message's own spacing is
     `pre-wrap` and this has none to keep. */
  white-space: nowrap;
}

.chat-text {
  min-width: 0;
}

/* The day, ruled across the log where it changes. The rules are the
   pseudo-elements either side, so the date sits in a gap in one line rather
   than on top of one -- and they take whatever width is left, so the date
   stays centred as the window is dragged. */
.chat-day {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.5rem 0 0.2rem;
  color: var(--dim);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.chat-day::before,
.chat-day::after {
  content: "";
  flex: 1 1 0;
  border-top: 1px solid var(--rule);
}

.chat-entry {
  display: flex;
  gap: 0.4rem;
  padding: 0.4rem 0.6rem;
  border-top: 1px solid var(--rule);
  background: var(--panel);
}

.chat-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.2rem 0.35rem;
  background: var(--bg);
  border: 1px solid var(--rule);
  color: var(--fg);
  font: inherit;
}
.chat-input:focus {
  outline: none;
  border-color: var(--accent);
}

/* Signed out the room is still readable -- it is public -- but there is
   nothing to say it with, so the box says so rather than failing on submit. */
.chat-input:disabled,
.chat-send:disabled {
  color: var(--dim);
  border-color: var(--rule);
  cursor: not-allowed;
}
.chat-send:disabled:hover {
  border-color: var(--rule);
  color: var(--dim);
}
