/* ============================================================
   Benjamin Kheong — 8-bit personal site
   Dark only. No external requests. Everything is self-hosted.
   ============================================================ */

@font-face {
  font-family: "Press Start 2P";
  src: url("/assets/fonts/press-start-2p-latin.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* NES-leaning palette, 8 hues, all checked for contrast on --bg */
  --bg:        #14121f;
  --bg-2:      #1d1a2e;
  --panel:     #221f36;
  --ink:       #e8e6f2;
  --dim:       #8e88ad;
  --green:     #6ee39a;
  --blue:      #6fa8ff;
  --yellow:    #ffcf5c;
  --red:       #ff6b5e;
  --magenta:   #d97bd9;
  --edge-lit:  #4a4470;
  --edge-dark: #0b0a12;

  /* The page is the same room the scene is in, lit by the same hour.
     js/scene.js overwrites all of these from its TIMES table every time
     the hour changes; the values here are the night room, so the page is
     still a room with JS off.

     Each one is the scene's own colour pulled most of the way to
     #0b0a12, because the room's real wall is far too light to put 8px
     text on. The factors are picked against measured contrast: the wall
     keeps --dim at 5.0:1 (AA), the upper wall keeps the green subtitle
     at 7.0:1, and no text sits on the floor at all. */
  --hour-edge: #4a4470;      /* panel + stage edge: the wall trim, raw */
  --hour-wall: #121020;      /* wall, where the footer and panels sit   */
  --hour-wall-top: #1b1730;  /* upper wall, behind the header           */
  --hour-line: #3a3267;      /* the rail, and the skirting highlight    */
  --hour-floor: #38251b;     /* floorboards along the bottom            */
  --hour-seam: #301f16;      /* the gaps between the boards             */

  /* where the upper wall ends and the rail runs: it has to land in the
     gap between the header and the stage, which is shorter on mobile.
     Too close to the stage and the upper wall stops reading as a lit
     wall and starts reading as a header bar butted against the frame. */
  --wall-top-h: 114px;

  --pixel: "Press Start 2P", ui-monospace, "Cascadia Mono", Consolas, monospace;
  --mono:  ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas,
           "Liberation Mono", monospace;

  --u: 4px; /* one "pixel" unit — all spacing is a multiple of this */
}

* { box-sizing: border-box; }

/* Registering the room's colours as real colour properties is what lets
   them transition - a var() inside a gradient cannot animate otherwise,
   and the hour would land as a hard cut across the whole page. Browsers
   without @property just get that cut. */
@property --hour-wall     { syntax: "<color>"; inherits: true; initial-value: #121020; }
@property --hour-wall-top { syntax: "<color>"; inherits: true; initial-value: #1b1730; }
@property --hour-line     { syntax: "<color>"; inherits: true; initial-value: #3a3267; }
@property --hour-floor    { syntax: "<color>"; inherits: true; initial-value: #38251b; }
@property --hour-seam     { syntax: "<color>"; inherits: true; initial-value: #301f16; }

html {
  color-scheme: dark;
  scroll-behavior: smooth;
  /* The room lives on body, and a propagated body background is
     positioned against the ROOT box - not the viewport. With 1018px of
     content in a 1300px window that left the floorboards stranded 282px
     up the page with dead colour below them. */
  min-height: 100vh;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--ink);
  font: 400 15px/1.7 var(--mono);
  -webkit-font-smoothing: antialiased;

  /* The room, built out of flat bands - no smooth gradient anywhere,
     because 8-bit hardware could not blend. Top to bottom it is: upper
     wall behind the header, a rail, wallpaper, then a skirting line and
     floorboards along the very bottom of the document. Same structure as
     the wall in the scene, same colours, same hour.

     The first attempt at this was three near-identical dark bands, 3 to
     4 values apart per channel. Invisible. The visible part here is the
     warm floor against the cool wall - 38 to 57 values apart in red. */
  background-image:
    /* floorboard seams */
    repeating-linear-gradient(90deg,
                              var(--hour-seam) 0 1px,
                              transparent      1px 18px),
    /* skirting highlight, sitting on top of the boards */
    linear-gradient(var(--hour-line), var(--hour-line)),
    /* the floor itself */
    linear-gradient(var(--hour-floor), var(--hour-floor)),
    /* 2px dither: two colours mixed the only way a small palette can */
    repeating-conic-gradient(rgba(255,255,255,.022) 0% 25%,
                             transparent           0% 50%),
    /* wallpaper, every 8px, the same rhythm the scene stripes the room */
    repeating-linear-gradient(90deg,
                              rgba(255,255,255,.030) 0 1px,
                              transparent            1px 8px),
    /* upper wall, rail, wall */
    linear-gradient(to bottom,
                    var(--hour-wall-top) 0,
                    var(--hour-wall-top) var(--wall-top-h),
                    var(--hour-line)     var(--wall-top-h),
                    var(--hour-line)     calc(var(--wall-top-h) + 2px),
                    var(--hour-wall)     calc(var(--wall-top-h) + 2px));
  background-size:
    100% 36px,          /* seams    */
    100% 2px,           /* skirting */
    100% 36px,          /* floor    */
    4px 4px,            /* dither   */
    8px calc(100% - 38px),
    100% 100%;
  background-position:
    left bottom, left bottom 36px, left bottom,
    left top, left top, left top;
  background-repeat:
    no-repeat, no-repeat, no-repeat, repeat, repeat-x, no-repeat;
  transition:
    --hour-wall 900ms linear, --hour-wall-top 900ms linear,
    --hour-line 900ms linear, --hour-floor 900ms linear,
    --hour-seam 900ms linear;
}

.wrap {
  max-width: 74ch;
  margin: 0 auto;
  padding: calc(var(--u) * 8) calc(var(--u) * 5) calc(var(--u) * 14);
}

/* ---------- chunky 8-bit panel: stepped corners, no border-radius ---------- */

.panel {
  position: relative;
  /* flat, and a shade above the darkest band so the stack reads as
     panels ON a wall rather than holes IN one */
  background: var(--panel);
  border: var(--u) solid var(--edge-dark);
  box-shadow:
    0 0 0 var(--u) var(--hour-edge),
    0 0 0 calc(var(--u) * 2) var(--edge-dark);
  padding: calc(var(--u) * 5);
  margin: calc(var(--u) * 10) 0;
  /* slow enough to read as the light changing, not as a flash */
  transition: box-shadow 900ms linear;
}

.panel > h2 {
  position: absolute;
  top: calc(var(--u) * -3.5);
  left: calc(var(--u) * 3);
  margin: 0;
  padding: 0 var(--u);
  background: var(--panel);
  font: 400 11px/1 var(--pixel);
  letter-spacing: .04em;
  color: var(--yellow);
  text-transform: uppercase;
}

/* ---------- header ---------- */

header { text-align: center; }

h1 {
  font: 400 clamp(17px, 5.2vw, 30px)/1.35 var(--pixel);
  margin: 0 0 calc(var(--u) * 3);
  color: var(--ink);
  letter-spacing: .02em;
  /* hard 8-bit drop shadow — offset, not blurred */
  text-shadow:
     var(--u) var(--u) 0 var(--edge-dark),
     var(--u) 0      0 var(--edge-dark),
     0       var(--u) 0 var(--edge-dark);
}

.sub {
  font: 400 9px/1.9 var(--pixel);
  color: var(--green);
  letter-spacing: .08em;
  text-transform: uppercase;
  margin: 0;
}

.controls {
  display: flex;
  gap: calc(var(--u) * 2);
  flex-wrap: wrap;
}

/* a toggle that is on reads as lit, not as a different label */
button[aria-pressed="true"] {
  color: var(--green);
  box-shadow:
    inset var(--u) var(--u) 0 var(--edge-dark),
    inset calc(var(--u) * -1) calc(var(--u) * -1) 0 var(--edge-lit);
}

.sub .lv { color: var(--yellow); }
.sub .hp { color: var(--red); }

/* ---------- the scene ---------- */

.stage {
  margin: calc(var(--u) * 8) 0 0;
  border: var(--u) solid var(--edge-dark);
  box-shadow:
    0 0 0 var(--u) var(--hour-edge),
    0 0 0 calc(var(--u) * 2) var(--edge-dark);
  transition: box-shadow 900ms linear;
  background: #0d0b15;
  position: relative;
  overflow: hidden;
}

#scene {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 240 / 128;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* ---------- the stage breaks out of the text column ----------
   The canvas is 240x128. Stretched to whatever the 74ch column happened
   to be it ran at 2.51x, so every other source pixel got 3 device pixels
   and its neighbour got 2 - the fat-pixel grid was never square, on the
   one element whose whole point is the grid.

   So from the width where a whole 2x fits, the stage takes an integer
   multiple and steps out of the column: the scene is the thing on this
   page worth looking at big. Below that it still fills the column,
   because 1x would be smaller than today and phones are dpr 2-3, where
   a fractional CSS scale lands on whole device pixels anyway.

   Width includes the border, because box-sizing is border-box globally -
   hence the + var(--u) * 2, without which 960px of box is 952px of
   canvas and the whole point is lost. The HUD takes the same width so
   the caption and the button stay on the stage's edges. */

@media (min-width: 33rem) {
  .stage, .hud {
    --z: 2;
    width: calc(240px * var(--z) + var(--u) * 2);
    margin-left: 50%;
    transform: translateX(-50%);
  }
}
@media (min-width: 48rem) { .stage, .hud { --z: 3; } }
@media (min-width: 64rem) { .stage, .hud { --z: 4; } }
@media (min-width: 80rem) { .stage, .hud { --z: 5; } }

/* CRT scanlines over the canvas only - and over the overlays too, which
   is why this sits a layer above them. */
.stage::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0,0,0,.16) 0 2px,
    transparent 2px 4px
  );
  mix-blend-mode: multiply;
}

/* ---------- overlays on the screen: boot, and paused ---------- */

/* display:grid would beat the hidden attribute, so restate it */
[hidden] { display: none !important; }

.overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: calc(var(--u) * 3);
  padding: calc(var(--u) * 4);
  background: rgba(9, 8, 15, .84);
  text-align: center;
  cursor: pointer;
}

.overlay-title {
  margin: 0;
  font: 400 clamp(10px, 2.4vw, 14px)/1.6 var(--pixel);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink);
}

