/* A faux terminal: amber phosphor on a warm black, the home page's burnt
   orange carried over as the prompt colour. Everything sits on one character
   grid, so the screen is a single <pre> and machina.js can map the pointer
   to a cell.

   Contrast against --bg: amber 11:1, the prompt orange 6.9:1, the dimmed
   hex bytes 4.1:1 (they only stay dim until they are decoded). */

:root {
  --bg: #0f0a06;
  --fg: #ffb547;
  --dim: #9a6a2a;
  --hot: #ff6a3d;
  --glow: rgba(255, 150, 40, 0.45);
  /* Line height of the grid. The art is made of CP437 half and full blocks,
     which read as solid shapes only when the rows nearly touch. */
  --lh: 1.07;
  /* The grid: 72 columns (the lettering's width) by 30 rows, matching the
     rows in index.html. Size the type so the whole screen fits the viewport
     in both directions: a monospace cell is about 0.6em wide (0.61 leaves
     slack for fonts that run slightly wider), and the status line and
     margins take about 5rem of the height. 6px is the floor; below it the
     screen scrolls sideways inside main rather than the page. */
  --cols: 72;
  --rows: 30;
  --term-size: clamp(6px, min((100vw - 2rem) / (var(--cols) * 0.61), (100vh - 5rem) / (var(--rows) * var(--lh))), 22px);
  /* GitHub's code font stack, which the art was drawn for. The home page's
     footer prompt (style.css, .prompt) copies it. */
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  color-scheme: dark only;
}

@supports (height: 100dvh) {
  :root {
    --term-size: clamp(6px, min((100vw - 2rem) / (var(--cols) * 0.61), (100dvh - 5rem) / (var(--rows) * var(--lh))), 22px);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  background: var(--bg);
}

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--mono);
}

::selection {
  background: var(--fg);
  color: var(--bg);
}

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

main {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 1.5rem 1rem;
  overflow-x: auto;
  /* Eases the vertical-hold slip (main.slip, under "Signal decay"). */
  transition: transform 0.35s ease-in-out;
}

/* ---- The screen ---- */
.term {
  position: relative;
  /* Auto margins centre the screen, and drop to zero rather than push it off
     the left edge when it is wider than main and has to scroll. */
  margin: auto;
  /* The full grid width up front, so the screen doesn't widen, and shift to
     stay centred, as longer rows play in. 1ch is one cell in a monospace
     font. */
  width: calc(var(--cols) * 1ch);
  /* And the full height: while the last row is still empty, the newline
     before it ends the text and draws no line of its own. */
  height: calc(var(--rows) * var(--lh) * 1em);
  font-family: var(--mono);
  font-size: var(--term-size);
  line-height: var(--lh);
  white-space: pre;
  /* Phosphor bloom: a tight halo for the glyph edges and a wide, faint one
     for the light spilling onto the glass. The faint copy a third of a cell
     to the right is ghosting from a tired cable. */
  text-shadow:
    0 0 1px var(--glow),
    0 0 0.6em rgba(255, 140, 30, 0.22),
    0.35em 0 1px rgba(255, 181, 71, 0.1);
  font-variant-ligatures: none;
  tab-size: 8;
  /* The signal never quite holds: see "Signal decay" below. */
  animation: unsteady 17s linear infinite;
}

.ps1 {
  color: var(--hot);
}

.fig {
  text-shadow:
    0 0 1px var(--glow),
    0 0 0.5em rgba(255, 150, 40, 0.5),
    0.35em 0 1px rgba(255, 181, 71, 0.1);
}

/* The hex bytes start dim and light up as machina.js decodes them; without
   the playback they are simply lit. */
.term.playing .hex .b {
  color: var(--dim);
  text-shadow: none;
}

.term.playing .hex .b.lit {
  color: var(--fg);
  text-shadow: inherit;
}

.term .hex .b.hot {
  background: var(--fg);
  color: var(--bg);
  text-shadow: none;
}

.caret {
  background: var(--fg);
  box-shadow: 0 0 0.5em var(--glow);
  animation: blink 1.06s step-end infinite;
}

/* The caret holds steady from the first keystroke until the command is
   done, as many terminals hold it while busy, and blinks at the prompt. */
