/*
 * The page wants to read like the terminal it is showing, without pretending
 * to be a terminal emulator: one monospace family, block characters for the
 * bars and the histograms, and no colour carrying meaning on its own.
 */

:root {
  --ink: #e8e2d4;
  --ink-dim: #9a9383;
  --paper: #14110d;
  --paper-raised: #1d1913;
  --rule: #332c22;
  --steel: #c8a34a;
  --blood: #a8452f;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --ink: #221c14;
    --ink-dim: #5f5748;
    --paper: #f4efe4;
    --paper-raised: #fffdf7;
    --rule: #d8cfbb;
    --steel: #8a6d1f;
    --blood: #8c3423;
  }
}

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

/*
 * The torch's position, registered so it can be animated at all.
 *
 * An unregistered custom property has no type, so the browser cannot
 * interpolate between two values of it and a transition on it does nothing —
 * the mask simply snaps to each new coordinate. Declaring the syntax is what
 * turns the jump into a glide, and it is the whole reason these two are here
 * rather than written straight into the gradient.
 */
@property --tx {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 70%;
}

@property --ty {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 25%;
}

/*
 * The blade, lit by the pointer.
 *
 * The darkness is the mask's, not the image's: the picture is a lit blade on
 * black, and what makes it "barely there" is that only a circle of it is ever
 * let through. Opacity is capped because the steel carries real highlights and
 * the tables have to stay readable over them.
 */
.blade {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("blade.jpg");
  background-size: cover;
  background-position: center;
  /*
   * A wide, soft, dim torch reads as haze rather than steel: the grain is lost
   * to the falloff long before the eye finds it. A tighter circle with a
   * harder edge and more contrast shows the forge marks, and keeps the lit
   * area small enough that the text it crosses stays legible.
   */
  /*
   * Held as variables so the light theme can swap the treatment without
   * restating the geometry below it.
   */
  --blade-opacity: 0.3;
  --blade-filter: contrast(1.35) brightness(0.72);
  --blade-blend: normal;

  opacity: var(--blade-opacity);
  filter: var(--blade-filter);
  mix-blend-mode: var(--blade-blend);

  /* Set by the pointer; these are the fallback until it moves. */
  --tx: 70%;
  --ty: 25%;

  /*
   * The light trails the pointer rather than teleporting to it. Linear, not
   * eased: the torch is chasing a moving target and an ease-out reads as lag.
   * Each update interrupts the one before it, which is what makes a stream of
   * discrete coordinates look like one continuous sweep.
   */
  transition:
    --tx 140ms linear,
    --ty 140ms linear;

  /*
   * The gradient is written out at both properties rather than held in a
   * custom property and pulled in with var(). Safari has long been unreliable
   * about an unregistered custom property standing in for a whole mask value,
   * and the failure is not a missing torch but a missing mask — the full
   * rectangle of steel over the page.
   */
  -webkit-mask-image: radial-gradient(
    circle 240px at var(--tx) var(--ty),
    #000 0%,
    rgba(0, 0, 0, 0.72) 45%,
    transparent 78%
  );
  mask-image: radial-gradient(
    circle 240px at var(--tx) var(--ty),
    #000 0%,
    rgba(0, 0, 0, 0.72) 45%,
    transparent 78%
  );
}

/*
 * A browser that cannot mask at all would otherwise show the whole plate of
 * steel across the page. Nothing is better than that.
 */
