/* ============================================================
   PEN-DOS — SECRET PS1 SCENE
   ------------------------------------------------------------
   Everything is laid out in a fixed 640x360 "render space".
   The whole stage is then scaled up with transform: scale()
   and image-rendering: pixelated, which is what produces the
   chunky PlayStation-era upscale. Never author anything in
   screen pixels — always in the 640x360 space.
   ============================================================ */

:root{
  --base-w: 640;
  --base-h: 360;
  --letterbox: #000;
}

*{ box-sizing: border-box; }

html, body{
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--letterbox);
  overflow: hidden;
  font-family: "VT323", "Courier New", monospace;
  color: #cfe3ff;
  cursor: default;
}

/* ---------- viewport + stage ---------- */

.viewport{
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--letterbox);
}

.stage{
  position: relative;
  width: calc(var(--base-w) * 1px);
  height: calc(var(--base-h) * 1px);
  transform-origin: center center;
  overflow: hidden;
  background: #05060f;
  /* set by JS: transform: scale(n) */
}

/* ---------- screens smaller than the stage itself ----------
   The stage is always 640x480 in LAYOUT — only a transform scales it, and
   a transform does not change the box the grid lays out. So on a viewport
   narrower than 640 the implicit grid column sizes itself to the stage's
   max-content and computes to 640px, wider than its own container. The
   track then starts hard against the left edge, `place-items: center`
   centres the stage inside that 640px track rather than inside the
   screen, and scaling about the stage's own centre leaves it sitting off
   to one side: measured at 375px wide, the stage landed at 133..508 with
   133px of dead letterbox on the left and 133px cut off the right.

   Pinning the track to the container fixes it. The stage still overflows
   the track, but now equally on both sides, so scaling about its centre
   brings it back to the middle of the screen. fitStage()'s maths was
   never wrong — the scale was correct all along, only the position.

   Rows as well as columns, for a landscape phone shorter than 480.

   Scoped to exactly the sizes where the track cannot fit, so every
   larger viewport keeps the behaviour it already had. Above these widths
   the track stretches to the container anyway and this rule would be a
   no-op — it is a media query for the sake of leaving the desktop
   provably untouched, not because the fix is only valid here. */
@media (max-width: 639px), (max-height: 479px){
  .viewport{
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
  }
}

/* every layer image sits in the same 640x360 box */
.layer{
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--base-w) * 1px);
  height: calc(var(--base-h) * 1px);
  image-rendering: pixelated;
  -ms-interpolation-mode: nearest-neighbor;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;      /* picking is done in JS, not by the DOM */
  will-change: transform;
}

/* the highlight applied to whichever object is under the cursor */
.layer.is-hot{
  filter: brightness(1.35) contrast(1.05);
}

/* PenMart interaction cues are sized very differently. The broad storefront
   should barely lift; the small entrance must be easier to discover. */
.layer.mart-store-hotspot.is-hot{
  filter: brightness(1.055) contrast(1.015);
}

.layer.mart-door-hotspot.is-hot{
  opacity: .16 !important;
  mix-blend-mode: screen;
  filter:
    sepia(.12) saturate(1.3) brightness(.92)
    drop-shadow(0 0 2px rgba(222, 244, 255, .42));
}

/* Interior object masks are invisible white silhouette renders. Revealing a
   small amount on hover produces a readable PS1-style sheen without painting
   a rectangular UI highlight over the scene. */
.layer.mart-interior-mask.is-hot{
  opacity: .085 !important;
  mix-blend-mode: screen;
  filter:
    sepia(.08) saturate(1.2) brightness(.86)
    drop-shadow(0 0 1px rgba(205, 238, 255, .30));
}

.layer.mart-interior-clerk-hotspot.is-hot{
  filter:
    brightness(1.10) contrast(1.025)
    drop-shadow(0 0 1px rgba(188, 222, 255, .26));
}

/* The drive radio's ordinary hotspot artwork is a deliberately invisible
   white render of the real console geometry. Reveal just a trace of that
   silhouette on hover so the control can be found in the dark, without
   turning the whole lower dashboard into a bright rectangular button. */
