/*
 * All colour lives in custom properties, set by the active theme (themes.ts).
 * Nothing here hard-codes a colour, which is what allows a theme — eventually a
 * per-game one — to restyle the player without touching any of this.
 */

/*
 * The IBM VGA 9x16 ROM font — the actual character cell a DOS text display drew.
 * VileR's remake, from The Ultimate Oldschool PC Font Pack, CC BY-SA 4.0.
 * https://int10h.org/oldschool-pc-fonts/
 *
 * Fetched by `npm run fonts`, never committed. If it is absent this rule simply
 * does not load and the CGA theme falls back to the next face in its stack — so
 * the player still works, it just is not period-correct.
 */
@font-face {
  font-family: 'Web437 IBM VGA';
  src: url('fonts/Web437_IBM_VGA_9x16.woff') format('woff');
  font-display: swap;
}

/*
 * The MCGA text font: an 8x16 cell, which is what the PC drew a v6 game's text with.
 *
 * Committed rather than fetched, unlike the one above, because it is CC0 — public
 * domain — and so carries none of the copyleft that keeps the other one out of an MIT
 * repository. "DOS MCGA 8x16" by Marty Seefeldt.
 *
 * It is pixel-exact, but not at the size you would guess: its em box is 2000 units and
 * one design pixel is 153.75 of them, so the cell is reached at a ~13px font size, not
 * a 16px one. The canvas works that out by measuring rather than assuming — see
 * V6Canvas.measure.
 */
@font-face {
  font-family: 'DOS MCGA';
  src: url('fonts/dos-mcga-8x16.otf') format('opentype');
  font-display: swap;
}

/*
 * The font a v6 game's text is drawn in.
 *
 * Deliberately not the theme's font. A v6 story draws its own borders, panels and
 * frames as artwork and fits its text inside them; the look is the game's, not the
 * player's, and it was the PC's ROM font that filled those frames. Themes still own
 * everything they own for v1-v5, where the screen really is just styled text.
 *
 * A theme is free to override this. Nothing breaks if it does — the canvas measures
 * whatever it is given and puts it on the game's 8px grid.
 */
:root {
  --v6-font: 'DOS MCGA', 'Web437 IBM VGA', monospace;
}

/*
 * A bitmap font has exactly one right size: its native cell, or an integer
 * multiple of it. Anything else resamples the pixels and the crispness that is
 * the entire point of using it disappears. 9x16 means 16px.
 *
 * Antialiasing does the same damage, so it is turned off for this theme only —
 * the pixels are the typeface.
 */
:root[data-theme='cga'] body {
  -webkit-font-smoothing: none;
  -moz-osx-font-smoothing: grayscale;
  font-smooth: never;
}

* {
  box-sizing: border-box;
}

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

body {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-size: var(--font-size);
  line-height: var(--line-height);
  transition: background 200ms ease, color 200ms ease;
}

/* ---- top bar ------------------------------------------------------------ */

#bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
  background: color-mix(in srgb, var(--bg) 88%, var(--fg));
  flex: 0 0 auto;
}

#title {
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--accent);
}

#meta {
  color: var(--dim);
  font-size: 0.82em;
}

.spacer {
  flex: 1;
}

.button,
#theme {
  font: inherit;
  font-size: 0.85em;
  color: var(--fg);
  background: color-mix(in srgb, var(--bg) 70%, var(--fg));
  border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
  border-radius: 5px;
  padding: 4px 10px;
  cursor: pointer;
}

.button:hover:not(:disabled),
#theme:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ---- audio controls ----------------------------------------------------- */

#audio {
  display: flex;
  align-items: center;
  gap: 8px;
}

#volume {
  width: 90px;
  accent-color: var(--accent);
  cursor: pointer;
}

/*
 * The sound indicator. It has three things to say, and they are not the same
 * kind of thing: a sound is playing, the browser has not let us make noise yet,
 * or the game asked for a sound we were never given. The last one matters most —
 * a v3 game cannot be told a sound is missing, so this is the only place anyone
 * will ever find out.
 */