@supports not (
    (mask-image: radial-gradient(#000, transparent)) or
    (-webkit-mask-image: radial-gradient(#000, transparent))
  ) {
  .blade {
    display: none;
  }
}

/*
 * A finger is a pointer. Touch fires pointermove while it is down, so dragging
 * or scrolling sweeps the torch across the steel, which is the same gesture
 * the mouse makes and wants no special case. What a phone does need is a
 * wider, brighter circle: the screen is small, often held in daylight, and a
 * 240px window at the opacity a monitor wants is invisible out there.
 */
@media (pointer: coarse), (hover: none) {
  .blade {
    /*
     * Only the circle widens here. This block used to set `opacity` straight,
     * which beat the themed `opacity: var(--blade-opacity)` above it — so a
     * phone in light mode wore the highest opacity in the stylesheet instead
     * of the lowest. A theme's treatment has to survive the device's.
     */
    /*
     * Longer, because a finger arrives rather than travels: a tap is a single
     * coordinate a long way from the last one, and at 140ms that still reads
     * as a jump. At 320ms the light sweeps across to meet it.
     */
    transition:
      --tx 320ms ease-out,
      --ty 320ms ease-out;
    -webkit-mask-image: radial-gradient(
      circle 300px at var(--tx) var(--ty),
      #000 0%,
      rgba(0, 0, 0, 0.75) 50%,
      transparent 82%
    );
    mask-image: radial-gradient(
      circle 300px at var(--tx) var(--ty),
      #000 0%,
      rgba(0, 0, 0, 0.75) 50%,
      transparent 82%
    );
  }
}

/*
 * A light that chases the pointer is motion, and some people have asked for
 * none. !important because the pointer writes --tx and --ty as inline styles,
 * and an inline style beats a stylesheet rule — without it this block would be
 * a comment rather than a behaviour.
 */
@media (prefers-reduced-motion: reduce) {
  .blade {
    --tx: 72% !important;
    --ty: 22% !important;
    transition: none !important;
  }
}

/*
 * Light mode gets the same steel read as a shadow rather than a gleam.
 *
 * Inverted, the picture's black ground turns white and disappears into the
 * paper, and the lit bevel becomes the dark mark the torch finds; multiplied,
 * it darkens the page where the blade is instead of laying grey over it. This
 * block used to be `display: none`, on the untested assumption that dark steel
 * on pale paper could only be a smudge — which meant every visitor whose
 * device prefers light never saw the effect at all.
 */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .blade {
    --blade-opacity: 0.2;
    --blade-filter: invert(1) contrast(1.2) brightness(1.02);
    --blade-blend: multiply;
  }
}

/* The same, for a page told explicitly which theme to wear. */
:root[data-theme="light"] .blade {
  --blade-opacity: 0.2;
  --blade-filter: invert(1) contrast(1.2) brightness(1.02);
  --blade-blend: multiply;
}

/* The page ends in the sea: no bounce past it into bare paper. */
html {
  overscroll-behavior-y: none;
}

body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.6;
}

main {
  max-width: 62rem;
  margin: 0 auto;
  /* Little at the foot: the sea comes up close under the last line. */
  padding: 2.5rem 16px 0.5rem;
}

h1 {
  margin: 0;
  font-size: clamp(2rem, 8vw, 3.2rem);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

h2 {
  margin: 2.5rem 0 0.75rem;
  font-size: 1.05rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--steel);
}

.tagline {
  margin: 0.25rem 0 1.5rem;
  color: var(--ink-dim);
}

.quote {
  margin: 0 0 1.5rem;
  padding-left: 1rem;
  border-left: 2px solid var(--blood);
  color: var(--ink);
}

.attribution {
  display: block;
  color: var(--ink-dim);
}

.note,
.aside,
.lede {
  color: var(--ink-dim);
  max-width: 62ch;
}

.lede {
  color: var(--ink);
}

/* The menu is the terminal's menu: one screen visible at a time. */
.menu {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 2rem 0 1rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}

.menu-item {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.5rem 0.85rem;
  background: var(--paper-raised);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 2px;
  font: inherit;
  cursor: pointer;
}

.menu-item:hover {
  border-color: var(--steel);
}

.menu-item.active {
  border-color: var(--steel);
  color: var(--steel);
}

.menu-item:focus-visible {
  outline: 2px solid var(--steel);
  outline-offset: 2px;
}

.menu-number {
  color: var(--ink-dim);
}

.panel {
  padding-top: 0.5rem;
}

/* Tables carry the numbers, and scroll rather than reflow on a phone. */
table {
  width: 100%;
  margin: 1rem 0;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  display: block;
  overflow-x: auto;
  white-space: nowrap;
}

th,
td {
  padding: 0.4rem 0.6rem;
  text-align: left;
  border-bottom: 1px solid var(--rule);
}

th {
  color: var(--ink-dim);
  font-weight: normal;
  letter-spacing: 0.04em;
}

td.num,
th:has(+ td.num) {
  text-align: right;
}

.num {
  text-align: right;
}

.warrior {
  display: block;
}

.address,
.title {
  display: block;
  color: var(--ink-dim);
  font-size: 0.85em;
}

.bar,
.spark {
  color: var(--steel);
  letter-spacing: -0.05em;
}

.legend {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.2rem 1rem;
  margin: 1.5rem 0 0;
  color: var(--ink-dim);
}

.legend dt {
  color: var(--steel);
}

.legend dd {
  margin: 0;
}

.candidates {
  padding-left: 1.2rem;
}

.candidates ul {
  padding-left: 1.2rem;
  list-style: none;
}

pre {
  margin: 1rem 0;
  padding: 0.9rem 1rem;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: 2px;
  overflow-x: auto;
}

code,
kbd {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 0 0.3em;
}

kbd {
  color: var(--steel);
}

.disclaimer {
  padding-left: 1.2rem;
  max-width: 68ch;
}

footer {
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}

a {
  color: var(--steel);
}

/* The Søbernetics mark. Small on purpose, and small is the whole of how it stays quiet: the
   opacity this first had took it below the contrast required of a link. Subtlety is size, not a
   washed-out colour. The red is Søbernetics' own and does not follow this page's palette. */
.sobernetics {
  --sobernetics-accent: #f44851;
  display: inline-block;
  margin-top: 1.5rem;
  color: var(--ink-dim);
}

@media (prefers-color-scheme: light) {
  .sobernetics {
    --sobernetics-accent: #c8102e;
  }
}

.sobernetics:hover,
.sobernetics:focus-visible {
  color: var(--ink);
}

.sobernetics svg {
  display: block;
  height: 18px;
  width: auto;
}

/*
 * The dialog the game opens in, and the hint at the foot of the page. The
 * game's own styles come with the game (site/descent/descent.css).
 */
.descent-dialog {
  width: 100vw;
  height: 100dvh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: auto;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--mono);
}

.descent-dialog::backdrop {
  background: rgb(0 0 0 / 0.72);
}

/* Under the water, the game fills the window, and comes up out of the dark. */
.descent-dialog[open] {
  animation: rise 600ms ease-out;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(4vh);
  }
}

