/* Web fonts (VT323, DotGothic16) are linked from index.html */

/* ── RESET */
*, *::before, *::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
}

html {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}
input, textarea, [contenteditable] {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

:root { --u: calc(100vw / 1440); }
@media (orientation: portrait) {
  :root { --u: calc(100vh / 844); --u: calc(100dvh / 844); }
}

/* ── CONTROLS — the size of each part */
:root {
  /* ── LANDSCAPE (wide screens) */
  --buttons-h:   1;    /* carousel buttons + frames + gaps */
  --filter-h:    0.8;    /* "filter: all" box, top left */
  --guestbook-h: 0.8;    /* guest book form, bottom centre */
  --entries-h:   0.8;    /* guest book cards, right edge */
  --counter-h:   1;    /* visitor counter, bottom */
  --loader-h:    0.7;    /* ENTER screen: loading bar + ENTER gif */
  --about-face:  0.92;   /* the face inside the about button (both orientations): 1 = fills it */

  /* ── PORTRAIT (tall screens) */
  --buttons-v:   1.3;
  --filter-v:    1;
  --guestbook-v: 1;
  --entries-v:   1;
  --counter-v:   1;
  --loader-v:    0.8;
}

/* Wiring — no need to touch. Each part gets its own unit: the site unit
   (--u, THE ONE RULE above) × that part's control. */
:root {
  --u-buttons:   calc(var(--u) * var(--buttons-h));
  --u-filter:    calc(var(--u) * var(--filter-h));
  --u-guestbook: calc(var(--u) * var(--guestbook-h));
  --u-entries:   calc(var(--u) * var(--entries-h));
  --u-counter:   calc(var(--u) * var(--counter-h));
  --u-loader:    calc(var(--u) * var(--loader-h));
}
@media (orientation: portrait) {
  :root {
    --u-buttons:   calc(var(--u) * var(--buttons-v));
    --u-filter:    calc(var(--u) * var(--filter-v));
    --u-guestbook: calc(var(--u) * var(--guestbook-v));
    --u-entries:   calc(var(--u) * var(--entries-v));
    --u-counter:   calc(var(--u) * var(--counter-v));
    --u-loader:    calc(var(--u) * var(--loader-v));
  }
}

/* ── WORLD (rotated on portrait) */
.world {
  position: fixed;
  inset: 0;
  z-index: 0;
}

/* Portrait: rotate the background art to fill the screen.
   Buttons live outside .world and stay upright. */
@media (orientation: portrait) {
  .world {
    inset: auto;
    width: 100vh;
    width: 100dvh;
    height: 100vw;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(90deg);
  }
}

/* ── BACKGROUND LOOP */
.bg {
  position: fixed;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}
.bg video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ── PARALLAX LAYERS */
.layer {
  position: fixed;
  inset: 0;
  z-index: 5;
  overflow: hidden;
  pointer-events: none;
  will-change: transform;
}

.layer img {
  position: absolute;
  left: -5%;
  top: 50%;
  width: 110%;
  height: auto;
  transform: translateY(-50%);
}

/* ── BUTTONS */
/* ── CAROUSEL SIZING */
:root {
  /* ── HORIZONTAL (wide screens) */
  --btn-size-h:  calc(151 * var(--u-buttons));   /* the button itself */
  --btn-gap-h:   calc(72 * var(--u-buttons));    /* space between buttons */
  --btn-pad-h:   calc(14 * var(--u-buttons));    /* breathing room at the ends of the strip */
  --btn-deco-h:  calc(223 * var(--u-buttons));   /* the frame GIF around each button */

  /* ── VERTICAL (tall screens) */
  --btn-size-v:  calc(101 * var(--u-buttons));
  --btn-gap-v:   calc(42 * var(--u-buttons));
  --btn-pad-v:   calc(17 * var(--u-buttons));
  --btn-deco-v:  calc(143 * var(--u-buttons));

  /* ── Both */
  --btn-deco-hover: 1.02;  /* how much the frame swells on hover */
  --btn-max:        none;  /* safety cap on button width; vertical sets one */

  /* Live set — horizontal by default, swapped by the media query. Don't edit. */
  --btn-size: var(--btn-size-h);
  --btn-gap:  var(--btn-gap-h);
  --btn-pad:  var(--btn-pad-h);
  --btn-deco: var(--btn-deco-h);
}

.buttons-frame {
  position: fixed;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  touch-action: pinch-zoom; /* drags can start here, between the buttons (carousel.js) */
  mask-image: var(--edge-dissolve-mask);
  mask-size: 100% 100%;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-image: var(--edge-dissolve-mask);
  -webkit-mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
}

.buttons {
  position: fixed;
  inset: 0;
  will-change: transform;
  display: flex;
  justify-content: center;
  justify-content: safe center;
  align-items: center;
  /* landscape: row */
  flex-direction: row;
  gap: var(--btn-gap);
  padding: var(--btn-pad);
  /* carousel: the strip is longer than the screen and slides (translate,
     carousel.js); the frame around it clips it */
  overflow: visible;
  /* Touch is dragged and flung by carousel.js itself (like the mouse): the
     phone's own scrolling stops dead every time the endless loop jumps. */
  touch-action: pinch-zoom;
  scrollbar-width: none;
  -ms-overflow-style: none;
  cursor: default;
}

.buttons.in-band {
  cursor: grab;
}

.buttons::-webkit-scrollbar {
  display: none;
}

.buttons.dragging {
  cursor: grabbing;
}

.btn-wrap {
  position: relative;
  flex-shrink: 0;
}

/* portrait: vertical column */
@media (orientation: portrait) {
  /* No numbers here on purpose — this only points the live tokens at the -v set.
     Every button size lives in the one block at the top of the file. */
  :root {
    --btn-size: var(--btn-size-v);
    --btn-gap:  var(--btn-gap-v);
    --btn-pad:  var(--btn-pad-v);
    --btn-deco: var(--btn-deco-v);
    --btn-max:  80vw;   /* a very narrow screen must not push buttons off the sides */
  }
  .buttons {
    flex-direction: column;
  }
}

/* Per-button decoration GIF — larger than button, centered, bleeds outside */
.btn-decoration {
  position: absolute;
  width: var(--btn-deco);
  height: var(--btn-deco);
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 3;
  transition: transform 0.15s ease-out;
}
video.btn-decoration,
canvas.btn-decoration {
  filter: url(#luma-alpha);
  object-fit: contain;
}
/* Safari's engine (every iPhone browser) doesn't draw that filter on a
   playing video — the frames vanished. Screen blending works there. */
html.is-webkit video.btn-decoration,
html.is-webkit canvas.btn-decoration { filter: none; mix-blend-mode: screen; }

/* The one hidden player per button video (button-videos.js). Kept on screen
   (tiny, near-invisible) so phones don't pause them as "not visible". */
#bv-players { position: fixed; left: 0; top: 0; width: 2px; height: 2px; overflow: hidden; opacity: 0.01; pointer-events: none; z-index: 0; }
#bv-players video { display: block; width: 2px; height: 2px; }

video::-webkit-media-controls { display: none !important; }
video::-webkit-media-controls-panel { display: none !important; }
video::-webkit-media-controls-start-playback-button { display: none !important; -webkit-appearance: none; }
video::-webkit-media-controls-overlay-play-button { display: none !important; }

.btn-wrap:hover .btn-decoration {
  transform: translate(-50%, -50%) scale(var(--btn-deco-hover));
}

/* The frame drawn in code (frame.js) — same box as the frame video. */
.btn-frame {
  position: absolute;
  width: var(--btn-deco);
  height: var(--btn-deco);
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  overflow: visible;
  pointer-events: none; /* indicators only: taps go to the button */
  z-index: 4;
  transition: transform 0.15s ease-out;
}
.btn-wrap:hover .btn-frame {
  transform: translate(-50%, -50%) scale(var(--btn-deco-hover));
}

.btn {
  position: relative;
  width: var(--btn-size);
  height: var(--btn-size);
  max-width: var(--btn-max);
  overflow: hidden;
  text-decoration: none;
  display: block;
  background: none;
  border: none;
  padding: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* The button's picture: a GIF <img> or a video (button-videos.js plays it). */
/* (Button videos are drawn into <canvas class="… bv"> copies — button-videos.js.) */
.btn img,
.btn video,
.btn canvas.bv {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.btn-hover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 3;
  opacity: 0;
  transition: opacity 0.2s;
}

.btn.locked:hover {
  cursor: not-allowed;
}

/* Socials: the "menu open" picture sits on top, shown while the pie is open
   (pie-menu.js adds .open to the trigger). */
.btn .btn-open { z-index: 2; opacity: 0; }
.btn.open .btn-open { opacity: 1; }

.btn.locked:hover .btn-hover,
.btn.show-locked .btn-hover { /* main.js: on a tap, and when the buttons come back */
  opacity: 1;
}

/* ── LOCK OVERLAY */
.lock-overlay {
  position: fixed;
  inset: 0;
  z-index: 10;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s;
}

.lock-overlay.active {
  opacity: 1;
  pointer-events: auto;
}

.lock-overlay video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (orientation: portrait) {
  .lock-overlay video {
    position: absolute;
    width: 100vh;
    width: 100dvh;
    height: 100vw;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(90deg);
  }
}

/* Buttons hidden during locked video playback */
/* ── Project feeds (project-feed.js) */
.feed {
  position: fixed;
  inset: 0;
  z-index: 45;
  opacity: 0;
  transform: scale(0.12);
  transition: opacity var(--feed-open-ms, 320ms) ease-out, transform var(--feed-open-ms, 320ms) cubic-bezier(.2, .8, .2, 1);
  cursor: grab;
  touch-action: pinch-zoom;
}
.feed.is-open { opacity: 1; transform: none; }
/* The page behind a feed, darkened (project-feed.js, FEED.dim). */
.feed-dim {
  position: fixed;
  inset: 0;
  z-index: 44;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--feed-open-ms, 320ms) ease-out;
}
.feed-dim.is-open { opacity: var(--feed-dim, 0.85); }
/* Full focus on the project: the guestbook stops scrolling behind it. */
body.feed-shown #entries-scroll { animation-play-state: paused !important; }
.feed-track {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* Each tile is placed along the strip by project-feed.js (transform). */
.feed-tile {
  position: absolute;
  left: 0;
  top: 0;
  height: calc(var(--feed-size) * 100vh);
  aspect-ratio: var(--feed-aspect);
  will-change: transform;
}
.feed.is-vertical .feed-tile { height: auto; width: calc(var(--feed-size) * 100vw); }
.feed-tile video,
.feed-tile img { display: block; width: 100%; height: 100%; object-fit: cover; pointer-events: none; user-select: none; }
/* A project's text card (project-feed.js: title / text per project). Same
   height as the videos on wide screens, same width on tall ones. */
.feed-tile.feed-info {
  aspect-ratio: auto;
  width: min(30em, 60vw);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.8em;
  padding: 0 1.5em;
  box-sizing: border-box;
  color: #fff;
  font-family: var(--ab-font, 'VT323', monospace);
  font-size: 18px;
  line-height: 1.35;
  text-align: left;
}
.feed.is-vertical .feed-tile.feed-info { height: auto; width: calc(var(--feed-size) * 100vw); padding: 1.5em 0; font-size: 16px; }
.feed-info h2 { margin: 0; font: inherit; font-size: 1.4em; }
.feed-info p { margin: 0; }

/* A sound video's volume bar (project-feed.js SOUND): under the video on wide
   screens, on its left on tall ones. It travels with its video. */
.feed-vol {
  position: absolute;
  top: 100%;
  left: 50%;
  width: calc(var(--vol-len, 0.5) * 100%);
  height: 22px;                       /* the line is thin; this is the place to touch */
  margin-top: calc(var(--vol-gap, 14px) - 11px + 1px);
  transform: translateX(-50%);
  cursor: pointer;
  touch-action: none;
  outline: none;
}
.feed-vol-track { position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: rgba(255, 255, 255, 0.3); }
.feed-vol-fill  { position: absolute; left: 0; top: 0; bottom: 0; width: calc(var(--vol, 0.8) * 100%); background: #fff; }
.feed-vol-knob  {
  position: absolute; top: 50%; left: calc(var(--vol, 0.8) * 100%);
  width: 7px; height: 7px; background: #fff;
  transform: translate(-50%, -50%);
}
.feed-vol:focus-visible .feed-vol-knob { outline: 1px solid #fff; outline-offset: 2px; }
.feed.is-vertical .feed-vol {
  top: 50%;
  left: auto;
  right: 100%;
  width: 22px;
  height: calc(var(--vol-len, 0.5) * 100%);
  margin: 0 calc(var(--vol-gap, 14px) - 11px + 1px) 0 0;
  transform: translateY(-50%);
}
.feed.is-vertical .feed-vol-track { left: 50%; right: auto; top: 0; bottom: 0; width: 1px; height: auto; }
.feed.is-vertical .feed-vol-fill  { top: auto; bottom: 0; left: 0; right: 0; width: auto; height: calc(var(--vol, 0.8) * 100%); }
.feed.is-vertical .feed-vol-knob  { top: auto; left: 50%; bottom: calc(var(--vol, 0.8) * 100%); transform: translate(-50%, 50%); }

/* The loading bar above the videos (project-feed.js LOADING): fills as they
   download, then fades away. */
.feed-load {
  position: absolute;
  left: 50%;
  top: calc(50vh - var(--feed-size) * 50vh - var(--load-gap, 18px) - 3px);
  width: calc(var(--load-w, 0.3) * 100vw);
  height: 3px;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, 0.2);
  pointer-events: none;
  transition: opacity 0.6s ease-out;
}
.feed.is-vertical .feed-load { top: var(--load-gap, 18px); }
.feed-load-fill {
  position: absolute; inset: 0;
  background: #fff;
  transform: scaleX(var(--load, 0));
  transform-origin: left;
  transition: transform 0.2s linear;
}
.feed-load.is-done { opacity: 0; }
/* The main strip steps back while a feed is open. */
.buttons.feed-open .btn-wrap { opacity: var(--feed-strip, 0.12); pointer-events: none; transition: opacity 0.3s; }
/* A feed button isn't "locked": normal pointer on hover. */
.btn.locked[data-feed]:hover { cursor: pointer; }

.buttons.locked-playing .btn-wrap {
  opacity: 0;
  pointer-events: none;
}

@keyframes flicker-in {
  0%   { opacity: 0; }
  10%  { opacity: 0.8; }
  12%  { opacity: 0; }
  30%  { opacity: 0.9; }
  32%  { opacity: 0; }
  50%  { opacity: 1; }
  52%  { opacity: 0; }
  68%  { opacity: 1; }
  70%  { opacity: 0; }
  85%  { opacity: 1; }
  100% { opacity: 1; }
}

.btn-wrap.flickering {
  animation: flicker-in 0.45s both; /* both: stays hidden during an animation-delay */
}
/* Filter switch (filter.js): a lighter blink-in — dim, on, dim, on.
   steps(1) holds each value until the next keyframe (hard cuts, no fading). */
@keyframes flicker-in-soft {
  0%   { opacity: 0.5; }
  25%  { opacity: 1; }
  50%  { opacity: 0.5; }
  75%  { opacity: 0.2; }
  100% { opacity: 1; }
}
.btn-wrap.flickering.flicker-soft {
  animation: flicker-in-soft 0.3s steps(1, end) both;
}
/* Filter switch (filter.js): the reverse — blink out, ending hidden. */
@keyframes flicker-out {
  0%   { opacity: 1; }
  15%  { opacity: 0; }
  30%  { opacity: 1; }
  50%  { opacity: 0; }
  65%  { opacity: 0.8; }
  80%, 100% { opacity: 0; }
}
.btn-wrap.flickering-out {
  animation: flicker-out 0.3s forwards;
}

.looker { position: relative; }
.looker > .look-stack { position: absolute; inset: 0; pointer-events: none; }
.look-frame { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; visibility: hidden; }
.look-frame.is-on { visibility: inherit; }
/* An image already in the element (e.g. an about-page layer's own picture)
   only holds the size; the stack draws over it. */
.looker > img:not(.look-frame) { visibility: hidden; }
/* In a carousel button: fill the button, like the button's own picture. */
.btn > .looker { position: absolute; inset: 0; scale: var(--about-face, 1); }
/* Flipbook (flipbook.js): a copy of the about face in every button, shown over
   the button picture while the strip is scrolled fast (.is-flipbook). */
.btn > .btn-flip { position: absolute; inset: 0; z-index: 4; visibility: hidden; pointer-events: none; background: #000; }
.btn-flip > .looker { position: absolute; inset: 0; scale: var(--about-face, 1); }
.buttons.is-flipbook .btn-flip { visibility: visible; }
.buttons.is-flipbook.flip-hides-frames .btn-decoration { visibility: hidden; }
/* A frame over the button picture (about: around the face), drawn on black
   and "screen" blended so the black disappears. */
.btn > .btn-overlay { z-index: 2; mix-blend-mode: screen; pointer-events: none; }
.btn .look-frame { object-fit: cover; }
.ab-el-img .look-frame { height: 100%; }

.ab-swarm { position: fixed; inset: 0; z-index: 70; overflow: hidden; cursor: pointer; touch-action: pinch-zoom; /* a finger drag wakes them (about-swarm.js), not a scroll */ }
/* Every face is the design exactly as drawn; they differ only in behaviour. */
.ab-swarm-face { position: absolute; }
/* The clicked face, staying put on top of the swarm (about-swarm.js). */
.ab-swarm-hero { position: absolute; z-index: 3; }
/* Blinking (about-swarm.js): the shut / half-open eyes over the eye frames. */
.ab-swarm-blink { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: 1; pointer-events: none; }
/* Preview aid only (markStill in about-swarm.js / the ?tune SWARM tab):
   the still faces shown inverted, so they're easy to spot while designing. */
.ab-swarm.is-marking .ab-swarm-face[data-type="still"] { filter: invert(1); }
/* A face that starts the swarm looks clickable. */
.ab-el[data-look-swarm] { cursor: pointer; }

.ab-soul { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.ab-soul.is-behind { z-index: -1; }
/* Preview aid only (placeholder in about-soul.js / the ?tune SOUL tab): the
   face's own images stand in for the spirit, inverted to tell them apart. */
.ab-soul.is-placeholder { filter: invert(1); }
.ab-soul-fly { position: fixed; z-index: 75; pointer-events: none; }
.ab-soul-video { z-index: 80; }

/* ── FILTER (filter.js) — opposite the guest book form: top centre on wide
   screens, on the right on tall ones. Sizes in design px. */
:root {
  --fi-top:  100;       /* wide: distance from the top of the screen (inside the UI frame, above the buttons) */
  --fi-edge: 90;        /* tall: distance from the right edge (clear of the guest book cards) */

  /* Scale of the squares, lines and arrow (not the text, not the placement).
     The whole filter, placement included, also scales with --filter-h /
     --filter-v in CONTROLS at the top of this file. */
  --fi-scale-h: 0.3;    /* wide screens */
  --fi-scale-v: 0.25;    /* tall screens */

  /* ── TREE (filter.js STYLE 'tree'). × --fi-scale; filter.js rounds every
     one to whole screen pixels, so the gap inside a picked square is even. */
  --ft-main:      40;   /* the square you open it with */
  --ft-child:     40;   /* the three category squares */
  --ft-outline:   3;    /* their outline */
  --ft-inset:     8;    /* gap between the outline and the fill of a picked one */
  --ft-line:      3;    /* the lines out to them */
  --ft-spread:    90;   /* centre to centre, between the three */
  --ft-drop:      70;   /* from the square to the three */
  --ft-arrow-w:   25;   /* the arrow pointing at the square */
  --ft-arrow-h:   20;
  --ft-arrow-line: 3;
  --ft-arrow-gap: 14;   /* between the arrow and the square */
  /* The text ("filters", then the picked name): NOT × --fi-scale, so it stays readable. */
  --ft-text:      12;
  --ft-text-gap:  4;   /* between the text and the square */
  /* The three squares wiggle a little while it's open (their lines follow). */
  --ft-wiggle-x:     3; /* how far, sideways (× --fi-scale, like the squares) */
  --ft-wiggle-y:     3; /* how far, up and down */
  --ft-wiggle-speed: 1; /* 0 = still */
  --ft-blur:     0.4px; /* soften the whole filter (squares, lines, arrow, text); 0 = sharp */

  /* ── ART (filter.js STYLE 'art'): icons in a row. */
  --fi-size: 40;        /* icon height */
  --fi-gap:  10;        /* space between the icons */

  --fi-scale: var(--fi-scale-h); /* live value — don't edit */
}
@media (orientation: portrait) {
  :root { --fi-scale: var(--fi-scale-v); }
}

.btn-wrap.is-filtered-out { display: none; }

#filter {
  position: fixed;
  filter: blur(var(--ft-blur, 0px));
  z-index: 7; /* over the carousel (2) and HUD (3–6), under the lock overlay (10) */
}

/* ── Tree: filter.js sets every size and position in px. */
.ft-probe { /* filter.js reads the design units off this */
  position: fixed; left: 0; top: var(--gb-top);
  width: calc(1000 * var(--u-filter) * var(--fi-scale));
  height: calc(1000 * var(--u-filter));
  visibility: hidden; pointer-events: none;
}
.ft-svg { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.ft-arrow { fill: none; stroke: #fff; stroke-linecap: square; stroke-linejoin: miter; }
#filter.is-open .ft-arrow { visibility: hidden; }
.ft-line {
  stroke: #fff;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;               /* pathLength 1: fully hidden */
  transition: stroke-dashoffset 120ms linear; /* duration: filter.js LINE_MS / OUT_MS */
}
#filter.is-open .ft-line { stroke-dashoffset: 0; }

.ft-main, .ft-pick {
  position: absolute;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
/* Bigger than they look, for fingers. */
.ft-main::before, .ft-pick::before { content: ''; position: absolute; inset: -8px; }
.ft-main { background: #fff; border: none; }
/* The three: invisible tap areas over their drawing in the svg (.ft-kid). */
.ft-pick { background: transparent; border: none; visibility: hidden; }
#filter.is-open .ft-pick { visibility: visible; }
.ft-pick:disabled { cursor: default; }
.ft-ring, .ft-dot { fill: #fff; }
.ft-kid { visibility: hidden; }
.ft-dot { visibility: hidden; }
.ft-kid.is-on .ft-dot { visibility: inherit; }
/* They blink in as the lines reach them; closing, they're gone at once. */
#filter.is-open .ft-kid {
  visibility: visible;
  animation: ft-in var(--ft-blink-ms, 150ms) steps(1, end) var(--ft-line-ms, 120ms) both; /* filter.js BLINK_MS / LINE_MS */
}
@keyframes ft-in {
  0%   { opacity: 0; }
  25%  { opacity: 1; }
  50%  { opacity: 0.4; }
  75%, 100% { opacity: 1; }
}

.ft-text {
  position: absolute;
  font-family: 'fi-font', 'VT323', monospace;
  line-height: 1;
  color: #fff;
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
}

/* The decipher, as on the about page: a scrambling glyph cut out of a block
   in the text colour, until the letter resolves. */
.fi-ch { position: relative; }
.fi-ch.is-ciphered { color: transparent; }
.fi-ch.is-ciphered::before {
  content: '';
  position: absolute;
  inset: 0;
  background: #fff;
}
.fi-ch.is-ciphered::after {
  content: attr(data-g);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'fi-glyph', 'DotGothic16', monospace;
  font-size: 0.5em;
  line-height: 1;
  color: #000;
  writing-mode: horizontal-tb;
}

/* ── Art: icons in a row (a column on tall screens). */
#filter.fi-style-art {
  top: calc(var(--fi-top) * var(--u-filter));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: calc(var(--fi-gap) * var(--u-filter));
}
@media (orientation: portrait) {
  #filter.fi-style-art {
    top: var(--gb-top); /* level with the guest book tab on the left */
    left: auto;
    right: calc(var(--fi-edge) * var(--u-filter));
    transform: translateY(-50%);
    flex-direction: column;
  }
}
.filter-icon {
  display: block;
  position: relative;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  line-height: 0;
}
.filter-icon img { display: none; height: calc(var(--fi-size) * var(--fi-scale) * var(--u-filter)); width: auto; }
/* One picture at a time: filter.js sets show-idle / show-active / show-tap
   (the tap GIF plays once, then it settles on idle or active). */
.filter-icon.show-idle .fi-idle,
.filter-icon.show-active .fi-active,
.filter-icon.show-tap .fi-tap { display: block; }
.filter-icon:disabled { cursor: default; }
/* Until an icon's art is uploaded: its name as text. */
.filter-icon .fi-label { display: none; }
.filter-icon.no-art img { display: none; }
.filter-icon.no-art .fi-label {
  display: block;
  padding: 0.3em 0.6em;
  border: 1px solid #fff;
  background: #000;
  color: #fff;
  font-family: 'VT323', monospace;
  font-size: calc(12 * var(--fi-scale) * var(--u-filter));
  line-height: 1;
  letter-spacing: 0.1em;
}
.filter-icon.no-art.is-on .fi-label { background: #fff; color: #000; }

/* ── LOADING GATE */
.enter-overlay { position: fixed; inset: 0; z-index: 20; background: #000; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: opacity 0.5s; }
.enter-overlay.dismissed { opacity: 0; pointer-events: none; }
.enter-overlay:not(.ready) { cursor: default; }
:root { --gate-btn: calc(120 * var(--u-loader)); --gate-bar: calc(165 * var(--u-loader)); }
@media (orientation: portrait) { :root { --gate-btn: calc(117 * var(--u-loader)); --gate-bar: calc(195 * var(--u-loader)); } }
.enter-overlay .enter-btn { max-width: var(--gate-btn); max-height: var(--gate-btn); display: none; cursor: pointer; }
.enter-overlay.ready .enter-btn { display: block; }
.enter-overlay.ready .loader { display: none; }
.loader { display: flex; flex-direction: column; align-items: center; gap: calc(10 * var(--u-loader)); width: var(--gate-bar); }
.loader-bar { width: 100%; height: calc(8 * var(--u-loader)); border: 1px solid #fff; background: #000; overflow: hidden; }
.loader-fill { height: 100%; width: 0%; background: #fff; transition: width 0.2s ease-out; }
.loader-bar + .loader-sub { margin-top: calc(-6 * var(--u-loader)); } /* just under the bar */
/* The thin bar (loading-gate.js): one file at a time. While it loads, a short
   segment steps back and forth; when it's ready the bar fills for a moment. */
.loader-sub { position: relative; width: 100%; height: max(1px, calc(2 * var(--u-loader))); background: rgba(255, 255, 255, 0.18); overflow: hidden; }
.loader-sub-fill { position: absolute; top: 0; bottom: 0; left: 0; width: 25%; background: #fff; }
.loader-sub.is-fail .loader-sub-fill { background: #ff5a5a; }
.loader-pct { font-family: monospace; font-size: calc(11 * var(--u-loader)); letter-spacing: 0.2em; color: #fff; }
.loader { position: relative; }
.loader-log {
  position: absolute;
  top: calc(100% + 18 * var(--u-loader));
  left: 0;
  width: max-content;
  max-width: calc(50vw + var(--gate-bar) / 2 - 16px);
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: monospace;
  font-size: calc(9 * var(--u-loader));
  line-height: 1.5;
  letter-spacing: 0.05em;
  color: #fff;
  text-align: left;
  white-space: pre;       /* keep every space: the status field is fixed-width */
}
.loader-log.is-waiting { opacity: 0.45; }
.loader-log.is-fail { color: #ff5a5a; }

/* ── PIE MENU */
.pie-menu {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 50;
}

.pie-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}

.pie-center {
  position: absolute;
  overflow: hidden;
  pointer-events: none;
  transform: scale(0);
  transform-origin: center;
}

.pie-center img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.pie-item {
  position: absolute;
  overflow: hidden;
  cursor: pointer;
  pointer-events: auto;
  transform: scale(0);
  transform-origin: center;
}

.pie-item img,
.pie-item video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.pie-item img { filter: grayscale(1); } /* the old GIF placeholders only; videos show as they are */

.pie-item-label {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  text-align: center;
  color: #fff;
  font-family: 'VT323', monospace;
  font-size: calc(8 * var(--u)); /* pie-menu.js PIE_LABEL_SIZE overrides, in the same units */
  letter-spacing: 0.2em;
  text-transform: uppercase;
  z-index: 2;
  mix-blend-mode: difference;
  pointer-events: none;
}

.pie-toast {
  position: fixed;
  background: #000;
  color: #fff;
  border: 1px solid #fff;
  padding: calc(5 * var(--u)) calc(10 * var(--u));
  font-family: 'VT323', monospace;
  font-size: calc(16 * var(--u));
  letter-spacing: 0.1em;
  white-space: nowrap;
  transform: translateX(-50%);
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
  z-index: 60;
}

.pie-toast.visible { opacity: 1; }

/* ── GUESTBOOK — FORM */
/* ── The little character */
:root {
  --gb-gap: 10;         /* wide: design px between the box and the visitor counter */
  --gb-jump: 1.2em;     /* wide: how high the box jumps (em = the form's size) */
  --gb-top: 50%;        /* tall: where the tab sits, from the top */
  --gb-hop: 3em;        /* tall: how far the tab hops out */
  --gb-hop-time: 1.3s;  /* both: how long one jump / hop lasts */
}

/* ── Wide screens: face box at the bottom centre */
#form-container {
  position: fixed;
  bottom: calc((12 + 13.5) * var(--u-counter) + 2px + var(--gb-gap) * var(--u));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  z-index: 3;
  font-size: calc(9 * var(--u-guestbook));
}
#form-container #form-inner { display: none; }
#form-container.is-revealed #form-inner { display: grid; animation: gb-open 0.18s ease-out; }
@media (orientation: landscape) {
  #form-container.is-revealed #form-inner { position: relative; background: #000; }
  #form-container.is-revealed #form-tab {
    position: absolute;
    bottom: calc(100% - 1px);
    left: 50%;
    z-index: -1;
    pointer-events: none;
    transform: translate(-50%, 110%);
    /* Invisible while hiding: the form fades in when it opens, and a tab
       parked behind it would show through for those few frames. */
    visibility: hidden;
  }
  #form-container.is-revealed.gb-peek #form-tab,
  #form-container.is-revealed.gb-duck #form-tab { visibility: visible; }
  #form-container.is-revealed.gb-peek #form-tab { transform: translate(-50%, 0); animation: gb-peek-up 0.3s ease-out; }
  #form-container.is-revealed.gb-duck #form-tab { animation: gb-peek-down 0.2s ease-in; }
}
@keyframes gb-peek-up {
  0%   { transform: translate(-50%, 110%); }
  70%  { transform: translate(-50%, -12%); }
  100% { transform: translate(-50%, 0); }
}
@keyframes gb-peek-down {
  from { transform: translate(-50%, 0); }
  to   { transform: translate(-50%, 110%); }
}
@media (orientation: landscape) {
  /* The bubble then sits above the character, not the form. */
  #form-container.is-revealed.gb-peek #gb-bubble { bottom: calc(100% + 2.4em + 0.667em + 2px); }
}
/* Opening: a quick fade up. Size stays put, so the pointer that opened it
   is already inside the form and hover doesn't flicker. */
@keyframes gb-open { from { opacity: 0; transform: translateY(0.4em); } }

#form-tab {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  border: 1px solid #fff;
  padding: 0.25em 0.6em;
  cursor: pointer;
  font-family: 'VT323', monospace;
  font-size: 1.591em;
  line-height: 1;
  color: #fff;
  letter-spacing: 0.1em;
  white-space: pre;
  user-select: none;
}

/* The jump: up fast, a small bounce on landing, a beat, back down. */
@keyframes gb-jump {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  16%      { transform: translateX(-50%) translateY(calc(var(--gb-jump) * -1)); }
  30%      { transform: translateX(-50%) translateY(0); }
  40%      { transform: translateX(-50%) translateY(calc(var(--gb-jump) * -0.35)); }
  50%      { transform: translateX(-50%) translateY(0); }
}
#form-container.gb-hop { animation: gb-jump var(--gb-hop-time) ease-out; }

/* ── Tall screens: tab on the left edge, form slides out ── */
@media (orientation: portrait) {
  #form-container {
    font-size: calc(8.8 * var(--u-guestbook));
    bottom: auto;
    top: var(--gb-top);
    left: -17.5em;          /* = #form-inner's width: only the tab shows */
    transform: translateY(-50%);
    flex-direction: row;
    align-items: stretch;
    transition: left 0.3s ease-out;
  }
  #form-container.is-revealed { left: 0; }
  #form-container #form-inner { display: grid; pointer-events: none; }
  #form-container.is-revealed #form-inner { pointer-events: auto; animation: none; }
  #form-container.is-revealed #form-tab { display: flex; }
  #form-tab {
    width: 2.286em;
    padding: 0;
    border-left: none;
    writing-mode: vertical-rl;
    line-height: normal;
  }
  #form-container.gb-hop { animation-name: gb-hop; }
}
/* The hop: out fast, a little bounce, a beat, then back in. */
@keyframes gb-hop {
  0%, 100% { transform: translateY(-50%) translateX(0); }
  18%      { transform: translateY(-50%) translateX(calc(var(--gb-hop) * 1.1)); }
  28%      { transform: translateY(-50%) translateX(calc(var(--gb-hop) * 0.8)); }
  36%      { transform: translateY(-50%) translateX(var(--gb-hop)); }
  72%      { transform: translateY(-50%) translateX(var(--gb-hop)); }
}
@media (prefers-reduced-motion: reduce) {
  #form-container.gb-hop { animation: none; }
  #form-container.is-revealed #form-inner { animation: none; }
}

#form-inner { box-sizing: border-box; border: 1px solid #fff; padding: 0.75em; display: grid; grid-template-columns: 1fr auto; gap: 0.375em; width: 17.5em; overflow: hidden; }
#form-inner input, #form-inner button { min-width: 0; }
/* After sending, the fields are hidden but keep their space; the face and the
   thanks message sit on top of them (outside the grid, so the size never changes). */
#form-inner { position: relative; }
#form-inner.is-sent { background: #000; } /* phones: solid behind the thanks, like on desktop */
#title { grid-column: 1/-1; font-family: 'VT323', monospace; font-size: 1em; letter-spacing: 0.15em; text-align: center; pointer-events: none; color: #fff; }
#char-counter { grid-column: 1/-1; font-family: 'VT323', monospace; font-size: 0.8125em; text-align: right; background: #000; color: #fff; opacity: 0.5; padding: 0.154em 0.308em; }
#char-counter.warn { opacity: 1; color: #c00; }
#form-container input[type="text"] { border: 1px solid #000; padding: 0.5em 0.75em; font-family: 'VT323', monospace; font-size: 1em; outline: none; background: #fff; }
#form-container button { border: 1px solid #000; padding: 0.5em 1em; font-family: 'VT323', monospace; font-size: 1em; background: #000; color: #fff; cursor: pointer; transition: background 0.15s, color 0.15s; }
#form-container button:hover { background: #fff; color: #000; }
#form-container #kaomoji-btn { padding: 0.533em 0.667em; font-size: 0.9375em; white-space: nowrap; }
#kaomoji-btn, #enter-btn { border: 1px solid #fff; }
#kaomoji-picker { display: none; position: absolute; bottom: calc(100% + 0.5em); left: 50%; transform: translateX(-50%); background: #000; border: 1px solid #444; padding: 0.5em; grid-template-columns: repeat(4, 1fr); gap: 0.25em; z-index: 4; width: max-content; }
#kaomoji-picker.open { display: grid; }
#form-container .kao { font-size: 0.875em; padding: 0.286em 0.429em; color: #fff; cursor: pointer; white-space: nowrap; background: transparent; border: 1px solid transparent; font-family: 'VT323', monospace; }
.kao:hover { border-color: #444; }
#submission-kao { display: none; position: absolute; left: 0; right: 0; top: calc(50% + 0.6em); transform: translateY(-50%); text-align: center; font-family: 'VT323', monospace; font-size: 1.375em; color: #fff; padding: 0.182em 0; white-space: nowrap; }
#done-message { display: none; position: absolute; left: 0; right: 0; top: calc(50% + 0.6em); transform: translateY(-50%); text-align: center; font-family: 'VT323', monospace; font-size: 1em; color: #fff; opacity: 0.5; padding: 0.25em 0; }
@keyframes gb-annoyed {
  0%, 100% { translate: 0 0; }
  20%      { translate: -0.35em 0; }
  40%      { translate: 0.35em 0; }
  60%      { translate: -0.2em 0; }
  80%      { translate: 0.12em 0; }
}
#form-container.gb-annoyed { animation: gb-annoyed 0.32s steps(2); }
#form-container input[type="text"].gb-missing { border-color: #c00; box-shadow: 0 0 0 1px #c00; }
@media (prefers-reduced-motion: reduce) { #form-container.gb-annoyed { animation: none; } }
/* Tall screens: the picker opens along the slide-out, three across. */
@media (orientation: portrait) {
  #kaomoji-picker { left: 0; transform: none; width: 17.5em; grid-template-columns: repeat(3, 1fr); padding: 0.682em; gap: 0.341em; }
  #form-container .kao { font-size: 0.75em; padding: 0.25em 0.333em; }
}

/* "psst!" — a speech bubble pointing at the face. Wide: above the box.
   Tall: just right of the tab. It rides along with the jump / hop. */
#gb-bubble { position: absolute; bottom: calc(100% + 0.667em); left: 50%; transform: translateX(-50%) scale(0.85); transform-origin: bottom center; background: #000; border: 1px solid #fff; color: #fff; font-family: 'VT323', monospace; font-size: 0.9375em; letter-spacing: 0.05em; padding: 0.4em 0.667em; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.3s ease, transform 0.3s ease; z-index: 5; }
#gb-bubble.show { opacity: 1; transform: translateX(-50%) scale(1); }
#gb-bubble::before, #gb-bubble::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 0.4em solid transparent; border-top-color: #fff; }
#gb-bubble::after { top: calc(100% - 1px); border-width: 0.333em; border-top-color: #000; }
@media (orientation: portrait) {
  #gb-bubble { bottom: auto; left: calc(100% + 0.667em); top: 50%; transform: translateY(-50%) scale(0.85); transform-origin: left center; }
  #gb-bubble.show { transform: translateY(-50%) scale(1); }
  #gb-bubble::before, #gb-bubble::after { top: 50%; left: auto; right: 100%; transform: translateY(-50%); border-top-color: transparent; border-right-color: #fff; }
  #gb-bubble::after { top: 50%; right: calc(100% - 1px); border-right-color: #000; }
}
#form-container.is-revealed #gb-bubble { opacity: 0; }
#form-container.is-revealed.gb-peek #gb-bubble.show { opacity: 1; }

/* ── GUESTBOOK — ENTRIES */
:root {
  --card-w: calc(165 * var(--u-entries));
  --card-off: calc(82.5 * var(--u-entries));
  --scroll-gap: calc(12 * var(--u-entries));
  --entry-font: calc(10.5 * var(--u-entries));
  --entry-pad: calc(6 * var(--u-entries)) calc(9 * var(--u-entries));
  --meta-name-font: calc(9.75 * var(--u-entries));
  --meta-date-font: calc(8.25 * var(--u-entries));
}
@media (orientation: portrait) {
  :root {
    --card-w: calc(170 * var(--u-entries));
    --card-off: calc(145 * var(--u-entries));
    --scroll-gap: calc(5 * var(--u-entries));
    --entry-font: calc(10 * var(--u-entries));
    --entry-pad: calc(5 * var(--u-entries)) calc(7 * var(--u-entries));
    --meta-name-font: calc(10 * var(--u-entries));
    --meta-date-font: calc(8 * var(--u-entries));
  }
}

#entries-container {
  position: fixed;
  right: calc(-1 * var(--card-off));
  top: 0;
  bottom: 0;
  width: var(--card-w);
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
  transition: right 0.3s ease-out;
}
#entries-container.is-hovered { right: 0; }

@keyframes entries-scroll {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-1 * var(--scroll-dist))); }
}

#entries-scroll {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--scroll-gap);
  will-change: transform;
}

.entry {
  width: var(--card-w);
  flex-shrink: 0;
  padding: var(--entry-pad);
  background: #000;
  color: #fff;
  border: 1px solid #444;
  font-family: 'VT323', monospace;
  font-size: var(--entry-font);
  line-height: 1.0;
  text-align: left;
  pointer-events: auto;
  transition: border-color 0.25s, background 0.6s ease, color 0.6s ease;
}
.entry.new { background: #fff; color: #000; border-color: #fff; }
.entry.new .meta-name { color: #000; }
.entry.new .meta-date { color: #555; }
#entries-container.is-hovered .entry { border-color: #888; }
.entry .message { word-break: break-word; }
.entry .kao-nw { white-space: nowrap; } /* a kaomoji moves to the next line whole (guestbook.js) */
.meta-name { font-size: var(--meta-name-font); font-weight: bold; color: #fff; margin-top: calc(6 * var(--u-entries)); font-family: 'VT323', monospace; }
.meta-date { font-size: var(--meta-date-font); color: #777; margin-top: calc(2 * var(--u-entries)); font-family: 'VT323', monospace; }

/* ── VISITOR COUNTER */
#visitor-counter-wrap {
  position: fixed;
  left: 50%;
  bottom: calc(12 * var(--u-counter));
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  justify-content: center;
}
#y2k-counter { display: inline-flex; gap: 1px; }
#y2k-counter span {
  display: inline-block;
  width: calc(10.5 * var(--u-counter));
  height: calc(13.5 * var(--u-counter));
  line-height: calc(13.5 * var(--u-counter));
  text-align: center;
  background: #000;
  color: #fff;
  font-family: 'VT323', monospace;
  font-size: calc(9 * var(--u-counter));
  border: 1px solid #444;
}

@media (orientation: portrait) {
  #y2k-counter span {
    width: calc(10 * var(--u-counter));
    height: calc(13 * var(--u-counter));
    line-height: calc(13 * var(--u-counter));
    font-size: calc(9 * var(--u-counter));
  }
}

/* ── ABOUT PAGE */
:root {
  --stage-ease: cubic-bezier(.3, .7, .2, 1); /* the about text's entrance */

  --about-bg:   #c40000; /* the about page red: background, wipe AND cipher glyph colour */
  /* Film grain over the about background (about-bg.js, made in code). */
  --grain-amount: 0.02;  /* strength: 0 = none, 0.06 = subtle film, 0.15 = heavy */
  --grain-size:   1.3;   /* grain size in the screen's own pixels (1 = finest): finer on sharp phone screens */
  --grain-fps:    24;    /* how often the grain changes per second (film = 24) */
  --px-cell:    1px;    /* pixel size of the wipe, in design px (× --u, about-pixels.js) */
  --px-dur:     1s;    /* wipe duration, each way (before hitches) */
  --px-jitter:  0.05;    /* random per-pixel noise on the front — 0 = none */
  --px-dither:  .2;   /* ordered (Bayer) dither band on the front — 0 = none, higher = wider band */
  --px-veins:   4;     /* physarum scouts per click — more = more tendrils */
  --px-lag:     0;    /* 0 = smooth; higher = more slow zones + clock hitches */
}

#about-pixels {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 90; /* above the whole stage and #about, below the ?tune panel (100) */
  pointer-events: none;
  display: none;
}
#about-pixels.is-active { display: block; }

body.about-open .buttons-frame,
body.about-open #filter,
body.about-open #form-container,
body.about-open #entries-container,
body.about-open #visitor-counter-wrap {
  pointer-events: none;
}
body.about-shown .world,
body.about-shown .buttons-frame,
body.about-shown #filter,
body.about-shown #form-container,
body.about-shown #entries-container,
body.about-shown #visitor-counter-wrap {
  visibility: hidden;
}

/* ── The page itself */
/* The about background: red + film grain (about-bg.js), right behind #about. */
#about-bg {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 19;
  pointer-events: none;
  background: var(--about-bg);
  visibility: hidden;
}
body.about-shown #about-bg { visibility: visible; }

#about {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: transparent; /* the grain background (#about-bg) shows through */
  color: #fff;
  font-family: 'VT323', monospace;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* No transition of its own: it only ever switches while the pixel wipe
     covers the screen, so it snaps. */
  pointer-events: none;
  visibility: hidden;
}

body.about-shown #about {
  pointer-events: auto;
  visibility: visible;
}

/* ── TYPOGRAPHY TOKENS */
#about {
  --ab-weight:       400;

  /* Terminal effect. about-text.js reads these too, so one slider moves both. */
  --ab-char-w:       1ch;     /* fixed character cell — set to `auto` for a
                                 proportional, non-grid look */
  --ab-cursor-color: #000000;
  --ab-cipher-color: var(--about-bg); /* glyph — cut out of the box in the page colour */
  --ab-cipher-box:   #000000;         /* the inverted block behind each glyph */
  --ab-cipher-scale:     0.5; /* glyph size × text size (0.5 ≈ one cell wide) */
  --ab-cipher-box-scale: 1;   /* box height × text size */
  --ab-cipher-box-shift: 0;   /* nudge the box up (−) / down (+), × text size */

  --ab-cipher-size:  calc(var(--ab-cipher-scale) * 1em);
  --ab-cipher-box-h: calc(var(--ab-cipher-box-scale) * 1em);
  --ab-cipher-box-y: calc(var(--ab-cipher-box-shift) * 1em);
  --ab-cursor-blink: 0.5s;
  --ab-type-ms:      18;      /* ms per character (unitless — JS reads it) */
  --ab-line-pause:   260;     /* ms between lines */
  --ab-cipher-r:     110;     /* cursor influence radius, in board px (scales with the page) */
  --ab-cipher-hz:    26;      /* glyph reshuffles per second */
  --ab-cipher-on:    1;       /* 0 = no cipher near the cursor, anywhere */
  --ab-loader-color: #000000; /* the loading bar top-left */
  --ab-blob-wobble:  0.5;     /* how much the cipher blob's edge breathes; 0 = still */
  --ab-blob-trail:   0.5;     /* how far it lags, stretches and tails behind; 0 = glued */
}

/* ── TERMINAL TEXT */
.ab-w { white-space: nowrap; }

.ab-char {
  display: inline-block;
  /* inline-size, not width: in vertical text the cell's run is its height. */
  inline-size: calc(var(--ab-char-w) + var(--ab-track, 0em));
  letter-spacing: 0;
  text-align: center;
}
.ab-char.is-wide { inline-size: calc(var(--ab-char-w) * 2 + var(--ab-track, 0em)); }

.ab-el-text.is-vertical {
  writing-mode: vertical-rl;
  text-orientation: upright;
}
.ab-el-text.is-vertical .ab-char,
.ab-el-text.is-vertical .ab-char.is-wide {
  inline-size: calc(1em + var(--ab-track, 0em));
}

/* Not yet typed: holds its space so nothing reflows as the line fills in. */
.ab-char.is-pending { visibility: hidden; }

/* The write head — a character-sized red block that blinks as text appears. */
.ab-char.is-head {
  visibility: visible;
  color: transparent;
  background: var(--ab-cursor-color);
  animation: ab-blink var(--ab-cursor-blink) steps(1) infinite;
}

@keyframes ab-blink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0.15; }
}

