/* contracthero.dev — a CRT on a desk. One stylesheet, no build step. */

:root {
  --phosphor: #9dffb0;
  --phosphor-hi: #e9ffe9;
  --phosphor-dim: #4f9a63;
  --glow: rgba(90, 255, 140, .55);
  --mono: "VT323", "Courier New", ui-monospace, monospace;
  --vh: 100vh;                  /* the viewport height the scene and the stage agree on */
}
@supports (height: 1dvh) { :root { --vh: 100dvh; } }

*, *::before, *::after { box-sizing: border-box; }
html { background: #000; color: var(--phosphor); }
body { margin: 0; font-family: var(--mono); -webkit-font-smoothing: antialiased; }
a { color: inherit; }

/* ---------- scroll stage ---------- */
.story { position: relative; }
.scene {
  position: sticky; top: 0;
  height: var(--vh);
  overflow: hidden;
}

/* One square photo for every viewport. The stage is the photo's own box; everything on the screen is
   positioned in the photo's coordinates, so the mapping survives any crop.

   Size: cover the viewport (--cover), but never let the screen grow past 90% of the viewport width or
   72% of its height (--S). In portrait the photo zooms in past cover, up to 1.4x, until the screen
   reaches 90% of the width: a phone shows the monitor and the keyboard, not the whole room. In
   landscape at least 500px tall the whole computer must stay in view: the band from the wall above the
   bezel (22% of the photo height) to the desk edge (80%) always fits the viewport height, so on wide
   displays the photo is narrower than the viewport and its sides fade to black (--fade) instead of
   cropping the keyboard. Where the photo is shorter than the viewport instead (aspect narrower than
   1:2.27) the top and bottom fade the same way (--fade-y).
   Position: the crop is anchored on the centre of the screen (--cx/--cy), which sits at 50% of the
   width and 47% of the height of the viewport, then clamped so the photo edge never enters the viewport
   and, in landscape, so that band stays visible.

   --ar is the photo's pixel aspect ratio. --sx/--sy/--sw/--sh is the black screen rectangle inside
   assets/room.jpg as fractions of the photo: the INSET line printed by tools/measure-screen.py. */
.stage {
  position: absolute;
  --ar: 1;
  --sx: .3165; --sy: .3339; --sw: .3660; --sh: .2633;   /* INSET line for assets/room.jpg, 2048x2048 */
  --cx: calc(var(--sx) + var(--sw) / 2);
  --cy: calc(var(--sy) + var(--sh) / 2);
  --cols: 46; --rows: 12; --adv: .4;                 /* the terminal grid; VT323 advances .4em per character */
  --cover: max(100vw, calc(var(--vh) * var(--ar)));
  --fit-w: calc(90vw / var(--sw));
  --fit-h: calc(.72 * var(--vh) / var(--sh));
  --band-top: .22; --band-bottom: .80;               /* the computer: wall above the bezel to the desk edge */
  --keep-computer: 0;                                 /* 1 while the landscape rule below is in force; tools/check-viewports.mjs reads it */
  --safe-h: var(--fit-h);                             /* landscape below tightens this to the band */
  --want: var(--cover);
  --S: min(var(--want), var(--fit-w), var(--safe-h));
  --H: calc(var(--S) / var(--ar));
  --fade: clamp(0px, calc((100vw - var(--S)) * .6), calc(var(--S) * .14));
  --fade-y: clamp(0px, calc((var(--vh) - var(--H)) * .6), calc(var(--H) * .14));
  --anchor-y: .47;
  --top-want: calc(var(--anchor-y) * var(--vh) - var(--H) * var(--cy));
  --top-min: min(0px, calc(var(--vh) - var(--H)));
  --top-max: max(0px, calc(var(--vh) - var(--H)));
  width: var(--S);
  height: var(--H);
  left: clamp(min(0px, calc(100vw - var(--S))), calc(50vw - var(--S) * var(--cx)), max(0px, calc(100vw - var(--S))));
  top: clamp(var(--top-min), var(--top-want), var(--top-max));
}
@media (orientation: portrait) { .stage { --want: max(var(--cover), min(var(--fit-w), calc(1.4 * var(--cover)))); } }
@media (orientation: landscape) and (min-height: 500px) {
  .stage {
    --keep-computer: 1;
    --safe-h: calc(var(--vh) / (var(--band-bottom) - var(--band-top)));
    --top-min: max(calc(var(--vh) - var(--H)), calc(-1 * var(--band-top) * var(--H)));
    --top-max: min(0px, calc(var(--vh) - var(--band-bottom) * var(--H)));
  }
}
.stage img {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent),
                      linear-gradient(to bottom, transparent, #000 var(--fade-y), #000 calc(100% - var(--fade-y)), transparent);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent),
              linear-gradient(to bottom, transparent, #000 var(--fade-y), #000 calc(100% - var(--fade-y)), transparent);
  mask-composite: intersect;
}

/* ---------- the screen ---------- */
/* The screen and the glass over it share the photo's screen rectangle. The glass is a sibling, not a
   child, so the barrel filter on the screen warps the raster and not the scanlines. */
.screen, .glass {
  position: absolute;
  left: calc(var(--sx) * 100%); top: calc(var(--sy) * 100%);
  width: calc(var(--sw) * 100%); height: calc(var(--sh) * 100%);
  border-radius: 1.4% / 1.9%;
}
.screen {
  overflow: hidden;
  filter: url(#barrel);   /* the tube face is curved. The SVG ships scale="0", so until crt.js fills the map the screen renders flat */
  /* The tube face: a faint phosphor tint in the middle that is black by the bezel, as on a real terminal.
     A tint that reaches the edge shows as a green wash in the empty margin under the last row. */
  background:
    radial-gradient(ellipse at 50% 45%, rgba(20, 60, 30, .25), rgba(2, 6, 3, .1) 50%, rgba(0, 0, 0, .7) 95%);
  box-shadow: inset 0 0 calc(var(--S) * .02) rgba(0, 0, 0, .75);
}
.spill {
  position: absolute; pointer-events: none;
  left: calc((var(--sx) - .06) * 100%); width: calc((var(--sw) + .12) * 100%);
  top: calc(var(--band-bottom) * 100% - 1%); height: 12%;   /* the glow falls from the foot of the case onto the desk; at --sy + --sh it sat on the bezel lip */
  background: radial-gradient(ellipse at 50% 0%, rgba(90,255,140,.07), rgba(0,0,0,0) 70%);
}
/* The screen is a fixed terminal grid, --cols by --rows (46 by 12), on any device: the type is sized so
   --cols characters of --adv em each use 90% of the screen width. 12 rows at line-height 1.15 are then
   24.7% of the screen width, which is 94% of this 1.39:1 screen's height (--sw/--sh). Percentage padding is
   relative to the width on every side, so 1.5% 4% adds 4.2% of the height and the rows clear the box by 2%;
   3% does not fit. tools/check-viewports.mjs reads --cols and --rows and
   fails a chapter that does not fit. */
.tube {
  position: absolute; inset: 0;
  padding: 1.5% 4%;
  overflow: hidden;
  font-size: calc(var(--S) * var(--sw) * .9 / (var(--cols) * var(--adv)));
  line-height: 1.15;
  animation: flicker 6s infinite steps(1);
  --beam: 0 0 calc(var(--S) * .012) var(--glow);   /* the glow of the collapsed raster in the power keyframes */
}
.text {
  will-change: transform;
  text-shadow: 0 0 5px var(--glow), 0 0 12px rgba(90,255,140,.18);
}
.text a { text-decoration: underline; text-decoration-color: var(--phosphor-dim); text-underline-offset: .15em; }
.text a:hover, .text a:focus-visible { background: var(--phosphor); color: #030503; text-shadow: none; outline: none; }

/* the story's own classes, shared by the screen and the no-JS page */
.cmd, .dim, .k { color: var(--phosphor-dim); }
.chapter-heading { display: flex; justify-content: space-between; gap: 1ch; white-space: nowrap; }
.chapter-count { flex-shrink: 0; }
.hi { color: var(--phosphor-hi); }
.kv { display: grid; grid-template-columns: 7ch 1fr; gap: 0 1ch; }
:where(.text, html.no-js .chapter) :where(p, pre) { margin: 0; }   /* zero specificity, so .sp wins in both render modes */
.sp { margin-top: .6em; }                      /* half a blank row */
pre { font: inherit; white-space: pre; }

.cursor {
  display: inline-block; width: .6em; height: 1em; vertical-align: -.12em; margin-left: 1px;
  background: var(--phosphor); box-shadow: 0 0 6px var(--glow);
  animation: blink 1.05s steps(1) infinite;
}
.screen.typing .cursor { animation: none; }

.barrel { position: absolute; width: 0; height: 0; overflow: hidden; }
.glass {
  pointer-events: none;
  background:
    repeating-linear-gradient(0deg, rgba(0,0,0,var(--scanline-alpha, .26)) 0 1px, rgba(0,0,0,0) 1px 3px),
    radial-gradient(ellipse at 24% 10%, rgba(255,255,255,.05), rgba(255,255,255,0) 45%),
    radial-gradient(ellipse at 50% 50%, rgba(0,0,0,0) 58%, rgba(0,0,0,.42) 100%);
}

/* At phone-size type, broad glow and dark scanlines obscure the letter shapes. Keep the CRT colour
   and glass, with a tighter glow for portrait phones and short landscape windows. */
@media (max-width: 600px), (max-height: 500px) {
  .text { text-shadow: 0 0 2px rgba(90,255,140,.32), 0 0 5px rgba(90,255,140,.12); }
  .glass { --scanline-alpha: .14; }
}

/* ---------- power button and LED (easter egg) ----------
   Both sit over the ones painted in assets/room.jpg, in photo fractions like the screen: LED centre
   (1361,1309) 9px wide, button box (1379,1288)-(1434,1326) of 2048. The LED overlay is a little wider
   than the painted dot so it covers it fully. */
.led {
  position: absolute; pointer-events: none;
  --d: .0068;
  /* one hue per state; the lens is built from it: a saturated dome, a lit core, a shaded underside and a specular hit */
  --led: #7ee23a; --led-core: #e6ffc4; --led-deep: #2f7a12; --led-glow: rgba(140, 255, 60, .6);
  left: calc((.6646 - var(--d) / 2) * 100%); top: calc((.6392 - var(--d) / 2) * 100%);
  width: calc(var(--d) * 100%); height: calc(var(--d) * 100%);
  border-radius: 50%;
  background:
    radial-gradient(circle at 34% 30%, rgba(255, 255, 255, .95) 0 14%, rgba(255, 255, 255, 0) 32%),   /* specular hit */
    radial-gradient(circle at 50% 55%, var(--led-core) 0 18%, var(--led) 46%, var(--led-deep) 100%);      /* lit dome */
  box-shadow:
    inset 0 calc(var(--S) * -.001) calc(var(--S) * .0015) rgba(0, 0, 0, .35),                             /* shade under the dome */
    0 0 calc(var(--S) * .003) var(--led),
    0 0 calc(var(--S) * .012) var(--led-glow);
  transition: box-shadow .25s;   /* gradients do not transition; the glow does */
}
.stage.off .led { --led: #ff3b2f; --led-core: #ffd6cf; --led-deep: #8a0f08; --led-glow: rgba(255, 60, 40, .6); }
/* A latching switch. The button is the tap area: the painted cap grown 30% to the left and 40% up and
   down (not to the right: on a portrait phone the cap sits at the viewport edge) so a phone can hit it;
   ::before is the cap itself. While the screen is on the cap sits pushed into the case, so the photo's
   raised cap gets a darker face and shadows: top edge, left edge, lit bottom lip. Off leaves the photo
   alone: the cap is out. --press scales the shadows with --S. */
.power {
  position: absolute;
  --px: .6733; --py: .6289; --pw: .0269; --ph: .0186;   /* the painted cap, in photo fractions */
  left: calc((var(--px) - var(--pw) * .3) * 100%); top: calc((var(--py) - var(--ph) * .4) * 100%);
  width: calc(var(--pw) * 1.3 * 100%); height: calc(var(--ph) * 1.8 * 100%);
  margin: 0; padding: 0; border: 0; background: transparent; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  --press: calc(var(--S) * .004);
}
.power::before {
  content: ''; position: absolute; inset: 22.2% 0 22.2% 23.1%;   /* .4ph / 1.8ph and .3pw / 1.3pw: back to the cap */
  border-radius: 12% / 20%;
  background: rgba(20, 10, 4, .22);
  box-shadow: inset 0 var(--press) calc(var(--press) * 2) rgba(0, 0, 0, .7),
              inset calc(var(--press) * .5) 0 var(--press) rgba(0, 0, 0, .35),
              inset 0 calc(var(--press) * -.5) 0 rgba(255, 220, 180, .18);
  transition: background .12s, box-shadow .12s;
}
.stage.off .power::before { background: transparent; box-shadow: none; }
.power:active::before { background: rgba(20, 10, 4, .32); box-shadow: inset 0 calc(var(--press) * 1.5) calc(var(--press) * 3) rgba(0, 0, 0, .8); }
.power:focus-visible { outline: 2px solid var(--phosphor); outline-offset: 0; border-radius: 12% / 20%; }

/* Screen off: the tube is black and the spill is gone. With motion allowed, the tube collapses like a
   CRT losing its beam and holds the last frame; waking runs the warm-up once alongside the idle flicker,
   and the base flicker restarts when crt.js drops .waking on animationend. */
.stage.off .tube { opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .stage.off .tube { animation: crt-off .7s ease-in forwards; }
  .stage.waking .tube { animation: crt-on .9s ease-out, flicker 6s infinite steps(1); }
}
.stage.off .screen { background: radial-gradient(ellipse at 50% 42%, rgba(10, 14, 11, .6), rgba(2, 3, 2, .3) 72%); }
.spill { transition: opacity .5s; }
.stage.off .spill { opacity: 0; }

/* ---------- skip link for keyboard users (visible only on focus) ---------- */
.skip { position: fixed; left: -999px; top: 8px; z-index: 10; padding: 6px 10px; background: var(--phosphor); color: #000; font-size: 18px; text-decoration: none; }
.skip:focus { left: 8px; }

/* ---------- the story text (source of truth for the screen) ---------- */
html.js .chapters {
  position: absolute; top: 0; left: 0; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
html.no-js .scene { display: none; }
html.no-js .chapters { max-width: 60ch; margin: 0 auto; padding: 8vh 6vw 14vh; font-size: 24px; line-height: 1.25; }
html.no-js .chapter { margin-bottom: 3em; }

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

/* Power on: the beam warms up. A brief overbright bloom, slightly stretched, that settles into place. */
@keyframes crt-on {
  0%   { transform: scale(1, .006); opacity: 0; filter: brightness(4); background: var(--phosphor-hi); box-shadow: var(--beam); }
  20%  { transform: scale(1, .006); opacity: 1; filter: brightness(6); background: var(--phosphor-hi); box-shadow: var(--beam); }
  45%  { transform: scale(1.02, 1.02); opacity: 1; filter: brightness(2.2); background: rgba(157, 255, 176, .1); box-shadow: none; }
  100% { transform: none; opacity: 1; filter: none; background: transparent; box-shadow: none; }
}
/* Power off: the beam loses vertical deflection first, so the raster squeezes into a bright line; then
   horizontal, so the line shrinks to a dot; then the dot decays. The brightness rises as the same
   energy lands on less phosphor. */
@keyframes crt-off {
  0%   { transform: none; opacity: 1; filter: none; background: transparent; box-shadow: none; }
  35%  { transform: scale(1, .4); background: rgba(157, 255, 176, .12); }
  55%  { transform: scale(1, .006); opacity: 1; filter: brightness(1.6); background: var(--phosphor-hi); box-shadow: var(--beam); }
  80%  { transform: scale(.03, .006); opacity: 1; filter: brightness(2); background: var(--phosphor-hi); box-shadow: var(--beam); }
  100% { transform: scale(.01, .006); opacity: 0; filter: none; background: var(--phosphor-hi); box-shadow: var(--beam); }
}
@keyframes flicker {
  0%, 100% { opacity: 1; }
  7%  { opacity: .985; }
  8%  { opacity: 1; }
  41% { opacity: .99; }
  42% { opacity: 1; }
  63% { opacity: .975; }
  64% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .tube, .cursor { animation: none; }
  .spill { transition: none; }
}