@media (prefers-reduced-motion: reduce) {
  .descent-dialog[open] {
    animation: fade 250ms ease-out;
  }
}

@keyframes fade {
  from {
    opacity: 0;
  }
}

.ebb {
  margin-top: 24px;
  margin-bottom: 0;
  color: var(--ink-dim);
  font-size: 0.8rem;
}

/*
 * The sea at the foot of the page. The page fades into it; mirror.js draws
 * the water itself once it comes near the screen, and until then, or without
 * WebGL, this gradient is the water.
 */
.mirror {
  position: relative;
  height: clamp(120px, 18vh, 220px);
  background: linear-gradient(to bottom, transparent, #2a2114 70%, #1e170d);
  cursor: pointer;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
}

.mirror.has-water {
  background: none;
}

.mirror:focus-visible {
  outline: 2px solid var(--steel);
  outline-offset: -4px;
}

.mirror__water {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* The one word the water says, the first time it is touched. */
.mirror__hint {
  position: absolute;
  transform: translate(-50%, -160%);
  color: var(--ink-dim);
  font-size: 0.85rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity 600ms ease;
  pointer-events: none;
}

.mirror__hint.is-shown {
  opacity: 0.85;
}

/* The hold, as a line, for someone who asked for less motion. */
.mirror__progress {
  display: none;
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  background: var(--steel);
  transform: scaleX(0);
  transform-origin: left;
}

@media (prefers-reduced-motion: reduce) {
  .mirror__progress {
    display: block;
  }

  .mirror__hint {
    transition: none;
  }
}

/* The water over the whole window, for the second it takes to go down or up. */
.mirror__veil {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 1000;
  pointer-events: none;
}