.layer.radio-hotspot.is-hot{
  opacity: .075 !important;
  mix-blend-mode: screen;
  filter:
    sepia(.65) saturate(3.5) hue-rotate(68deg) brightness(.72)
    drop-shadow(0 0 1px rgba(91, 255, 137, .35));
}

/* ---------- animated sprite layers ----------
   A sprite layer's image is a horizontal strip of N frames.
   The element is still 640x360; we shift the background. */
.layer.sprite{
  background-repeat: no-repeat;
  background-size: auto calc(var(--base-h) * 1px);
  image-rendering: pixelated;
}

/* ---------- scrolling bands ----------
   A seamlessly tiling strip slid sideways by the engine: the parallax
   world outside a moving car. The strip's pixel size is pinned inline
   from the artwork so it tiles at exactly its authored period; only
   the horizontal position is animated. */
.layer.scroll{
  background-repeat: repeat-x;
  image-rendering: pixelated;
  will-change: background-position;
}

/* ---------- plane-mapped bands ----------
   The same tiling strip, but laid onto the world plane it depicts
   instead of pasted flat, so it converges on the scene's real
   vanishing point. The engine writes a matrix3d onto .plane-art.

   The wrapper exists for one reason: mask-image applies in an
   element's own space, BEFORE its transform, so a window mask on the
   transformed element would be dragged onto the plane with the art.
   The mask stays out here, in screen space, and clips the child. */
.layer.plane{
  overflow: hidden;
}

.plane-art{
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  background-repeat: repeat-x;
  image-rendering: pixelated;
  will-change: background-position;
  backface-visibility: hidden;
}

/* ---------- glow layers ----------
   Sized and positioned inline by the engine; these are the light
   halos. .flicker gives a lamp that faint unsteady buzz. */
.layer.glow{
  width: auto;
  height: auto;
  image-rendering: auto;       /* a gradient must NOT be pixelated */
}

.layer.flicker{
  animation-name: flicker;
  animation-duration: 4s;
  animation-iteration-count: infinite;
  animation-timing-function: steps(1, end);
}

@keyframes flicker{
  0%, 62%, 100% { opacity: 1; }
  64%           { opacity: .55; }
  66%           { opacity: 1; }
  73%           { opacity: .72; }
  75%           { opacity: 1; }
}

.layer.pulse{
  animation: pulse 2.6s steps(6, end) infinite;
}

@keyframes pulse{
  0%, 100% { opacity: .82; }
  50%      { opacity: 1; }
}

/* ---------- atmospheric scene motion ---------- */

.layer.city-light{
  opacity: .12;
}

.layer.beacon-a{ animation: city-beacon-steady 3.2s steps(1, end) infinite; }
.layer.beacon-b{ animation: city-beacon 8.9s steps(1, end) -5.4s infinite; }
.layer.beacon-c{ animation: city-beacon 7.6s steps(1, end) -1.3s infinite; }

@keyframes city-beacon-steady{
  0%, 49%  { opacity: .10; }
  50%, 99% { opacity: 1; }
  100%     { opacity: .10; }
}

@keyframes city-beacon{
  0%, 78%, 90%, 100% { opacity: .10; }
  80%                 { opacity: 1; }
  82%                 { opacity: .18; }
  84%                 { opacity: .78; }
  86%                 { opacity: .10; }
}

.layer.window-a{ animation: city-window 13.1s steps(1, end) -3.7s infinite; }
.layer.window-b{ animation: city-window 17.3s steps(1, end) -11.2s infinite; }
.layer.window-c{ animation: city-window 11.9s steps(1, end) -6.8s infinite; }
.layer.window-d{ animation: city-window 19.7s steps(1, end) -14.1s infinite; }

@keyframes city-window{
  0%, 60%, 100% { opacity: .12; }
  62%, 82%      { opacity: .88; }
  84%           { opacity: .32; }
}