#sound-state {
  font-size: 0.78em;
  color: var(--dim);
  min-width: 8ch;
  white-space: nowrap;
}

#sound-state.playing {
  color: var(--accent);
}

#sound-state.missing {
  color: var(--z-yellow);
}

/* A sound that is currently sounding gets a gentle pulse, so silence caused by
   a stuck effect is distinguishable from silence caused by nothing playing. */
#sound-state.playing::before {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 6px;
  border-radius: 50%;
  background: currentColor;
  animation: pulse 1.1s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 0.25;
  }
  50% {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  #sound-state.playing::before {
    animation: none;
    opacity: 1;
  }
}

/* ---- story card --------------------------------------------------------- */

/*
 * A jacket, not a splash screen: it sits over the transcript and is dismissed with
 * a click or Escape. The game is already running behind it — nothing waits on this.
 */
#card {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(16px, 4vw, 48px);
  flex-wrap: wrap;
  padding: clamp(16px, 5vw, 64px);

  background: color-mix(in srgb, var(--bg) 94%, transparent);
  backdrop-filter: blur(3px);
  cursor: pointer;
}

#card[hidden] {
  display: none;
}

#card-cover {
  max-width: min(360px, 40vw);
  max-height: 70vh;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 16px 48px rgb(0 0 0 / 45%);
}

#card-text {
  max-width: 46ch;
  cursor: default;
}

#card-title {
  margin: 0 0 4px;
  font-size: 1.5em;
  line-height: 1.2;
  color: var(--fg);
}

#card-headline {
  margin: 0 0 14px;
  color: var(--accent);
  font-size: 0.95em;
}

#card-author {
  margin: 0 0 18px;
  color: var(--dim);
  font-size: 0.9em;
}

#card-description {
  margin: 0;
  color: var(--fg);
  font-size: 0.92em;
  line-height: 1.6;
  /* The blurb is prose, not terminal output. */
  white-space: normal;
}

#card-close {
  position: absolute;
  top: 14px;
  right: 14px;
}

/* ---- sound panel -------------------------------------------------------- */

/* ---- the picture gallery ------------------------------------------------- */

/*
 * A pop-out, sharing the sound panel's shape: you browse the artwork while the game
 * is still there behind it. Wider, because pictures need room.
 */
#gallery {
  position: fixed;
  top: 52px;
  right: 14px;
  z-index: 20;
  width: min(520px, calc(100vw - 28px));
  max-height: min(70vh, 640px);
  display: flex;
  flex-direction: column;

  background: color-mix(in srgb, var(--bg) 92%, var(--fg));
  border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
  border-radius: 8px;
  box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
  overflow: hidden;
}

#gallery[hidden],
#viewer[hidden] {
  display: none;
}

#gallery > header,
#gallery > footer {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
}

#gallery > footer {
  border-bottom: none;
  border-top: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
  font-size: 12px;
  opacity: 0.65;
}

#gallery-title {
  font-weight: 600;
  flex: 1;
}

#gallery-grid {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 8px;
}

/* A thumbnail. Pixel art, so it is never smoothed on the way up or down. */
.thumb {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 5px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}

.thumb:hover {
  border-color: color-mix(in srgb, var(--fg) 30%, transparent);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
}

.thumb canvas {
  image-rendering: pixelated;
  max-width: 100%;
  height: auto;
  /* A chequerboard, so a transparent picture reads as transparent rather than as
     whatever the theme happens to be. */
  background:
    repeating-conic-gradient(
      color-mix(in srgb, var(--fg) 12%, transparent) 0% 25%,
      transparent 0% 50%
    )
    50% / 12px 12px;
}

.thumb .number {
  opacity: 0.6;
}

/* One picture, blown up over everything. */
#viewer {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgb(0 0 0 / 70%);
  padding: 24px;
}

#viewer figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 100%;
  max-height: 100%;
}

