:root {
  --void: #000;
  --ash: #4c5054;      /* log noise, quiet chrome */
  --concrete: #8d9296; /* secondary text */
  --bone: #e8e2d8;     /* primary text, the colour of bone */
  --blood: #b3121b;    /* the owner's colour: wounds, drips, hovers */
  --mono: 'Martian Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --display: 'Grenze Gotisch', 'UnifrakturMaguntia', serif;
  --gutter: clamp(16px, 4vw, 48px);
  --ease: cubic-bezier(.7, 0, .2, 1);
  --expo: cubic-bezier(.16, 1, .3, 1);   /* fast start, long soft landing */
  color-scheme: dark;
}

* { box-sizing: border-box; }
/* One screen, never scrolls. */
html, body { margin: 0; background: var(--void); height: 100%; overflow: hidden; overscroll-behavior: none; }
body {
  height: 100dvh;
  color: var(--bone);
  font: 400 13px/1.7 var(--mono);
  font-variation-settings: 'wdth' 87.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
[data-cursor='crosshair'] body, [data-cursor='crosshair'] a, [data-cursor='crosshair'] button { cursor: crosshair; }
[data-cursor='normal'] a, [data-cursor='normal'] button { cursor: pointer; }

/* The blood cursor: a dot right on the pointer and a ring that trails it. */
.has-cursor body, .has-cursor a, .has-cursor button, .has-cursor input { cursor: none; }
.cur-dot, .cur-ring {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 20;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
  transition: opacity .3s;
}
.cur-on .cur-dot, .cur-on .cur-ring { opacity: 1; }
.cur-dot {
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  background: var(--blood);
  box-shadow: 0 0 8px var(--blood);
}
.cur-ring {
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  border: 1px solid rgb(232 226 216 / .35);
  transition: opacity .25s, scale .35s var(--expo);
}
/* Over a link the ring gets out of the way; the underline does the talking. */
.cur-hover .cur-ring { opacity: 0; scale: .4; }
.cur-down .cur-ring { scale: .8; }
.is-diving ~ .cur-dot, .is-diving ~ .cur-ring { opacity: 0; }
::selection { background: var(--blood); color: var(--bone); }
/* Rapid clicks would select the name; nothing on the page is meant to be selected. */
.main, .entry, .corner { -webkit-user-select: none; user-select: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:focus-visible { outline: 1px solid var(--blood); outline-offset: 4px; }
[hidden] { display: none !important; }

/* Everything that switches off together when the connection closes. */
.screen { height: 100dvh; transform-origin: 50% 50%; }
/* A faint scanline texture over everything, so text and building read as one screen. */
.screen::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background: repeating-linear-gradient(to bottom, rgb(255 255 255 / .022) 0 1px, transparent 1px 3px);
}
.screen.is-off { animation: crt-off .62s var(--ease) forwards; }
@keyframes crt-off {
  0%   { transform: scale(1, 1); filter: brightness(1); }
  45%  { transform: scale(1, .004); filter: brightness(3); }
  100% { transform: scale(0, .004); filter: brightness(6); }
}

#field {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* ---------- Intro: a counter in the eye of the dust ---------- */

.entry {
  position: fixed;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
}
.intro {
  display: grid;
  justify-items: center;
  translate: 0 -4vh;
}
.count {
  grid-area: 1 / 1;
  margin: 0;
  font-size: 12px;
  letter-spacing: .4em;
  padding-left: .4em;
  color: var(--concrete);
  font-variant-numeric: tabular-nums;
  transition: opacity .5s var(--expo), filter .6s var(--expo), translate .8s var(--expo);
}
.count.is-done { opacity: 0; filter: blur(6px); translate: 0 -.9em; }
.handle {
  grid-area: 1 / 1;
  margin: 0;
  font: 600 clamp(36px, 4.6vw, 60px)/1 var(--display);
  letter-spacing: .01em;
  color: var(--bone);
  text-shadow: 0 0 26px color-mix(in srgb, var(--blood) 45%, transparent);
  min-height: 1em;
}
.mori {
  position: fixed;
  left: 0;
  right: 0;
  bottom: max(18px, env(safe-area-inset-bottom));
  margin: 0;
  text-align: center;
  font-size: 10px;
  letter-spacing: .5em;
  padding-left: .5em;
  color: var(--ash);
  animation: fade-in 2s 1s both;
}
.entry.is-gone .mori { opacity: 0; transition: opacity .4s; }

/* The shell in the corner while it loads: real-looking commands for what's
   actually being loaded. */
.sh {
  position: fixed;
  left: var(--gutter);
  bottom: var(--gutter);
  margin: 0;
  max-width: min(64ch, calc(100vw - 2 * var(--gutter)));
  font: inherit;
  font-size: 11px;
  line-height: 1.65;
  color: var(--ash);
  white-space: pre-wrap;
  word-break: break-all;
  transition: opacity .6s var(--expo), translate .8s var(--expo), filter .6s var(--expo);
}
.sh .p { color: var(--blood); }
.sh .c { color: var(--concrete); }
.sh .o { color: var(--ash); }
.sh .cur { display: inline-block; width: .6em; height: 1.1em; margin-bottom: -.2em; background: var(--concrete); animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
.entry.is-gone .sh { opacity: 0; translate: 0 -10px; filter: blur(6px); }
/* Phones: a short strip showing only the latest lines. */
@media (max-width: 640px) {
  .sh {
    font-size: 9px;
    bottom: 42px;
    max-height: calc(1.6em * 4);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    opacity: .8;
  }
  .sh .sh-in { display: block; }
}

.gate {
  grid-area: 2 / 1;
  display: grid;
  justify-items: center;
  gap: 4px;
  margin-top: 26px;
}
.gate .enter { animation: gate-in 1.1s var(--expo) both, beckon 2.6s 1.1s ease-in-out infinite; }
.gate-note { margin: 0; color: var(--ash); font-size: 11px; animation: fade-in 1s .35s both; }
@keyframes gate-in {
  from { opacity: 0; translate: 0 12px; filter: blur(8px); }
  to   { opacity: 1; translate: 0 0; filter: blur(0); }
}
@keyframes fade-in { from { opacity: 0; } }

/* The enter button: a clear box that fills in on hover or tap, with a slow
   red pulse so it reads as the thing to press. */
.enter {
  font-size: 13px;
  letter-spacing: .3em;
  padding: 15px 40px 15px calc(40px + .3em);
  min-width: 180px;
  color: var(--bone);
  border: 1px solid rgb(232 226 216 / .55);
  position: relative;
  isolation: isolate;
  animation: beckon 2.6s ease-in-out infinite;
  transition: color .3s, border-color .3s, scale .15s;
}
.enter::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--bone);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .45s var(--expo);
}
.enter:hover, .enter:active { color: var(--void); border-color: var(--bone); }
.enter:hover::before, .enter:active::before { transform: scaleX(1); }
.enter:active { scale: .97; }
.enter span { position: relative; }
.enter:focus-visible { outline: 1px solid var(--blood); outline-offset: 5px; }
@keyframes beckon {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50% { box-shadow: 0 0 22px 2px color-mix(in srgb, var(--blood) 45%, transparent); }
}
.entry.is-gone .gate .enter { animation: gate-out .6s cubic-bezier(.7, 0, .84, 0) forwards; }
.entry.is-gone .gate-note { animation: none; opacity: 0; transition: opacity .3s; }
@keyframes gate-out {
  to { opacity: 0; translate: 0 -14px; filter: blur(8px); }
}