/* ---------- PenMart pole-lamp insects ----------
   Composed in the native 640x480 scene space around the near pole fixture. */

.layer.effect.mart-lamp-insects::before,
.layer.effect.mart-lamp-insects::after{
  content: "";
  position: absolute;
  width: 1px;
  height: 1px;
  background: rgba(255,232,165,.82);
  box-shadow:
    7px 3px rgba(255,244,198,.70),
    -5px 7px rgba(255,218,135,.54),
    11px -5px rgba(255,239,183,.48);
  filter: drop-shadow(0 0 1px rgba(255,198,98,.55));
}

.layer.effect.mart-lamp-insects::before{
  left: 489px;
  top: 61px;
  animation: mart-insects-a 6.7s steps(10, end) -2.1s infinite;
}

.layer.effect.mart-lamp-insects::after{
  left: 501px;
  top: 72px;
  opacity: .55;
  transform: scale(.8);
  animation: mart-insects-b 8.3s steps(12, end) -5.4s infinite;
}

@keyframes mart-insects-a{
  0%, 100% { transform: translate(0,0); opacity: .35; }
  14%      { transform: translate(5px,-3px); opacity: .78; }
  29%      { transform: translate(1px,4px); opacity: .52; }
  43%      { transform: translate(-6px,1px); opacity: .82; }
  61%      { transform: translate(-2px,-5px); opacity: .42; }
  77%      { transform: translate(6px,2px); opacity: .70; }
  89%      { transform: translate(2px,6px); opacity: .28; }
}

@keyframes mart-insects-b{
  0%, 100% { transform: translate(0,0) scale(.8); opacity: .22; }
  18%      { transform: translate(-4px,3px) scale(.8); opacity: .58; }
  37%      { transform: translate(3px,6px) scale(.8); opacity: .34; }
  52%      { transform: translate(6px,-2px) scale(.8); opacity: .62; }
  68%      { transform: translate(-1px,-6px) scale(.8); opacity: .30; }
  86%      { transform: translate(-7px,0) scale(.8); opacity: .54; }
}

/* Two fields of tiny horizontal ellipses drift at different rates.
   The lower edge is fitted to the TOP of shallows.png's alpha mask
   (y = 0.069878x + 335.679), not the outer quay line. This matches the
   water hotspot: only textured sea moves, while the pale submerged
   concrete band and foreground pier stay locked. */
.layer.effect.water-shimmer{
  clip-path: polygon(0 52.7%, 100% 52.7%, 100% 79.25%, 0 69.93%);
  background-image:
    radial-gradient(ellipse, rgba(185,210,255,.14) 0 12%,
                             rgba(150,185,245,.06) 28%, transparent 68%),
    radial-gradient(ellipse, rgba(215,228,255,.09) 0 10%,
                             rgba(155,190,240,.04) 30%, transparent 70%);
  background-repeat: repeat;
  background-size: 88px 7px, 137px 11px;
  background-position: 0 0, 40px 6px;
  mix-blend-mode: screen;
  opacity: .24;
  animation: water-drift 14s steps(28, end) infinite;
}

@keyframes water-drift{
  0%   { background-position: 0 0, 40px 6px; opacity: .18; }
  18%  { opacity: .26; }
  72%  { background-position: 88px 7px, -97px 17px; opacity: .20; }
  100% { background-position: 88px 7px, -97px 17px; opacity: .18; }
}