.overlay-hint {
  margin: 0;
  font: 400 8px/1.8 var(--pixel);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--dim);
}

/* chunky loader: hard edges, no easing, steps not a slide */
.overlay .bar {
  width: min(60%, 240px);
  height: calc(var(--u) * 3);
  padding: 2px;
  background: var(--edge-dark);
  box-shadow: 0 0 0 2px var(--hour-edge);
}

.overlay .bar span {
  display: block;
  width: 0;
  height: 100%;
  background: var(--green);
}

/* ---------- the message box ---------- */

.dialogue {
  position: relative;
  /* just enough for one line plus its padding - the box grows as lines
     are added, so this only sets the floor */
  min-height: calc(var(--u) * 14);
  padding: calc(var(--u) * 4) calc(var(--u) * 8) calc(var(--u) * 4)
           calc(var(--u) * 4);
  background: var(--bg);
  border: 2px solid var(--edge-dark);
  box-shadow: inset 0 0 0 2px var(--hour-edge);
  font: 400 clamp(9px, 2.2vw, 11px)/2.2 var(--pixel);
  color: var(--ink);
}

.dialogue p { margin: 0 0 calc(var(--u) * 2); }
.dialogue p:last-child { margin-bottom: 0; }

/* the caret: a solid block, the way a terminal draws one. It blinks
   only while the box is waiting - during typing it just moves. */