/* ---------- Text that rises into place (see rise() in main.js) ---------- */

/* Each word is its own clip window; letters slide up into it. The padding
   leaves room for ascenders, descenders and the glow; .is-done removes the
   clip once everything has landed. */
.rise .w {
  display: inline-block;
  overflow: hidden;
  padding: .12em .06em .26em;
  margin: -.12em -.06em -.26em;
  vertical-align: top;
}
.rise .ch {
  display: inline-block;
  translate: 0 115%;
  opacity: 0;
  filter: blur(10px);
  transition: translate 1.1s var(--expo), opacity .8s var(--expo), filter 1.1s var(--expo);
  transition-delay: calc(var(--d, 0ms) + var(--i) * var(--stagger, 34ms));
}
.rise.is-in .ch { translate: 0 0; opacity: 1; filter: blur(0); }
.rise.is-done .w { overflow: visible; }
.rise.is-out .ch {
  translate: 0 -115%;
  opacity: 0;
  filter: blur(10px);
  transition-duration: .65s;
  transition-timing-function: cubic-bezier(.7, 0, .84, 0);
  transition-delay: calc(var(--i) * 22ms);
}

.fade-up {
  opacity: 0;
  translate: 0 .7em;
  filter: blur(8px);
  transition: opacity .9s var(--expo), translate 1.1s var(--expo), filter 1.1s var(--expo);
  transition-delay: var(--d, 0ms);
}
.fade-up.is-in { opacity: 1; translate: 0 0; filter: blur(0); }

