/* ==========================================================================
   O.R.B.S. — orbs.blackhearthgames.com

   A normal website wearing the game's clothes. It scrolls like a document,
   navigates from a bar at the top, and works with JavaScript switched off.
   The game flare — box-drawn panes, the phosphor palette, the boot report,
   the telemetry pane, the command line — sits on top of that rather than
   standing in for it.

   Every colour below is converted from the game's own
   crates/orbs/src/render/palette.rs, whose comment notes the values are
   "solved, not picked" — they satisfy contrast tests in that file. Do not
   hand-tune them here; change them there and re-convert.
   ========================================================================== */

/* ===== FONT =====
   The game's shipping face, at its native 8x16 cell. See fonts/NOTICE.md —
   and note the unscii-16-full trap documented there before ever re-fetching. */
@font-face {
  font-family: 'unscii';
  src: url('../fonts/unscii-16.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  /* block, not swap: a bitmap face swapping in after a fallback reflows every
     character cell on the page, which on a grid reads as the screen glitching.
     The file is 44 KB. */
  font-display: block;
}

/* ===== RESET ===== */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* ==========================================================================
   TOKENS — the four phosphors
   The base hue carries all ordinary text through INTENSITY alone; the accent
   triad is reserved strictly for meaning and is never decorative. Ordinary
   content may only use --fg-dim / --fg / --fg-bright.
   ========================================================================== */

/* amber — the default, and the game's. "The tube a scrying orb ought to be." */
:root,
:root[data-phosphor="amber"] {
  --bg:        #120B05;
  --fg-dim:    #8C7052;
  --fg:        #DBB88C;
  --fg-bright: #FFE8CC;
  --danger:    #D94A42;
  --cost:      #30B8F2;
  --success:   #99FF80;
}

/* green — the canonical terminal. */
:root[data-phosphor="green"] {
  --bg:        #050E08;
  --fg-dim:    #528C54;
  --fg:        #8CDB8F;
  --fg-bright: #CCFFCF;
  --danger:    #F21726;
  --cost:      #73ADE6;
  --success:   #FFEB80;
}

/* muted violet — the one theme nobody mistakes for a real terminal. */
:root[data-phosphor="violet"] {
  --bg:        #0E0913;
  --fg-dim:    #69578F;
  --fg:        #AB96D6;
  --fg-bright: #EBE0FF;
  --danger:    #E03842;
  --cost:      #7AC9FF;
  --success:   #8FFF8C;
}

/* monochrome — the one theme that is not a phosphor. It earns its place on
   accessibility: the highest contrast on offer, and the only base ramp with no
   hue for a colour vision deficiency to take away. */
:root[data-phosphor="mono"] {
  --bg:        #050506;
  --fg-dim:    #616169;
  --fg:        #BDBDC7;
  --fg-bright: #FFFFFF;
  --danger:    #F2403D;
  --cost:      #4C9EFF;
  --success:   #9EFF99;
}

:root {
  --font-screen: 'unscii', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Vertical rhythm, in em, so everything scales with the reader's own font
     size rather than against it. */
  --line: 1.5;
  --row: 1.5em;
  /* em, not px, so the frame grows with the type instead of squeezing it. */
  --gap: 0.75em;
  --pad: 1em;
  /* The page width is in em for the same reason: a wide screen should buy more
     text at a comfortable size, not the same text stretched across more of it.
     ~64em holds the 78ch measure and the 26ch telemetry pane side by side at
     any size on the scale below. */
  --page-w: 64em;

  /* Measure, in characters. Prose past ~78ch stops scanning as terminal
     output and starts scanning as a wall. */
  --measure: 78ch;

  /* Clearance for the fixed command line, so the last line of the footer is
     never trapped underneath it. Overridden to 0 when there is no JS. */
  --prompt-h: 0px;
}
:root.has-js { --prompt-h: 3.2em; }

/* ==========================================================================
   PAGE — an ordinary scrolling document
   ========================================================================== */

html {
  background: var(--bg);
  scroll-behavior: smooth;
  /* Anchor links must not land a heading underneath the sticky masthead. */
  scroll-padding-top: 7em;

  /* Fluid type. The floor is 1rem — which is the reader's OWN default, not a
     hard 16px — so anyone who has raised their browser font size keeps every
     pixel of it, and the scale only ever adds on top. It reaches ~20px on a
     1440-wide window and tops out at 1.5rem, which is where a 78-character
     measure stops fitting beside the telemetry pane.
     unscii is happiest at exact multiples of 16px but does not require them:
     the sizes this lands on (17-24px) were checked and stay even, because
     every stem in the face is an axis-aligned rectangle. */
  font-size: clamp(1rem, 0.85rem + 0.45vw, 1.5rem);
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-screen);
  /* 1rem, not a fixed 16px. unscii is drawn on a 16-unit grid and is at its
     crispest at exactly 16px, which is also the default root size — so the
     common case is the ideal one, and a reader who has raised their browser's
     font size still gets what they asked for.
     Measured: x-height is 7 device px at 16px and 9 at 20px, and the face
     stays even and legible at the 20px/24px sizes that DPR 1.25 and 1.5
     rasterise it to. Non-integer scaling was the risk here and it does not
     bite, because every stem in this face is an axis-aligned rectangle. */
  /* Inherit the fluid scale set on <html>. */
  font-size: inherit;
  line-height: var(--line);
  /* NOT -webkit-font-smoothing: none. Killing antialiasing is right only when
     a pixel face lands on exact pixel boundaries; at DPR 1.25 or 1.5 — the
     Windows and macOS laptop defaults — it produces stems of alternating
     width, which is worse than a slightly soft edge. Chromium on Linux
     ignores the property, so leaving it in would have shipped a face nobody
     here could see go wrong. */
  text-rendering: optimizeLegibility;
  font-variant-ligatures: none;
  padding-bottom: var(--prompt-h);
}