#viewer canvas {
  image-rendering: pixelated;
  max-width: 100%;
  max-height: calc(100vh - 120px);
  object-fit: contain;
  border-radius: 4px;
  background:
    repeating-conic-gradient(rgb(255 255 255 / 10%) 0% 25%, transparent 0% 50%) 50% / 16px 16px;
}

#viewer figcaption {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #fff;
  font-size: 13px;
}

/*
 * A pop-out, not a modal. You audition a sound with the game still visible
 * behind it, and a looping effect keeps playing while you read the room it
 * belongs to. It floats over the transcript rather than reflowing it, so
 * opening it never moves the text you were reading.
 */
#sounds {
  position: fixed;
  top: 52px;
  right: 14px;
  z-index: 20;
  width: min(340px, calc(100vw - 28px));
  max-height: min(60vh, 520px);
  display: flex;
  flex-direction: column;

  background: color-mix(in srgb, var(--bg) 92%, var(--fg));
  border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
  border-radius: 8px;
  box-shadow: 0 12px 32px rgb(0 0 0 / 35%);
  overflow: hidden;
}

#sounds[hidden] {
  display: none;
}

#sounds header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px 8px 12px;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
}

#sounds-title {
  font-weight: 700;
  font-size: 0.85em;
  letter-spacing: 0.04em;
  color: var(--accent);
}

#sounds-list {
  overflow-y: auto;
  padding: 4px;
}

#sounds footer {
  padding: 7px 12px;
  border-top: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
  color: var(--dim);
  font-size: 0.74em;
}

/* One row per sound resource. */
.sound-row {
  display: grid;
  grid-template-columns: auto 3.5ch 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 5px;
  font-size: 0.82em;
}

.sound-row:hover {
  background: color-mix(in srgb, var(--fg) 8%, transparent);
}

.sound-row.playing {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

.sound-row .number {
  color: var(--dim);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.sound-row.playing .number {
  color: var(--accent);
}

.sound-row .detail {
  color: var(--dim);
  font-size: 0.92em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A looping sound never ends on its own; saying so is the difference between
   "still playing" and "stuck". */
.sound-row .loop {
  color: var(--z-yellow);
  font-size: 0.9em;
}

.sound-row .play {
  font: inherit;
  font-size: 1.05em;
  line-height: 1;
  color: var(--fg);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--fg) 25%, transparent);
  border-radius: 4px;
  width: 26px;
  height: 24px;
  cursor: pointer;
  padding: 0;
}

.sound-row .play:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.sound-row.unsupported {
  opacity: 0.5;
}

.sound-row.unsupported .play {
  cursor: default;
}

/* ---- screen ------------------------------------------------------------- */

#screen {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* The upper-window input overlay is positioned against this. */
  position: relative;
}

/*
 * The v6 screen.
 *
 * A graphical Infocom game draws its own everything — borders, status panels, the
 * lot — as pictures placed in pixels. So when one is playing, the character model
 * gets out of the way entirely: no status line, no upper window, no scrolling
 * transcript. The canvas *is* the screen.
 *
 * It is centred and letterboxed rather than stretched, because it is scaled by a
 * whole number and will rarely be exactly the size of the viewport. Stretching it
 * to fit would blur 1988 pixel art, which is the one thing worth protecting here.
 */
#screen.graphical {
  align-items: center;
  justify-content: center;
  background: var(--bg);
  overflow: hidden;
}

#screen.graphical #status,
#screen.graphical #upper,
#screen.graphical #lower {
  display: none;
}

#canvas {
  /* Nearest-neighbour on the way to the display, to match the way it is drawn. */
  image-rendering: pixelated;
  flex: 0 0 auto;
}

/*
 * The [MORE] prompt: the game has printed more than fits, and the next line would
 * scroll away text nobody has read.
 *
 * It floats over the canvas rather than being drawn into it. An interpreter that
 * stamps "[MORE]" into the bottom line destroys the text it lands on, and on a v6
 * game it lands on the artwork too.
 */