.ab-char { position: relative; }
.ab-char.is-ciphered { color: transparent; }
.ab-char.is-ciphered::before {
  content: '';
  position: absolute;
  /* Logical sides, so the box also sits right in vertical text. */
  inset-inline: 0;
  inset-block-start: calc(50% - var(--ab-cipher-box-h) / 2 + var(--ab-cipher-box-y));
  block-size: var(--ab-cipher-box-h);
  background: var(--ab-cipher-box);
  pointer-events: none;
}
.ab-char.is-ciphered::after {
  content: attr(data-g);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ab-glyph-font, 'DotGothic16', monospace);
  font-size: var(--ab-cipher-size);
  line-height: 1;
  color: var(--ab-cipher-color);
  pointer-events: none;
}

/* Trailing caret parked after the last line once everything has typed. */
.ab-caret {
  display: inline-block;
  width: var(--ab-char-w);
  height: 1em;
  vertical-align: text-bottom;
  background: var(--ab-cursor-color);
  animation: ab-blink var(--ab-cursor-blink) steps(1) infinite;
}

/* No focus box on a tap (about.js only focuses it for keyboard users). */
#about-close:focus:not(:focus-visible) { outline: none; }
#about-close {
  position: fixed;
  top: clamp(16px, 3vh, 32px);
  right: clamp(16px, 3vw, 40px);
  z-index: 2;
  background: none;
  border: none;
  color: #000; /* always black, at full strength (no dimming, no hover change) */
  font-family: 'VT323', monospace;
  font-size: clamp(18px, 2.4vw, 26px);
  cursor: pointer;
}