.page {
  max-width: var(--page-w);
  margin: 0 auto;
  padding: 0 var(--gap) var(--gap);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--bg);
  color: var(--fg-bright);
  border: 1px solid var(--fg);
  padding: var(--pad);
}
.skip-link:focus { left: var(--pad); top: var(--pad); }

/* Screen-reader-only. */
.vh {
  position: absolute !important;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ==========================================================================
   MASTHEAD — wordmark and navigation, at the top
   ========================================================================== */

.masthead {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--bg);
  padding: var(--gap) 0 8px;
  border-bottom: 1px solid var(--fg-dim);
  margin-bottom: var(--gap);
}

.masthead__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 2ch;
  margin-bottom: 0.5em;
}

.wordmark {
  font-size: inherit;
  font-weight: 400;
  letter-spacing: 0.15em;
}
.wordmark a {
  color: var(--fg-bright);
  text-decoration: none;
}
.wordmark a:hover { text-decoration: underline; }

.tagline { color: var(--fg-dim); }
.tagline a { color: var(--fg-dim); }
.tagline a:hover { color: var(--fg-bright); }

.nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0 2ch;
  align-items: baseline;
}
.nav a {
  color: var(--fg);
  text-decoration: none;
  padding: 2px 0;
  white-space: nowrap;
}
.nav a::before { content: '· '; color: var(--fg-dim); }
.nav a:hover,
.nav a:focus-visible {
  color: var(--fg-bright);
  text-decoration: underline;
  text-underline-offset: 3px;
  outline: none;
}
/* The place you are currently reading. Marked with a glyph as well as a
   weight, so it is not carried by colour alone. */
.nav a[aria-current="page"] { color: var(--fg-bright); }
.nav a[aria-current="page"]::before { content: '→ '; color: var(--fg-bright); }

.nav--minor { margin-top: 2px; }
.nav--minor a { color: var(--fg-dim); }
.nav--minor a:hover { color: var(--fg); }
.nav__label {
  color: var(--fg-dim);
  white-space: nowrap;
}
.nav__label::after { content: ':'; }

@media (max-width: 640px) {
  /* The masthead stops sticking on small screens: three wrapped rows of nav
     would eat a third of a phone viewport and follow the reader down the
     page while doing it. */
  .masthead { position: static; }
  .tagline__sep { display: none; }
  .tagline__studio { display: block; }
}