#more {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 12px;
  border-radius: 4px;
  background: var(--fg);
  color: var(--bg);
  font-weight: 600;
  letter-spacing: 0.1em;
  animation: cursor-blink 1.06s step-end infinite;
  pointer-events: none;
  z-index: 2;
}

@media (prefers-reduced-motion: reduce) {
  #more {
    animation: none;
  }
}

/*
 * The v3 status line. Versions 1-3 expect the *interpreter* to draw this, which
 * is why it is a real element and not something the game printed.
 */
#status {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 3px 14px;
  background: var(--status-bg);
  color: var(--status-fg);
  font-weight: 600;
  white-space: pre;
  overflow: hidden;
  flex: 0 0 auto;
}

/*
 * The upper window: a fixed character grid that never wraps or scrolls. Games
 * from v4 on draw their own status bars, menus and maps here by addressing
 * cells directly, so it has to stay a grid.
 */
#upper {
  margin: 0;
  padding: 0 14px;
  background: var(--upper-bg);
  white-space: pre;
  /* An 80-column grid at a large cell size is wider than a narrow window. Scroll
     it rather than clipping: a truncated status line or form is unreadable, and
     silently losing the right-hand columns is worse than a scrollbar. */
  overflow-x: auto;
  overflow-y: hidden;
  flex: 0 0 auto;
  font: inherit;

  /*
   * Cells must *tile*. The body's comfortable 1.55 leading is right for reading
   * prose and wrong for a grid: a span paints its background over its content
   * box, not the whole line box, so with leading every row of reverse-video
   * cells gets a gap above and below it. Bureaucracy fills its licence form with
   * reverse-video spaces to make a solid panel, and with leading that panel
   * disintegrates into separate horizontal bars.
   */
  line-height: var(--cell-height);
}

/*
 * inline-block, so each run's box is the full height of the row rather than just
 * the glyphs' content box. This is what actually makes the backgrounds meet.
 */
#upper span {
  display: inline-block;
  height: var(--cell-height);
  line-height: var(--cell-height);
  vertical-align: top;
}

#lower {
  flex: 1;
  overflow-y: auto;
  padding: 12px 14px 20px;
  outline: none;
  scrollbar-color: color-mix(in srgb, var(--fg) 30%, transparent) transparent;

  display: flex;
  flex-direction: column;
}

/*
 * The transcript and the input share one text flow, so the input sits immediately
 * after the ">" the *game* printed rather than on a line of its own below it.
 */
#flow {
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/*
 * "At the start of a game, the screen should be cleared and the text cursor
 * placed at the bottom left (so that text scrolls upwards as the game gets under
 * way)" — spec 8. So the flow is pushed to the bottom of the window until it is
 * long enough to fill it, after which it simply scrolls, exactly as a terminal
 * does. Anchoring at the top instead puts the whole opening of every game in the
 * wrong half of the screen.
 *
 * margin-top rather than justify-content: flex-end, which clips overflowing
 * content off the top of a scroll container in some browsers.
 */
#flow.anchor-bottom {
  margin-top: auto;
}

#transcript {
  display: inline;
}

/* ---- Z-machine text styles ---------------------------------------------- */

/*
 * Bold is themeable, because "bold" is not one thing.
 *
 * On a modern screen it means a heavier typeface. On a DOS text display it meant
 * *high intensity* — the same colour, brighter — which is why the original
 * Bureaucracy looks like it has levels of brightness rather than levels of
 * weight. Both are legitimate renderings of the same style bit, and the theme is
 * the right place to choose.
 *
 * Brightness is applied as a filter rather than a colour, so it brightens
 * whatever the run's colour already is instead of overriding the palette.
 */
.bold {
  font-weight: var(--bold-weight, 700);
}

/*
 * Intensity applies to the *foreground*, so it is deliberately not applied to a
 * reverse-video run: there, brightening would wash out the background block
 * rather than brighten the text sitting on it. A DOS display's intensity bit
 * worked the same way — it lit the ink, not the paper.
 */