.dialogue .caret {
  display: inline-block;
  width: .55em;
  height: 1em;
  margin-left: 2px;
  vertical-align: -.15em;
  background: var(--green);
}

.dialogue { cursor: pointer; }   /* click fills the message in */

@media (prefers-reduced-motion: reduce) {
  /* nothing types, so there is no caret to show */
  .dialogue .caret { display: none; }
  .dialogue { cursor: default; }
}

.hud {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--u) * 3);
  flex-wrap: wrap;
  margin-top: calc(var(--u) * 3);
  font: 400 9px/1.8 var(--pixel);
  color: var(--dim);
  letter-spacing: .05em;
}

#caption { color: var(--green); text-transform: uppercase; }
#caption::before { content: "\25B8  "; color: var(--yellow); }

/* ---------- 8-bit button ---------- */

button {
  font: 400 9px/1 var(--pixel);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--bg-2);
  border: 0;
  padding: calc(var(--u) * 2.5) calc(var(--u) * 3);
  cursor: pointer;
  box-shadow:
    inset calc(var(--u) * -1) calc(var(--u) * -1) 0 var(--edge-dark),
    inset var(--u) var(--u) 0 var(--edge-lit);
}

button:hover { color: var(--yellow); }

button:active {
  box-shadow:
    inset var(--u) var(--u) 0 var(--edge-dark),
    inset calc(var(--u) * -1) calc(var(--u) * -1) 0 var(--edge-lit);
}