/* ---------- Main: one centred column, vile-style ---------- */

.main {
  position: relative;
  z-index: 1;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 0 var(--gutter) clamp(40px, 8vh, 88px);
}
.main.is-pre { visibility: hidden; }
.spacer {
  /* The building takes whatever height the text leaves over. */
  flex: 1 1 auto;
  min-height: 90px;
  max-height: 600px;
  width: 1px;
  margin-top: 3vh;
}
.semi {
  margin: clamp(6px, 2vh, 22px) 0 .2em;
  color: var(--concrete);
  font-size: clamp(11px, 1.5vh, 12px);
  letter-spacing: .08em;
  min-height: 1.7em;
}
.name {
  margin: 0;
  font: 600 clamp(34px, min(6vw, 9vh), 84px)/.95 var(--display);
  letter-spacing: .005em;
  color: var(--bone);
  min-height: .9em;
  text-shadow: 0 0 30px color-mix(in srgb, var(--blood) 40%, transparent);
}
.bio {
  margin: .7em 0 0;
  font-size: clamp(12px, 1.7vh, 13px);
  color: var(--concrete);
  min-height: 1.7em;
}

.links {
  list-style: none;
  margin: clamp(10px, 3.5vh, 36px) 0 0;
  padding: 0;
  display: grid;
  /* A left-aligned list, centred as a block, so the icons form one column.
     Each row is as wide as its word, not its (longer) hover label. */
  justify-items: start;
  text-align: left;
  width: max-content;
  max-width: 100%;
  gap: 0;
}
.links .roll { width: var(--w, auto); }
.links a, .links button {
  display: inline-block;
  font-size: clamp(13px, 2vh, 16px);
  font-variation-settings: 'wdth' 100;
  line-height: 1.6;
  color: var(--bone);
  text-decoration: none;
  padding: 0 10px;
  position: relative;
  white-space: pre;
  /* Rising in from below the line, one after another (--d set per link). */
  translate: 0 110%;
  opacity: 0;
  transition: color .3s, text-shadow .3s, translate 1s var(--expo) var(--d, 0ms), opacity .7s var(--expo) var(--d, 0ms);
}
.links.is-in a, .links.is-in button { translate: 0 0; opacity: 1; }
.links li { overflow: hidden; padding: 0 14px; margin: 0 -14px; }
.links.is-done li { overflow: visible; }

/* The word and its handle share one cell; hover rolls one out and the other in. */
/* Clipped above and below only, for the roll; a longer label can run past sideways. */
.roll { display: inline-grid; clip-path: inset(0 -100vw); vertical-align: top; padding: .25em .5em; margin: -.25em -.5em; }
.roll > span { grid-area: 1 / 1; justify-self: start; white-space: nowrap; transition: translate .6s var(--expo), opacity .6s var(--expo); }
.roll .b { translate: 0 100%; opacity: 0; }
.links :hover > .roll .a, .links :focus-visible > .roll .a, .links .is-alt > .roll .a { translate: 0 -100%; opacity: 0; }
.links :hover > .roll .b, .links :focus-visible > .roll .b, .links .is-alt > .roll .b { translate: 0 0; opacity: 1; }
.links a::after, .links button::after {
  content: '';
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: .28em;
  height: 1px;
  background: var(--blood);
  transform: scaleX(0);
  transition: transform .45s var(--ease);
}
.links a:hover, .links button:hover, .links a:focus-visible, .links button:focus-visible {
  color: #fff;
  text-shadow: 0 0 2px var(--blood), 0 0 9px var(--blood);
}
.links a:hover::after, .links button:hover::after,
.links a:focus-visible::after, .links button:focus-visible::after { transform: scaleX(1); }
.links :focus-visible { outline: none; }

/* Every few seconds the name tears: two copies, sliced and shoved sideways. */
.name { position: relative; }
.name.is-glitch::before, .name.is-glitch::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  background: var(--void);
  pointer-events: none;
}
.name.is-glitch::before { clip-path: inset(18% 0 52% 0); color: var(--blood); animation: tear-a .2s steps(2) both; }
.name.is-glitch::after { clip-path: inset(58% 0 12% 0); color: var(--bone); animation: tear-b .2s steps(2) both; }

/* The liquid: drawn over the page, never in the way of a click. */
.blood {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
}