.bold:not(.reverse) {
  filter: brightness(var(--bold-intensity, 1));
}

.italic {
  font-style: italic;
}

/*
 * Reverse video swaps ink and paper. Games use it for emphasis and for menu
 * selection, and it has to compose with an explicit colour rather than
 * overriding it, hence the two custom properties instead of fixed colours.
 */
.reverse {
  background: var(--run-fg, var(--fg));
  color: var(--run-bg, var(--bg));
}

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

/*
 * inline-block, not inline.
 *
 * The cursor is absolutely positioned inside this, so this element is its
 * containing block — and a relatively-positioned *inline* element is a poor one:
 * it can be split across line fragments, and the offset then resolves against the
 * wrong fragment, which puts the cursor somewhere unrelated to where the text
 * actually starts. inline-block gives it one well-defined box while still sitting
 * inline, immediately after the ">" the game printed.
 */
#prompt {
  display: inline-block;
  position: relative;
  vertical-align: baseline;
}

/*
 * The cursor.
 *
 * A native input draws a thin I-beam caret, which is a word-processor idea and
 * wrong for a text-mode screen: the display had a *hardware* cursor occupying a
 * whole character cell, blinking, and drawn as an underscore on DOS or a block on
 * most terminals. So the input's own caret is hidden and this is drawn instead.
 *
 * Its shape is themeable for the same reason bold and colour are — it is part of
 * what a given machine looked like, not something the story has any say in.
 */
#cursor {
  position: relative;
  display: inline-block;
  min-width: 1ch;
}

/*
 * The cursor bar, drawn over the character it sits on.
 *
 * `difference` is doing real work here: a full-height bar inverts the glyph
 * beneath it, which is exactly what a block cursor is, while a two-scanline bar
 * simply underlines it. So one rule gives both a terminal block cursor and a DOS
 * underscore, and the theme picks by setting --cursor-height alone.
 */
#cursor::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1ch;
  height: var(--cursor-height, 1em);
  background: var(--cursor-color, var(--fg));
  mix-blend-mode: difference;
  animation: cursor-blink 1.06s step-end infinite;
  pointer-events: none;
}

@keyframes cursor-blink {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

/* A cursor that blinks is an animation like any other; some people cannot have
   one. It still has to be *visible*, so it goes solid rather than disappearing. */
@media (prefers-reduced-motion: reduce) {
  #cursor {
    animation: none;
    opacity: 1;
  }
}

/*
 * When the game is reading into the *upper* window, the input is positioned over
 * the character grid at the exact cell the story left its cursor on. Bureaucracy
 * draws a form up there and expects you to type into its fields; an input parked
 * at the bottom of the screen makes the form impossible to fill in.
 */
#prompt.overlay {
  position: absolute;
  z-index: 5;
}

/*
 * The input is never seen. It exists to take keystrokes — and to keep a software
 * keyboard and IME working on a phone, which a synthetic key handler would not.
 * Everything visible is drawn by #typed / #cursor / #rest.
 *
 * Not display:none and not visibility:hidden: either would make it unfocusable,
 * and an input that cannot be focused cannot be typed into.
 */
#input {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1em;
  opacity: 0;
  font: inherit;
  border: none;
  outline: none;
  padding: 0;
  margin: 0;
  caret-color: transparent;
}

#typed,
#rest,
#under {
  white-space: pre;
}

#prompt.overlay #input {
  /* On the form, the field is as wide as the game left room for. */
  color: var(--fg);
}

/* ---- drop target -------------------------------------------------------- */

#drop {
  position: fixed;
  inset: 0;
  display: none;
  place-items: center;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  border: 3px dashed var(--accent);
  color: var(--accent);
  font-size: 1.4em;
  z-index: 10;
}

body.dropping #drop {
  display: grid;
}

/* The idle state, before a story is loaded. */
#transcript .hint {
  color: var(--dim);
}