/* ---------- mirror reflections (drive scene) ----------
   The rearview and door mirrors would otherwise sit as flat blue
   rectangles for the whole scene. Each now carries ONE streetlight
   sweeping past, on the same cadence as the lamps outside, over glass
   that has been dimmed to stop it being the brightest thing in the cab.

   ONE LIGHT, NOT A FIELD OF THEM. The first version tiled three fields
   of blobs and the rearview held eight or ten at once, which read as a
   lit texture rather than as anything passing. The tile is now LARGER
   THAN THE GLASS in both directions — 330x210 against a 100x93 mirror,
   150x90 against a 33x33 one — so at most one blob centre can ever be
   inside it. Keep that true if you retune: a tile smaller than the glass
   in either axis puts a second light in frame.

   ITS HEIGHT IS PINNED, NOT TILED. background-position-y places the
   blob's row at the middle of the glass (tile centre is half the tile
   height, so y = mirror centre - half tile), and because the tile is
   taller than the glass no other row can reach in. That is what keeps
   the light travelling along the mirror instead of appearing in stacks.

   LINEAR, NOT steps(). Measured off a screen capture, the stepped
   version updated about seven times a second against the scene's 60,
   and that — not the drift speed — is what read as sluggish and out of
   keeping. The scrolling bands outside move every frame; so does this.

   THE CADENCE IS LOCKED TO THE LAMP SPACING. One light per 2164.8 ms,
   which is blender/build_pier.py's drive_sweep_period_ms — the same
   number the lamp sweep is phase-locked to. The lamps behind the car are
   the ones just driven past, so the period is exactly one tile of
   travel. Deliberately COMMENSURATE with the sweep, the opposite of the
   penguin's breath (handoff 6.16): the breath must not beat against the
   lamps, the mirror is showing them.

   THE CLIP PATHS ARE MEASURED, NOT DRAWN, and each is declared once for
   both of its layers. They are the mirror glass's own outline, taken off
   the rendered cabin layer by blender/find_mirrors.js, which separates
   the glass by its distinctive flat blue — rgb(12,15,39) against a
   dashboard at (2,2,7) — then resamples it and pushes it 1.6 px outward.
   That outward push is not cosmetic: a resampled polygon is inscribed in
   the true outline and cuts every rounded corner, which left 6.2% of the
   rearview glass and 8.8% of the door's outside the clip and undimmed,
   reading as a bright rim. Overshooting costs nothing, because just
   outside the glass is the mirror's own near-black housing.

   Cross-checked where it could be: the door mirror is a real object
   (CarBody_Mirror_0) and the colour outline agrees with its projected
   geometry to about a pixel. The rearview has nothing to check against —
   its glass is merged into a 100k-triangle interior lump with no
   material of its own — so that agreement is what licenses trusting it
   there. Re-measure both if the camera or the car ever moves. */

.layer.effect.mirror-rear,
.layer.effect.mirror-rear-dim{
  clip-path: polygon(27.72% 13.10%, 28.28% 9.90%, 28.94% 6.75%, 29.83% 3.73%,
                     31.94% 4.71%, 34.07% 6.24%, 36.24% 7.80%,
                     38.45% 9.43%, 40.61% 11.13%, 42.70% 12.79%,
                     43.84% 15.38%, 43.82% 18.65%, 43.68% 21.89%,
                     42.24% 22.79%, 39.96% 21.74%, 37.66% 20.69%,
                     35.30% 19.61%, 32.93% 18.44%, 30.62% 17.23%,
                     28.38% 16.00%);
}

.layer.effect.mirror-door,
.layer.effect.mirror-door-dim{
  clip-path: polygon(56.84% 44.58%, 58.04% 44.17%, 59.30% 44.04%, 60.58% 44.07%,
                     61.82% 44.13%, 62.34% 45.41%, 62.44% 47.09%,
                     62.43% 48.78%, 62.03% 50.23%, 60.84% 50.78%,
                     59.59% 51.15%, 58.34% 51.38%, 57.31% 51.22%,
                     57.07% 49.61%, 56.88% 47.93%, 56.80% 46.23%);
}

/* THE GLASS ITSELF, dimmed. The light layers below only ADD (`screen`),
   so they cannot make a mirror darker — and measured against the video,
   most of what read as "too bright" was the glass, not the lights.
   Removing the old field of blobs took the mirrors from 4.06x and 5.44x
   the dashboard's mean down to 2.89x and 3.77x; this takes them to 2.27x
   and 2.96x.

   Black at `multiply` is exactly a dimmer: the result is the backdrop
   times (1 - opacity), so THE OPACITY BELOW IS THE PERCENTAGE DARKER and
   is the one number to turn. Past about .35 it starts to look like a
   dead mirror rather than a dark one. */