#about-loader {
  /* Fallback spot, only until about-scene.js puts it on the board. */
  position: fixed;
  top: clamp(16px, 3vh, 32px);
  left: clamp(16px, 3vw, 40px);
  display: flex;
  align-items: center;
  color: var(--ab-loader-color, #000);
  font-size: 8px; /* = thickness (the layer's size) */
  pointer-events: none;
}
/* On the board it's a layer: position/size from the scene (inline styles). */
#about-loader.ab-el {
  position: absolute;
  top: auto;
  left: auto;
  z-index: 60;
}
.ab-ldr-bar {
  flex: 1;
  min-width: 0;
  height: 1em;
  box-sizing: border-box;
  border: max(1px, 0.12em) solid currentColor;
  padding: max(1px, 0.14em);
  overflow: hidden;
}
.ab-ldr-fill { height: 100%; width: 0; background: currentColor; transition: width 0.15s linear; }
/* 100%: hold a beat, then blink away (ends hidden). Leaving the page
   (.is-gone) hides it at once. The editor keeps it visible to place it. */
#about-loader.is-done { animation: ab-ldr-blink 0.9s steps(1) 0.35s forwards; }
#about-loader.is-gone { visibility: hidden; }
@keyframes ab-ldr-blink {
  0%   { opacity: 1; }
  15%  { opacity: 0; }
  30%  { opacity: 1; }
  45%  { opacity: 0; }
  60%  { opacity: 1; }
  75%, 100% { opacity: 0; visibility: hidden; }
}
#about.ab-editing #about-loader { animation: none; opacity: 1; visibility: visible; pointer-events: auto; cursor: move; }
/* Once about-scene.js has put it on the board, position and size come from
   the scene data (inline styles), in board units like every other element. */
