/* tragiskt — see DESIGN.md
   Black, white, and the film. No third colour anywhere in this file. */

@font-face {
  font-family: 'Squealer';
  src: url('../assets/Squealer.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: block; /* the gate has one word in it; a fallback face would be a different design */
}

:root {
  --ink: #ffffff;
  --canvas: #000000;
  --hair: rgba(255, 255, 255, 0.34);
  --hair-live: #ffffff;

  /* The one number the client specified for the film. */
  --scrim: 0.30;

  --micro: ui-sans-serif, 'Inter', 'Helvetica Neue', Arial, sans-serif;

  --chamfer: 14px;      /* echoes Squealer's 45-degree cuts */
  --gate-fade: 900ms;

  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out-quart: cubic-bezier(0.76, 0, 0.24, 1);
}

* { box-sizing: border-box; }

/* An author `display` beats the UA rule for [hidden], and every layer on the
   stage sets one. Without this the no-WebGL wordmark paints over the real one. */
[hidden] { display: none !important; }

html {
  color-scheme: dark;
}

html, body {
  margin: 0;
  height: 100%;
  background: var(--canvas);
  color: var(--ink);
  font-family: 'Squealer', 'Trebuchet MS', system-ui, sans-serif;
  overscroll-behavior: none;
}

body {
  overflow: hidden;
  /* dvh tracks the iOS URL bar; the fallback keeps old engines full-height */
  height: 100vh;
  height: 100dvh;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── stage ────────────────────────────────────────────────────────────── */

.stage {
  position: fixed;
  inset: 0;
  background: var(--canvas);
}

.stage__video,
.stage__gl,
.stage__scrim,
.stage__flat,
.stage__grain,
.stage__vignette {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.stage__video {
  object-fit: cover;      /* 1080x1080 source, so a 16:9 viewport crops the centre band */
  object-position: center;
  pointer-events: none;
  /* the film only exists once it is running; before that the stage is black */
  opacity: 0;
  will-change: opacity, transform;
}

.stage__scrim {
  background: #000;
  opacity: 0;             /* raised to var(--scrim) when the film starts */
  pointer-events: none;
}

.stage__gl {
  display: block;
  pointer-events: none;   /* the window listens for pointer moves, not the canvas */
}

/* No-WebGL wordmark. Same face, same weight, flat. */
.stage__flat {
  display: grid;
  place-items: center;
  font-size: clamp(3rem, 15vw, 15rem);
  letter-spacing: -0.01em;
  line-height: 1;
  color: var(--ink);
  text-shadow: 0 0 90px rgba(0, 0, 0, 0.9), 0 0 30px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}

/* Quiet texture. It exists to break up the banding in the vignette and to
   hide the softness of a 1080px film blown up to a 1920px viewport. */
.stage__grain {
  pointer-events: none;
  opacity: 0.055;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* Sized `farthest-corner`, which is the only sizing that guarantees the last
   stop lands exactly on the corner of the frame.
   This was written as `112% 108%`, and percentage radii are measured against
   the box, not against the distance to its corner — so on a 2048x947 viewport
   the gradient's radii came out 2294 x 1023px and the screen corner sat at 64%
   along it. Every stop past 68% was outside the visible frame: the vignette was
   in the markup, cost a composited layer, and did essentially nothing. */
.stage__vignette {
  pointer-events: none;
  background:
    radial-gradient(ellipse farthest-corner at 50% 50%,
      rgba(0,0,0,0)    0%,
      rgba(0,0,0,0)   30%,
      rgba(0,0,0,.07) 46%,
      rgba(0,0,0,.20) 62%,
      rgba(0,0,0,.40) 76%,
      rgba(0,0,0,.62) 88%,
      rgba(0,0,0,.80) 100%);
}

/* ── the gate ─────────────────────────────────────────────────────────── */

.gate {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  z-index: 10;
}

.gate__panel {
  position: absolute;
  left: 0;
  width: 100%;
  height: 50.5%;      /* a hair of overlap, so no seam shows at the join */
  background: var(--canvas);
  will-change: transform;
}

.gate__panel--top    { top: 0; }
.gate__panel--bottom { bottom: 0; }

.gate__inner {
  display: grid;
  justify-items: center;
  gap: 38px;
  transform: translateY(-3vh);
}

/* Squealer is a display face; below about 16px its chamfers close up and it
   stops being readable. The two pieces of micro-copy on the site are set in a
   plain grotesque instead — a clear second voice, used only for machine talk. */
.gate__note {
  margin: 0;
  font-family: var(--micro);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: rgba(255, 255, 255, 0.26);
  transition: color 300ms linear;
}

/* The eye.
 *
 * This is the one curve on the site. The rest of the page is built from
 * Squealer's 45-degree cuts and says so loudly, but an eye drawn on that grid
 * reads as a logo, not as something looking back — so the rule is broken here
 * on purpose, once, and the exception is documented in DESIGN.md.
 *
 * The unsettling part is not the drawing, it is the timing: the blink is
 * scheduled from main.js at irregular intervals, because anything periodic
 * reads as a loading spinner no matter how it is drawn. */
.eye {
  position: relative;
  display: block;
  width: clamp(140px, 22vw, 232px);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.eye__svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Not white. A lit sclera on a black page looks like a cartoon; this is the
   film's own near-black lifted just far enough to separate from the canvas. */
.eye__white { fill: #0d0d0f; }

.eye__iris {
  fill: rgba(255, 255, 255, 0.07);
  stroke: rgba(255, 255, 255, 0.34);
  stroke-width: 1.1;
  transition: stroke 420ms var(--ease-out-expo), fill 420ms var(--ease-out-expo);
}

.eye__pupil { fill: var(--canvas); }

/* The wet highlight. Four pixels of it are the difference between an icon and
   something with fluid in it. */
.eye__glint { fill: rgba(255, 255, 255, 0.82); }

.eye__rim {
  fill: none;
  stroke: var(--hair);
  stroke-width: 1.4;
  animation: breathe-stroke 4200ms var(--ease-in-out-quart) infinite;
}

/* One slow breath on the rim, so the eye is alive even between blinks and a
   black screen never reads as a failed load. */
@keyframes breathe-stroke {
  0%, 100% { stroke: rgba(255, 255, 255, 0.20); }
  50%      { stroke: rgba(255, 255, 255, 0.46); }
}

/* The transform goes on the <g>, never on the <rect> inside it: a CSS
   `transform` on an SVG <rect> computes to the identity matrix in Chrome and
   fails silently, which reads as an eye that simply never blinks. */
.eye__lid rect { fill: var(--canvas); }

.eye__lid { transform: translateY(0); }

/* 170ms, and asymmetric: shut fast, open slower. A blink that takes as long to
   open as it does to close reads as a wink. */
.eye.is-blinking .eye__lid--top    { animation: lid-top    170ms var(--ease-in-out-quart); }
.eye.is-blinking .eye__lid--bottom { animation: lid-bottom 170ms var(--ease-in-out-quart); }

@keyframes lid-top {
  0%   { transform: translateY(0); }
  38%  { transform: translateY(68px); }
  100% { transform: translateY(0); }
}

@keyframes lid-bottom {
  0%   { transform: translateY(0); }
  38%  { transform: translateY(-32px); }
  100% { transform: translateY(0); }
}

.eye:hover .eye__rim,
.eye:focus-visible .eye__rim {
  stroke: var(--hair-live);
  animation: none;
}

.eye:hover .eye__iris,
.eye:focus-visible .eye__iris {
  fill: rgba(255, 255, 255, 0.13);
  stroke: rgba(255, 255, 255, 0.6);
}

.eye:hover ~ .gate__note { color: rgba(255, 255, 255, 0.5); }

.eye:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 14px;
}

.eye:active .eye__pupil { fill: #000; }

/* A blinking eye is small, but it is still the only thing moving on the page,
   and it is exactly the kind of thing this query exists to stop. It holds
   open instead; main.js never schedules a blink in the first place. */
@media (prefers-reduced-motion: reduce) {
  .eye__rim { animation: none; stroke: var(--hair); }
  .eye.is-blinking .eye__lid--top,
  .eye.is-blinking .eye__lid--bottom { animation: none; }
}

/* The gate does not fade out. main.js drives --gate-cut from 0 to 1 over one
   beat and the two panels part from the centre, so the first thing the visitor
   sees is the word, not the corners of the film. */
.gate.is-cutting { pointer-events: none; }

.gate.is-cutting .gate__panel--top    { transform: translateY(calc(var(--gate-cut, 0) * -100%)); }
.gate.is-cutting .gate__panel--bottom { transform: translateY(calc(var(--gate-cut, 0) * 100%)); }

.gate.is-cutting .gate__inner {
  opacity: calc(1 - var(--gate-cut, 0) * 4.5);
  transform: translateY(-3vh) scale(calc(1 - var(--gate-cut, 0) * 0.05));
}

.gate.is-gone { display: none; }

/* ── the one piece of chrome ──────────────────────────────────────────── */

/* WCAG 2.2.2: motion and sound start on their own and run past five seconds,
   so there has to be a way to stop them. */
.halt {
  position: fixed;
  right: max(20px, env(safe-area-inset-right));
  bottom: max(18px, env(safe-area-inset-bottom));
  z-index: 12;
  padding: 9px 15px 8px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.4);
  color: rgba(255, 255, 255, 0.45);
  font-family: var(--micro);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  cursor: pointer;
  touch-action: manipulation;
  backdrop-filter: blur(6px);
  transition: color 260ms linear, border-color 260ms linear, opacity 700ms linear;
  -webkit-tap-highlight-color: transparent;
}

.halt:hover,
.halt:focus-visible {
  color: var(--ink);
  border-color: rgba(255, 255, 255, 0.5);
}

.halt:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 4px;
}

/* ── fallbacks ────────────────────────────────────────────────────────── */

.fallback {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: clamp(3rem, 15vw, 15rem);
  line-height: 1;
}

@media (prefers-reduced-motion: reduce) {
  .cut, .cut__edge, .cut__face, .gate__note { transition-duration: 1ms; }
  .cut__edge { animation: none; background: rgba(255, 255, 255, 0.42); }
}

@media (max-width: 720px) {
  .cut { font-size: 24px; padding: 24px 52px 21px; }
  .gate__inner { gap: 30px; }
}

@media (max-width: 420px) {
  .cut { font-size: 20px; padding: 21px 40px 18px; letter-spacing: 0.2em; }
  .cut__label { margin-right: -0.2em; }
}