.layer.effect.mirror-rear-dim,
.layer.effect.mirror-door-dim{
  background-image:
    radial-gradient(ellipse, rgba(0,0,0,1) 0 100%, rgba(0,0,0,1) 100%);
  background-repeat: repeat;
  background-size: 64px 64px;
  background-position: 0 0;
  mix-blend-mode: multiply;
  opacity: .26;
}

/* `screen` so the light ADDS to the glass rather than replacing it, and
   so the lamp sweep still brightens the mirror as a lamp goes past.
   Effect layers carry no pick mask and cannot block a hotspot. */
.layer.effect.mirror-rear{
  background-image:
    radial-gradient(ellipse, rgba(255,228,186,.90) 0 1.5%,
                             rgba(255,196,122,.30) 4%,
                             rgba(255,172,92,.09) 8%, transparent 15%);
  background-repeat: repeat;
  background-size: 330px 210px;
  background-position: 0 -41px;
  mix-blend-mode: screen;
  opacity: .34;
  animation: mirror-rear-drift 2.1648s linear infinite;
}

@keyframes mirror-rear-drift{
  from{ background-position:     0 -41px; }
  to  { background-position: 330px -41px; }
}

/* Closer to the car and a much narrower slice of road, so its light
   crosses in about half the time the rearview's takes. */
.layer.effect.mirror-door{
  background-image:
    radial-gradient(ellipse, rgba(255,226,182,.92) 0 2%,
                             rgba(255,194,118,.32) 6%,
                             rgba(255,170,90,.10) 12%, transparent 22%);
  background-repeat: repeat;
  background-size: 150px 90px;
  background-position: 0 184px;
  mix-blend-mode: screen;
  opacity: .32;
  animation: mirror-door-drift 2.1648s linear infinite;
}

@keyframes mirror-door-drift{
  from{ background-position:     0 184px; }
  to  { background-position: 150px 184px; }
}

.layer.vending-flutter{
  animation: vending-power-dip 16.3s steps(1, end) -9.4s infinite;
}

.layer.vending-flutter-glow{
  animation: vending-glow-dip 16.3s steps(1, end) -9.4s infinite;
}

@keyframes vending-power-dip{
  0%, 86%, 100% { opacity: 1; }
  87%           { opacity: .76; }
  88%           { opacity: 1; }
  89%           { opacity: .86; }
  90%           { opacity: 1; }
}

@keyframes vending-glow-dip{
  0%, 86%, 100% { opacity: .92; }
  87%           { opacity: .22; }
  88%           { opacity: 1; }
  89%           { opacity: .42; }
  90%           { opacity: .92; }
}

/* The engine is running, so the brake glow never switches off. Its
   uneven low-amplitude shimmer suggests vibration and old electrics
   without reading as a turn signal or a deliberate pulse. */
.layer.brake-shimmer{
  animation: brake-shimmer 5.7s steps(9, end) -2.4s infinite;
}

@keyframes brake-shimmer{
  0%, 100% { opacity: .82; }
  13%      { opacity: .91; }
  27%      { opacity: .78; }
  42%      { opacity: .88; }
  58%      { opacity: .80; }
  73%      { opacity: .94; }
  86%      { opacity: .84; }
}

@media (prefers-reduced-motion: reduce){
  .layer.flicker,
  .layer.pulse,
  .layer.city-light,
  .layer.effect.water-shimmer,
  .layer.effect.mirror-rear,
  .layer.effect.mirror-door,
  .layer.effect.mart-lamp-insects::before,
  .layer.effect.mart-lamp-insects::after,
  .layer.brake-shimmer,
  .layer.vending-flutter,
  .layer.vending-flutter-glow{ animation: none; }
}

/* ---------- PS1 post-processing overlay ----------
   A tiled 4x4 Bayer dither + faint scanlines. This is what
   sells the 15-bit colour depth. Kept very subtle on purpose. */