/* ==========================================================================
   COLUMNS — content, and the telemetry pane beside it
   ========================================================================== */

.columns {
  display: grid;
  gap: var(--gap);
  /* One column by default. The telemetry pane is JS-only, so reserving a
     track for it up front would leave a 26ch strip of nothing beside the
     content for every reader who does not run scripts. */
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
@media (min-width: 901px) {
  :root.has-js .columns { grid-template-columns: minmax(0, 1fr) 26ch; }
}

/* ==========================================================================
   PANES
   Drawn with a 1px border and a label notched into the top edge, which is
   what `┌ /tower ─────┐` looks like once it is a rule rather than a glyph.
   ========================================================================== */

.pane {
  position: relative;
  border: 1px solid var(--fg-dim);
  min-width: 0;
}
.pane--main { border-color: var(--fg); }

.pane__head {
  position: absolute;
  top: 0;
  left: 2ch;
  transform: translateY(-50%);
  background: var(--bg);
  padding: 0 1ch;
  color: var(--fg-dim);
  font-size: inherit;
  font-weight: 400;
  line-height: 1;
  white-space: nowrap;
  max-width: calc(100% - 4ch);
  overflow: hidden;
  text-overflow: ellipsis;
}
.pane--main .pane__head { color: var(--fg-bright); }

.pane__body { padding: calc(var(--pad) + 4px) var(--pad) var(--pad); }

/* The telemetry pane follows the reader down the page, because a live tick
   that scrolls out of sight is just a number. */
.pane--orb {
  position: sticky;
  top: 8em;
}
@media (max-width: 900px) {
  /* relative, NOT static. Cancelling the sticky behaviour with `static` also
     removes the containing block that .pane__head is absolutely positioned
     against — which sent the "orb" label to the top-left corner of the whole
     document, floating above the masthead. */
  .pane--orb { position: relative; }
}

/* ==========================================================================
   ORB PANE — the game's own telemetry rows, and only the ones this site can
   honestly compute. crates/orbs/src/shell/prompt.rs draws tick / held / tier /
   cols / rows / logged / queued / seed / focus. `tier` is deliberately absent
   here: Fidelity::FINEST is scale 1 while the design doc calls the finer tier
   "2", so the number runs opposite ways in the two sources and printing it
   would put a wrong reading next to the game's right one.
   ========================================================================== */

.orb-table {
  font: inherit;
  color: var(--fg);
  white-space: pre;
  overflow-x: auto;
}
.orb-table__head { color: var(--fg-dim); }
/* inline, not block: the rows are separated by real newlines inside a <pre>,
   so a block would put a second line break after each one and double the
   pane's height. On narrow screens white-space:normal collapses those
   newlines and the margin below does the separating instead. */
.orb-row { display: inline; }
.orb-row__name { color: var(--fg-dim); }
.orb-row__value { color: var(--fg); }

.orb-hint { margin-top: var(--row); }

@media (max-width: 900px) {
  /* One compact strip rather than a column — the game's own Wide focus mode,
     where parity is mandatory: the strip carries the same readings, never
     fewer. Only the rendering differs. */
  .orb-table { white-space: normal; }
  .orb-row {
    margin-right: 2ch;
    white-space: nowrap;
  }
  .orb-table__head { display: none; }
}

/* ==========================================================================
   THE COMMAND LINE — fixed to the bottom of the viewport
   ========================================================================== */

.prompt {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  background: var(--bg);
  border-top: 1px solid var(--fg-dim);
}

/* The label wraps the whole line for accessibility and does the layout
   itself, so the head and the field are direct flex siblings. */
.prompt__line {
  display: flex;
  align-items: flex-start;
  gap: 1ch;
  max-width: var(--page-w);
  margin: 0 auto;
  padding: 0.5em calc(var(--gap) + var(--pad));
}

.prompt__head {
  flex: 0 0 auto;
  color: var(--fg-dim);
  white-space: nowrap;
}

.prompt__field {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

/* The input and the mirror must agree on EVERY metric that affects where a
   glyph lands, or the caret drifts away from the text it is supposed to be
   sitting in. An <input> does not inherit font or line-height, and ships its
   own padding and border — so all four are set explicitly on both, and both
   are given the same fixed height. This is what was misaligned before. */
.prompt__input,
.prompt__mirror {
  font: inherit;
  font-family: var(--font-screen);
  line-height: var(--line);
  letter-spacing: inherit;
  margin: 0;
  padding: 0;
  border: 0;
  height: calc(1em * var(--line));
  white-space: pre;
}

.prompt__input {
  display: block;
  width: 100%;
  background: none;
  outline: none;
  /* The real input carries the behaviour; the mirror draws the text, so that
     the caret can be a block cell rather than a bar. */
  color: transparent;
  caret-color: transparent;
  position: relative;
  z-index: 1;
}
.prompt__input::placeholder { color: transparent; }
.prompt__input:disabled { cursor: default; }

.prompt__mirror {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  overflow: hidden;
  pointer-events: none;
  color: var(--fg-bright);
}
.prompt__ghost { color: var(--fg-dim); }

.prompt__caret {
  background: var(--fg);
  color: var(--bg);
  /* One cell, and exactly one cell. A cell with nothing in it still has to be
     a full cell wide, and the height is pinned to 1em rather than left to the
     1.5 line-height — otherwise the block is 8x24 against a face drawn on an
     8x16 grid, which reads as a bar floating in the line rather than as a
     terminal cursor sitting in a cell. text-bottom lands it exactly on the
     glyph box of the text either side of it. */
  display: inline-block;
  min-width: 1ch;
  height: 1em;
  line-height: 1em;
  vertical-align: text-bottom;
}
.prompt__caret.is-ghost { background: var(--fg-dim); }
.prompt__field:not(.is-focused) .prompt__caret {
  background: none;
  color: var(--fg-dim);
  box-shadow: inset 0 0 0 1px var(--fg-dim);
}
@media not (prefers-reduced-motion: reduce) {
  .prompt__field.is-focused .prompt__caret { animation: blink 1.06s steps(1) infinite; }
}
@keyframes blink {
  0%, 50% { background: var(--fg); color: var(--bg); }
  50.01%, 100% { background: none; color: var(--fg); }
}

/* ==========================================================================
   OUTPUT — records, echoes, and the boot report
   ========================================================================== */

/* An empty transcript takes no room at all — otherwise every page carries a
   band of blank space under it waiting for output that may never come. */
.transcript:empty { display: none; }
.transcript {
  margin-top: calc(var(--row) * 1.5);
  border-top: 1px dashed var(--fg-dim);
  padding-top: var(--row);
}
.transcript > * + * { margin-top: var(--row); }

.echo { color: var(--fg-dim); }
.echo__head { margin-right: 1ch; }
.echo__cmd { color: var(--fg-bright); }

/* Verbatim game output. Scrolls in its own box rather than widening the page —
   the body must never scroll horizontally. */
pre.screenful,
.record pre {
  font: inherit;
  white-space: pre;
  overflow-x: auto;
  max-width: 100%;
  color: var(--fg);
  scrollbar-width: thin;
}

/* The boot report — the first thing on the page, and a status readout rather
   than a headline, so it sits at the recessive weight with only its verdicts
   accented. */
.boot {
  color: var(--fg-dim);
  margin-bottom: calc(var(--row) * 1.5);
  /* Reserve the report's full height up front so the typewriter doesn't push
     the page around as it fills in. 12 lines, matching BOOT_LINES. */
  min-height: calc(var(--row) * 12);
}

/* The accent triad. Each is paired with a glyph or a word in the markup —
   colour never carries meaning alone. */
.ok { color: var(--success); }
.err { color: var(--danger); }
.cost { color: var(--cost); }
.dim { color: var(--fg-dim); }
.bright { color: var(--fg-bright); }

/* ==========================================================================
   CONTENT
   ========================================================================== */

.doc { max-width: var(--measure); }
.doc > * + * { margin-top: var(--row); }

/* The screens view carries pictures rather than prose, so it is allowed to be
   wider than the reading measure. Its paragraphs keep the measure. */
.doc--wide { max-width: 100%; }
.doc--wide > p { max-width: var(--measure); }

/* Without JS every section is present in one document, so they need rules
   between them; with JS only one is shown at a time and the rule would be a
   line under nothing. */
.view + .view {
  margin-top: calc(var(--row) * 2);
  border-top: 1px solid var(--fg-dim);
  padding-top: calc(var(--row) * 2);
}

.doc h2,
.doc h3 {
  color: var(--fg-bright);
  font-size: inherit;
  font-weight: 400;
}
.doc h2::before { content: '# '; color: var(--fg-dim); }
.doc h3::before { content: '## '; color: var(--fg-dim); }

.doc p { color: var(--fg); }
.doc strong { color: var(--fg-bright); font-weight: 400; }
.doc em { font-style: normal; color: var(--fg-bright); }

a { color: inherit; }

.doc a,
.link,
.site-footer a {
  color: var(--fg-bright);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.doc a:hover,
.link:hover,
.site-footer a:hover { background: var(--fg-bright); color: var(--bg); }

.doc ul { list-style: none; }
.doc li { padding-left: 2ch; text-indent: -2ch; }
.doc li::before { content: '· '; color: var(--fg-dim); }

.lede { color: var(--fg-bright); }

/* A run of key/value rows, the shape the game prints most. */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 0 2ch; }
.kv dt { color: var(--fg-dim); }
.kv dd { color: var(--fg); }
@media (max-width: 560px) {
  /* max-content on a narrow screen squeezes the value column to nothing. */
  .kv { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .kv dd { margin-bottom: 0.5em; }
}

kbd {
  font: inherit;
  border: 1px solid var(--fg-dim);
  padding: 0 0.5ch;
  color: var(--fg);
}

/* A filed screenshot and its text alternative. */
.shot { margin-top: var(--row); }
.shot img {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
  border: 1px solid var(--fg-dim);
  /* NOT image-rendering: pixelated. That is the right call when a pixel grid
     is being enlarged; here it is always being reduced, and nearest-neighbour
     on the way down throws away two thirds of the rows and turns the glyphs
     into noise. Smooth downscaling keeps the text readable. */
}
.shot figcaption { margin-top: var(--row); }
.shot figcaption > * + * { margin-top: calc(var(--row) * 0.5); }
.shot figcaption p { max-width: var(--measure); }

.shot summary {
  cursor: pointer;
  color: var(--fg-bright);
  list-style: none;
}
.shot summary::-webkit-details-marker { display: none; }
.shot summary::before { content: '▸ '; color: var(--fg-dim); }
.shot details[open] > summary::before { content: '▾ '; }
.shot summary:hover,
.shot summary:focus-visible { text-decoration: underline; outline: none; }
.shot details > * + * { margin-top: calc(var(--row) * 0.5); }

/* An in-band button that runs a command. JS-only by nature — it is styled as
   a link so it reads as one, and it is hidden without JS along with the rest
   of the command surface. */
.run {
  background: none;
  border: 0;
  font: inherit;
  color: var(--fg-bright);
  cursor: pointer;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.run:hover,
.run:focus-visible { background: var(--fg-bright); color: var(--bg); outline: none; }

/* ==========================================================================
   FOOTER
   ========================================================================== */

.site-footer {
  margin-top: calc(var(--row) * 2);
  padding-top: var(--row);
  border-top: 1px solid var(--fg-dim);
  color: var(--fg-dim);
}
.site-footer > * + * { margin-top: 0.5em; }
.site-footer a { color: var(--fg); }

/* ==========================================================================
   NO-JS
   The static document is the whole site. .has-js is added by main.js itself —
   never by an inline head script — so a blocked or broken main.js leaves a
   complete, readable page rather than a half-wired one.
   ========================================================================== */

.js-only { display: none; }
:root.has-js .js-only { display: revert; }

/* With JS, one section is attended at a time and the rules between them go
   away with the sections. */
:root.has-js .view { display: none; }
:root.has-js .view.is-active {
  display: block;
  margin-top: 0;
  border-top: 0;
  padding-top: 0;
}