#about-close.ab-el {
  position: absolute;
  top: auto;
  right: auto;
  z-index: 60; /* above every item.z in use, below the ?tune guides overlay's labels */
  padding: 0;
  text-align: left;
  white-space: nowrap;
  font-family: var(--ab-font, 'VT323', monospace); /* follow the master font */
}

/* ── Frame layers (about-scene.js / about-text.js) */
.ab-el-frame { box-sizing: border-box; }
.ab-el-frame .ab-fc { position: absolute; line-height: 1; white-space: nowrap; }
.ab-el-frame .ab-fc-tl { left: 0;  top: 0; }
.ab-el-frame .ab-fc-tr { right: 0; top: 0; }
.ab-el-frame .ab-fc-bl { left: 0;  bottom: 0; }
.ab-el-frame .ab-fc-br { right: 0; bottom: 0; }

/* ── Staggered entrance */
/* Delay is only applied in the shown state, so closing collapses uniformly
   instead of unwinding line by line in reverse. */
.about-line {
  opacity: 0;
  transition: opacity 0.55s ease;
  transition-delay: 0s;
}

.ab-el.about-line.is-in {
  opacity: 1;
  transform: none;
}

.ab-el[data-hover] { cursor: default; }
.ab-el[data-hover="shake"].is-hovering  { animation: ab-shake 0.35s steps(2) infinite; }
.ab-el[data-hover="glitch"].is-hovering { animation: ab-glitch 0.5s steps(1) infinite; }
.ab-el[data-hover="grow"]   { transition: scale 0.18s steps(3); }
.ab-el[data-hover="grow"].is-hovering   { scale: 1.08; }
.ab-el[data-hover="blink"].is-hovering  { animation: ab-blink-el 0.3s steps(1) infinite; }
.ab-el[data-hover="invert"].is-hovering {
  background: var(--ab-cipher-box, #000);
  color: var(--about-bg, #c40000) !important;
}
@keyframes ab-shake {
  0%   { translate: 2px 0; }
  25%  { translate: -2px 1px; }
  50%  { translate: 1px -2px; }
  75%  { translate: -1px 2px; }
  100% { translate: 0 0; }
}
/* Glitch hover: small jumps and flickers (was ±6–8px and down to 20% — toned down). */
@keyframes ab-glitch {
  0%   { translate: 0 0;      opacity: 1; }
  20%  { translate: -3px 0;   opacity: 0.75; }
  40%  { translate: 2px 0.5px; opacity: 1; }
  60%  { translate: 0 -1px;   opacity: 0.5; }
  80%  { translate: 4px 0;    opacity: 1; }
}
@keyframes ab-blink-el {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ab-el.is-hovering { animation: none !important; }
  .about-line { transition-duration: 0.01s; }
  .ab-char.is-head, .ab-caret { animation: none; }
}

/* ── ABOUT — ARTBOARD */
.ab-stage {
  position: relative;
  width: 100%;
}

.ab-board {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
}

.ab-el {
  position: absolute;
  /* --ab-font is the master font from the ?tune editor (about-fonts.js);
     a locked element sets its own font-family inline instead. */
  font-family: var(--ab-font, 'VT323', monospace);
  font-weight: var(--ab-weight, 400);
}

.ab-el-text {
  /* Honour Enter presses from the editor's text box on plain (non-terminal)
     text. Terminal text gets real <br>s from about-text.js instead. */
  white-space: pre-line;
  /* Not selectable: selecting drew highlight boxes over the page. */
}

.ab-el-img img {
  display: block;
  width: 100%;
  height: auto;
}
/* Hover media over an image layer (about-scene.js): the video, then the
   hover image on top — both fill the layer, both hidden until hover. */
.ab-el-img .ab-hv,
.ab-el-img img.ab-hg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
}
.ab-el-img .ab-hv { z-index: 1; }
.ab-el-img img.ab-hg { z-index: 2; }
.ab-el-img.is-hovering .ab-hv,
.ab-el-img.is-hovering img.ab-hg { opacity: 1; }