.post{
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 900;
}

.post-dither{
  position: absolute;
  inset: 0;
  background-image: var(--dither-url);
  background-repeat: repeat;
  image-rendering: pixelated;
  mix-blend-mode: overlay;
  opacity: .16;
}

.post-scan{
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0,0,0,.16) 0px,
    rgba(0,0,0,.16) 1px,
    rgba(0,0,0,0)   1px,
    rgba(0,0,0,0)   2px
  );
  opacity: .5;
}

/* a slight vignette pulls the eye to the middle of the frame */
.post-vig{
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse 70% 70% at 50% 55%,
    rgba(0,0,0,0) 40%,
    rgba(0,0,0,.55) 100%
  );
}

/* ---------- caption bar (the adventure-game verb line) ---------- */

.caption{
  position: absolute;
  left: 0;
  right: 0;
  bottom: 10px;
  z-index: 950;
  text-align: center;
  pointer-events: none;
  font-size: 15px;
  line-height: 1;
  letter-spacing: .02em;
  color: #eaf2ff;
  text-shadow:
     1px 1px 0 #000, -1px 1px 0 #000,
     1px -1px 0 #000, -1px -1px 0 #000;
  opacity: 0;
  transition: opacity .12s steps(3);
}

.caption.is-on{ opacity: 1; }

/* ---------- scene title, shown briefly on entering a scene ---------- */

.slate{
  position: absolute;
  left: 12px;
  top: 10px;
  z-index: 950;
  font-size: 13px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #9fb6d8;
  text-shadow: 1px 1px 0 #000;
  pointer-events: none;
  opacity: 0;
}

.slate.is-on{
  animation: slate-in 3.2s steps(8) forwards;
}

@keyframes slate-in{
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  70%  { opacity: 1; }
  100% { opacity: 0; }
}

/* ---------- scene-to-scene fade ---------- */

.fade{
  position: absolute;
  inset: 0;
  z-index: 980;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s steps(4);
}

.fade.is-on{
  opacity: 1;
  pointer-events: all;
}

/* ---------- back button ---------- */

/* The one text-button look, shared by the corner buttons and the choice
   menu. Declared together on purpose: the menu was asked for with "the
   look of the current buttons", and splitting the two would let them
   drift the first time either is retuned. Only POSITION differs. */
.back,
.menu-item{
  font-family: inherit;
  font-size: 13px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #9fb6d8;
  background: none;
  border: 0;
  padding: 4px 6px;
  cursor: pointer;
  text-shadow: 1px 1px 0 #000;
  opacity: .6;
}

.back{
  position: absolute;
  right: 10px;
  top: 8px;
  z-index: 960;
}