/* Clicking a link: the words fall away fast while the camera flies into
   the figure (drawn in field.js). */
.corner, .blood { transition: opacity .22s ease-out, filter .22s ease-out; }
.is-diving .corner, .is-diving .blood { opacity: 0; filter: blur(6px); }
.is-diving ~ .cur-dot, .is-diving ~ .cur-ring { opacity: 0; }
}

/* ---------- Corner ---------- */

.corner {
  position: fixed;
  z-index: 3;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 16px var(--gutter) max(16px, env(safe-area-inset-bottom));
  font-size: 11px;
  color: var(--ash);
  pointer-events: none;
  animation: fade-in 1.4s both;
}
.corner > * { pointer-events: auto; }
.corner p { margin: 0; }
.state::before { content: ' // '; }
.who b { font-weight: 400; color: var(--concrete); }
.ctl { display: flex; align-items: center; gap: 16px; }
.ctl button { color: var(--ash); transition: color .25s; }
.ctl button:hover, .ctl button[aria-pressed='true'] { color: var(--bone); }
.meter { display: flex; align-items: flex-end; gap: 2px; height: 10px; }
.meter span {
  width: 2px;
  height: 100%;
  background: var(--blood);
  transform-origin: bottom;
  transform: scaleY(.1);
}
.ecg { width: 96px; height: 20px; display: block; }

/* ---------- Hidden console: appears when you start typing ---------- */

.console {
  position: fixed;
  z-index: 4;
  left: 50%;
  bottom: clamp(48px, 8vh, 80px);
  width: min(560px, calc(100% - 2 * var(--gutter)));
  translate: -50% 12px;
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s, translate .5s var(--ease);
  text-align: left;
}
/* The page steps back while the console is open. */
.main { transition: opacity .5s, filter .5s; }
body.is-typing .main { opacity: .14; filter: blur(2px); }
.console.is-open { opacity: 1; translate: -50% 0; pointer-events: auto; }
.out { color: var(--concrete); white-space: pre-wrap; word-break: break-word; font-size: 12px; }
.out p { margin: 0; transition: opacity .8s; }
.out p.is-old { opacity: 0; }
.out .echo { color: var(--ash); }
.out .em { color: var(--bone); }
.prompt {
  display: flex;
  align-items: baseline;
  gap: 1ch;
  padding: 10px 0 0;
  border-top: 1px solid var(--ash);
  margin-top: 8px;
}
.ps1 { color: var(--ash); white-space: nowrap; font-size: 12px; }
.ps1::after { content: ' $'; }
.prompt input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 12px;
  color: var(--bone);
  background: none;
  border: 0;
  padding: 0;
  caret-color: var(--blood);
  outline: none;
}

.closed {
  position: fixed;
  inset: 0;
  z-index: 4;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 18px;
  color: var(--ash);
  font-size: 12px;
  animation: fade-in 1.2s .5s both;
}
.closed p { margin: 0; }

@media (max-width: 640px) {
  .spacer { margin-top: 2vh; }
  .meter { display: none; }
}

/* The dashboard's picture or video: hung from the top edge of the screen,
   edge to edge, melting into the black at its bottom and sides. The box only
   gets the space the text leaves over (basis 0) and the media is laid over
   it, so its own size can never stretch the page; main.js sizes it to fit. */
.spacer.has-media {
  flex: 1 1 0;
  min-height: 0;
  max-height: none;
  width: calc(100% + 2 * var(--gutter));
  margin: 0 calc(-1 * var(--gutter));
  position: relative;
  overflow: hidden;
}
.spacer.has-media > .side-media {
  position: absolute;
  top: 0;
  left: 50%;
  translate: -50% 0;
  display: block;
  object-fit: cover;
  -webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent 100%), linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(to bottom, #000 45%, transparent 100%), linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
  mask-composite: intersect;
  opacity: 0;
  filter: blur(10px);
  transition: opacity 1.6s var(--expo), filter 1.6s var(--expo);
}
.spacer.has-media.is-in > .side-media { opacity: 1; filter: blur(0); }
/* Pulled down from above as it appears. */
.spacer.has-media > .side-media { transform: translateY(-6%); transition: opacity 1.6s var(--expo), filter 1.6s var(--expo), transform 2s var(--expo); }
.spacer.has-media.is-in > .side-media { transform: none; }
/* An MP4 can't be transparent, so a video on a black background would show as a
   black block over the dust at the top, where the mask above keeps it solid.
   Screen drops the black. It can only blend with what's behind .main if .main
   isn't its own stacking context, hence the z-index. */