button:focus-visible {
  outline: var(--u) solid var(--green);
  outline-offset: var(--u);
}

/* ---------- body copy ---------- */

.panel p { margin: 0 0 calc(var(--u) * 4); }
.panel p:last-child { margin-bottom: 0; }

/* the whole bio, such as it is */
.creed {
  font: 400 clamp(10px, 2.6vw, 13px)/2 var(--pixel);
  letter-spacing: .06em;
  color: var(--ink);
  margin: 0;
}

/* ---------- link menu ---------- */

.menu { list-style: none; margin: 0; padding: 0; }

.menu li + li { margin-top: calc(var(--u) * 2); }

.menu a {
  display: flex;
  gap: calc(var(--u) * 3);
  align-items: baseline;
  flex-wrap: wrap;
  font: 400 10px/1.6 var(--pixel);
  letter-spacing: .03em;
  color: var(--blue);
  text-decoration: none;
  padding: calc(var(--u) * 2);
  border: var(--u) solid transparent;
}

.menu a .tag {
  color: var(--dim);
  min-width: 11ch;
}

/* the menu cursor: always in the layout so nothing shifts, only visible
   on the selected row */
.menu a::before {
  content: "\25BA";
  color: transparent;
}

.menu a:hover,
.menu a:focus-visible {
  color: var(--yellow);
  background: var(--bg-2);
  border-color: var(--hour-edge);
  outline: none;
}

.menu a:hover::before,
.menu a:focus-visible::before { color: var(--yellow); }

.menu a[aria-disabled="true"] {
  color: var(--dim);
  cursor: not-allowed;
}

/* ---------- footer ---------- */

footer {
  margin-top: calc(var(--u) * 12);
  text-align: center;
  font: 400 8px/2 var(--pixel);
  color: var(--dim);
  letter-spacing: .06em;
}

footer a { color: var(--dim); }

.blink { animation: blink 1.2s step-end infinite; }

@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .blink { animation: none; }
  /* the hour still sets the room's colours, they just arrive instantly */
  .panel, .stage, body { transition: none; }
}

/* ---------- small screens ---------- */

@media (max-width: 30rem) {
  :root { --u: 3px; }
  body { font-size: 14px; }
  /* the header is shorter here, so the rail comes up to meet it */
  :root { --wall-top-h: 88px; }   /* header ends at 76, stage starts at 100 */
  .wrap { padding-left: calc(var(--u) * 4); padding-right: calc(var(--u) * 4); }
  .menu a .tag { min-width: 0; width: 100%; }
}

/* visually-hidden but readable by screen readers */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