.back:hover,
.menu-item:hover,
.menu-item:focus-visible{ opacity: 1; color: #fff; outline: 0; }
.back[hidden]{ display: none; }

/* ---------- the choice menu ----------
   Opened by a hotspot whose action is `menu` — the car on the pier. Just
   text, stacked, in the same style as the corner buttons above.

   POSITION IS SET BY THE ENGINE, at the cursor, and left/top here are
   only a safe starting point for the first layout pass. It was fixed in
   the lower left first and was too easy to miss; opening where you
   clicked keeps the choice next to the thing you clicked on, which is
   what a point-and-click expects. openMenu() clamps it so it can never
   hang off an edge — the one thing a fixed corner was better at.

   It lives inside #stage so it scales with everything else, and the
   engine positions it in the 640-wide render space, never in screen
   pixels. */
.menu{
  position: absolute;
  left: 0;
  top: 0;
  z-index: 965;
  display: flex;
  flex-direction: column;
  align-items: stretch;   /* both boxes as wide as the longer label */
  gap: 2px;
}

.menu[hidden]{ display: none; }

/* A box behind each line, because over the pier's concrete the bare text
   was hard to pick out. Alpha .8 against the .6 opacity the shared rule
   above already applies works out at ~.48 black at rest — the 50% asked
   for — and rises to .8 on hover, so hovering darkens the box as well as
   whitening the text. */
.menu-item{
  text-align: left;
  background: rgba(0, 0, 0, .8);
}

/* The second, optional button, stacked directly under the first. Its
   top is the first button's own box — 8px down, plus 13px of text at the
   default line height and 4px of padding either side — so the two read
   as one group rather than two stray controls. It shares .back for
   everything else; only the position differs. The audio toggle is
   bottom-right and cannot collide with it. */
.home{ top: 32px; }

/* ---------- global audio toggle ---------- */

.audio-toggle{
  position: absolute;
  right: 8px;
  bottom: 7px;
  z-index: 970;
  width: 34px;
  height: 25px;
  margin: 0;
  padding: 0;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: none;
  color: #9fb6d8;
  cursor: pointer;
  opacity: .6;
}

.audio-toggle:hover,
.audio-toggle:focus-visible{
  color: #fff;
  opacity: 1;
  outline: 0;
}

.audio-toggle:disabled{
  cursor: default;
  opacity: .22;
}

.audio-glyph,
.audio-glyph::before,
.audio-glyph::after,
.audio-waves,
.audio-x{
  position: absolute;
  display: block;
  box-sizing: border-box;
}

.audio-glyph{
  left: 1px;
  top: 1px;
  width: 32px;
  height: 23px;
  filter: drop-shadow(1px 1px 0 #000);
}

.audio-glyph::before{
  content: "";
  left: 1px;
  top: 8px;
  width: 6px;
  height: 7px;
  background: currentColor;
}

.audio-glyph::after{
  content: "";
  left: 6px;
  top: 3px;
  width: 10px;
  height: 17px;
  background: currentColor;
  clip-path: polygon(
    0 35%, 20% 35%, 20% 24%, 40% 24%, 40% 12%,
    60% 12%, 60% 6%, 100% 6%, 100% 94%, 60% 94%,
    60% 88%, 40% 88%, 40% 76%, 20% 76%, 20% 65%, 0 65%
  );
}

.audio-waves{
  left: 13px;
  top: 1px;
  width: 18px;
  color: currentColor;
  font-family: "VT323", "Courier New", monospace;
  font-size: 20px;
  line-height: 20px;
  letter-spacing: -5px;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
}

.audio-x{
  display: none;
  left: 17px;
  top: -2px;
  color: #d44b55;
  font-family: "VT323", "Courier New", monospace;
  font-size: 27px;
  font-weight: bold;
  line-height: 24px;
  text-shadow: 1px 1px 0 #000;
}

.audio-toggle.is-muted .audio-waves{ display: none; }
.audio-toggle.is-muted .audio-x{ display: block; }

/* ---------- loading ---------- */

/* The boot loader, and the line held on the black between two scenes —
   the same panel, declared together so the transition genuinely looks
   "like the loading screen" and stays that way if either is retuned.
   Above .fade (980) on purpose: the fade is still fully on underneath,
   so the outgoing scene is gone and the incoming one cannot flash
   through while it builds. */
.loading,
.interstitial{
  position: absolute;
  inset: 0;
  z-index: 985;
  display: grid;
  place-items: center;
  background: #000;
  color: #4d6488;
  font-size: 14px;
  letter-spacing: .2em;
  text-transform: uppercase;
}

/* A sentence, where the loader is one word: it needs room to breathe and
   somewhere to wrap. At 640 wide this line lands just inside the frame,
   and the padding keeps it off the edges if the text is ever changed. */
.interstitial{
  padding: 0 48px;
  text-align: center;
  line-height: 1.7;
}

.loading[hidden],
.interstitial[hidden]{ display: none; }

/* ---------- cursor states ---------- */

.viewport.is-hot{ cursor: pointer; }

/* ---------- reduced motion ----------
   Kill parallax and the blinking, keep the scene readable. */
@media (prefers-reduced-motion: reduce){
  .layer{ transform: none !important; }
  .slate.is-on{ animation: none; opacity: 1; }
}