.spacer.has-media > video.side-media { mix-blend-mode: screen; }
.main:has(> .spacer.has-media > video.side-media) { z-index: auto; }

/* ---------- Around everything ---------- */

.topbar, .rail, .ghost { position: fixed; pointer-events: none; opacity: 0; transition: opacity 2s var(--expo); }

/* A thin status bar along the top, over the picture. */
.topbar {
  z-index: 3;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-between;
  padding: 14px var(--gutter);
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--concrete);
  text-shadow: 0 0 8px #000, 0 0 2px #000;
  font-variant-numeric: tabular-nums;
}
.topbar.is-in { opacity: 1; }
#tb-who::before {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 8px;
  border-radius: 50%;
  background: var(--blood);
  box-shadow: 0 0 8px var(--blood);
  vertical-align: 1px;
  animation: pulse-dot 2.4s ease-in-out infinite;
}
@keyframes pulse-dot { 50% { opacity: .25; } }

/* Faint text creeping up the left edge and down the right. */
.rail {
  z-index: 1;
  top: 0;
  bottom: 0;
  width: 1.4em;
  overflow: hidden;
  font-size: 10px;
  letter-spacing: .32em;
  color: var(--ash);
  writing-mode: vertical-rl;
  white-space: nowrap;
  -webkit-mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent);
  mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent);
}
.rail.is-in { opacity: .75; }
.rail-l { left: calc(var(--gutter) * .45); }
.rail-r { right: calc(var(--gutter) * .45); }
.rail span { display: inline-block; animation: rail-up 140s linear infinite; }
.rail-r span { animation-name: rail-down; }
@keyframes rail-up { from { translate: 0 0; } to { translate: 0 -50%; } }
@keyframes rail-down { from { translate: 0 -50%; } to { translate: 0 0; } }

/* The name again, enormous and barely there, behind everything. */
.ghost {
  z-index: 0;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  font: 600 min(34vw, 46vh) / 1 var(--display);
  white-space: nowrap;
  color: var(--blood);
  -webkit-mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
  mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
  filter: blur(1.5px);
}
.ghost.is-in { opacity: .09; animation: ghost-drift 38s ease-in-out infinite alternate; }
@keyframes ghost-drift { from { translate: -4% 2%; } to { translate: 4% -2%; } }

.is-diving .topbar, .is-diving .rail, .is-diving .ghost { opacity: 0; transition: opacity .25s; }

@media (max-width: 640px) {
  .topbar { font-size: 10px; padding: 10px 14px; }
  .rail { font-size: 8px; }
  .rail-l { left: 3px; }
  .rail-r { right: 3px; }
  .ghost { font-size: 48vw; }
}

/* ---------- Link icons ---------- */

.ico {
  width: .95em;
  height: .95em;
  margin-right: .6em;
  vertical-align: -.13em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .5;
  transition: opacity .3s, color .3s, translate .45s var(--expo);
}
.links :hover > .ico, .links :focus-visible > .ico { opacity: 1; color: var(--blood); translate: -2px 0; }

/* Clicking a link: the picture rushes towards you and the words fall away. */
.is-diving .main > :not(.spacer) { opacity: 0; filter: blur(6px); transition: opacity .22s ease-out, filter .22s ease-out; }
.is-diving .main { opacity: 1; filter: none; }
/* It may grow past its box instead of being clipped; specific enough to beat
   the rules that hold it in place while it's showing. */
.screen.is-diving .spacer.has-media { overflow: visible; }  /* no z-index: a layer of its own would bring back a video's black */
.screen.is-diving .spacer.has-media.is-in > .side-media {
  scale: 9;
  filter: blur(2px);
  transition: scale .65s cubic-bezier(.55, 0, .85, .4), filter .65s ease-in;
}
/* The dive ends on solid black, which stays until the next page arrives. */
.blackout {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s ease-out;
}
.blackout.is-on { opacity: 1; transition: opacity .3s .32s ease-in; }

/* Very short screens (a phone on its side): tighter list, room for the corner. */
@media (max-height: 480px) {
  .main { padding-bottom: 50px; }
  .semi { display: none; }
  .links a, .links button { line-height: 1.3; }
}
@media (max-height: 380px) {
  .spacer { min-height: 56px; }
  .name { font-size: 30px; }
  .links { margin-top: 6px; }
  .links a, .links button { font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-delay: 0s !important; }
}