.term.typing .caret {
  animation: none;
}

@keyframes blink {
  50% {
    opacity: 0;
  }
}

/* Hide the finished screen until machina.js blanks it to play it back. If the
   script never arrives, show it anyway after two seconds. */
.js .term:not(.live) {
  visibility: hidden;
  animation: show-anyway 0s 2s forwards;
}

@keyframes show-anyway {
  to {
    visibility: visible;
  }
}

/* ---- CRT glass ---- */
/* Scanlines and a vignette over everything, the status line included. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background:
    radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.55) 100%),
    linear-gradient(rgba(0, 0, 0, 0.22) 50%, transparent 50%) 0 0 / 100% 3px;
}

/* A soft band of brighter phosphor that rolls down the screen. */
body::before {
  content: "";
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  height: 25vh;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(transparent, rgba(255, 170, 60, 0.035), transparent);
  transform: translateY(-25vh);
  animation: roll 14s linear infinite;
}

@keyframes roll {
  to {
    transform: translateY(100vh);
  }
}

/* ---- Signal decay ---- */
/* The terminal is some 38,000 years old and its link is failing. Static
   crawls over the glass, the picture dims and drifts at odd moments, and
   machina.js adds the occasional fault: a band of rows tearing sideways
   with colour fringes, glyphs coming through corrupted, a burst of static,
   the vertical hold slipping. */

/* Static: amber speckle from an SVG turbulence tile, jumped to a new offset
   five times a second. The layer overhangs the viewport by the largest
   jump, so its edge never shows; translating it is a compositor-only
   change. */
.grain {
  position: fixed;
  inset: -48px;
  z-index: 3;
  pointer-events: none;
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 0.75  0 0 0 0 0.4  2.2 0 0 0 -1'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 160px 160px;
  opacity: 0.2;
  /* A cold screen comes up in a wash of static that settles. */
  animation:
    grain 2s steps(1) infinite,
    warm-up 2.6s ease-out;
  /* Bursts of static (below) swell and fade rather than snap. */
  transition: opacity 0.3s ease-out;
}

@keyframes grain {
  0% { transform: translate(0, 0); }
  10% { transform: translate(-31px, 17px); }
  20% { transform: translate(23px, -41px); }
  30% { transform: translate(-12px, 38px); }
  40% { transform: translate(44px, 9px); }
  50% { transform: translate(-40px, -22px); }
  60% { transform: translate(15px, 29px); }
  70% { transform: translate(-25px, -8px); }
  80% { transform: translate(36px, -33px); }
  90% { transform: translate(-6px, 45px); }
}

@keyframes warm-up {
  from {
    opacity: 0.7;
  }
}

/* machina.js floods the glass with static for a moment. */
.static-burst .grain {
  opacity: 0.45;
}

/* Brightness sags at uneven intervals. Most of the cycle is steady; each
   sag dims over about a tenth of a second and recovers over two tenths.
   Twice a cycle the picture also drifts 2px sideways and back. That moves
   everything at once, which is hard on the eyes when it snaps, so it eases
   out over about 0.2s and back over 0.5s. */
@keyframes unsteady {
  0%, 100% { opacity: 1; transform: none; }
  8% { opacity: 1; }
  8.6% { opacity: 0.88; }
  9.8% { opacity: 1; }
  31% { opacity: 0.97; }
  37% { opacity: 1; transform: none; animation-timing-function: ease-in-out; }
  37.6% { opacity: 0.85; }
  38.2% { transform: translateX(2px); animation-timing-function: ease-in-out; }
  38.8% { opacity: 1; }
  41.2% { transform: none; }
  64% { opacity: 1; }
  64.6% { opacity: 0.92; }
  65.8% { opacity: 1; }
  81% { opacity: 1; transform: none; animation-timing-function: ease-in-out; }
  81.6% { opacity: 0.88; }
  82.2% { transform: translateX(-2px); animation-timing-function: ease-in-out; }
  82.8% { opacity: 1; }
  85.2% { transform: none; }
}

/* The vertical hold slips: the screen eases up about half a row (main's
   transition), machina.js holds it there for a moment, and it eases back.
   A straight lift, measured in the screen's own type: a lean would skew
   the grid the pointer cell snaps to. */
main.slip {
  transform: translateY(calc(-0.6 * var(--term-size)));
}

/* A corrupted glyph, drawn over the real one at its cell rather than
   written into the row, so selecting and copying the screen never meets it
   (the glyph is generated content). */
.debris {
  position: absolute;
  left: calc(var(--col) * 1ch);
  top: calc(var(--row) * var(--lh) * 1em);
  width: 1ch;
  height: calc(var(--lh) * 1em);
  background: var(--bg);
  pointer-events: none;
  user-select: none;
}

.debris::before {
  content: attr(data-ch);
}

/* A torn row: shifted sideways (relative offsets apply to inline boxes),
   with the red and blue of a misconverged gun on either side. */
.torn {
  position: relative;
  text-shadow:
    -0.15em 0 rgba(255, 40, 60, 0.6),
    0.15em 0 rgba(60, 210, 255, 0.45),
    0 0 0.6em rgba(255, 140, 30, 0.22);
}

/* ---- Pointer ---- */
/* machina.js adds these only for a fine pointer that can hover. The cell is
   a reverse-video block snapped to the grid, the way a terminal draws its
   mouse cursor; the trail cells are where it has just been, fading like
   phosphor; the glow is a wide warm light under the glass. */
.has-pointer main {
  cursor: none;
}

.cursor-layer {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.cell {
  position: absolute;
  left: 0;
  top: 0;
  font-family: var(--mono);
  font-size: var(--term-size);
  line-height: var(--lh);
  white-space: pre;
  text-align: center;
  background: var(--fg);
  color: var(--bg);
  will-change: transform;
}

.cell--head {
  box-shadow: 0 0 0.8em var(--glow);
}

.cell--trail {
  opacity: 0;
}

.glow {
  position: fixed;
  left: -20rem;
  top: -20rem;
  z-index: 0;
  width: 40rem;
  height: 40rem;
  pointer-events: none;
  background: radial-gradient(closest-side, rgba(255, 150, 40, 0.09), transparent);
  will-change: transform;
}

/* ---- Status line ---- */
.status {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0 1.5ch;
  padding: 0.15rem 1ch;
  background: var(--fg);
  color: var(--bg);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.status__right {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.5ch;
}

.status__left,
.status__right > * {
  white-space: pre;
}

.status a {
  color: inherit;
  text-decoration: none;
}

.status a:hover,
.status a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.status a:focus-visible {
  outline: 2px solid var(--bg);
  outline-offset: 2px;
}

.status__clock:empty {
  display: none;
}

/* The ruler takes its place in the status line as soon as machina.js finds
   a fine pointer, at one width (its longest text, "0x44 'D'  25,15", is 15
   characters) whatever it says, and shows once the pointer moves. A ruler
   that appeared, or grew and shrank, could wrap the status line, resize
   main and move the screen under a still pointer. */
.status__ruler {
  display: none;
}

.fine-pointer .status__ruler {
  display: inline-block;
  min-width: 16ch;
  text-align: right;
  visibility: hidden;
}

.has-pointer .status__ruler {
  visibility: visible;
}

/* ---- Preferences ---- */
/* Reduced motion keeps the static as a still texture and drops everything
   that moves or flickers; machina.js holds back its faults too. (The
   show-anyway fallback above is more specific than .term, so it stays.) */
@media (prefers-reduced-motion: reduce) {
  .caret,
  .term,
  .grain,
  body::before {
    animation: none;
  }

  body::before {
    display: none;
  }
}

/* More contrast: plain glyphs on a flat field. No halos, ghosts or colour
   fringes, no glow around the caret or the pointer cell, no static, glass
   or flicker and drift. (Faults still tear, corrupt and slip, without the
   fringes.) */
@media (prefers-contrast: more) {
  .term,
  .fig,
  .torn {
    text-shadow: none;
  }

  .caret,
  .cell--head {
    box-shadow: none;
  }

  .term {
    animation: none;
  }

  body::after,
  body::before,
  .glow,
  .grain {
    display: none;
  }
}
