/* The vault login scene — design frozen in docs/vault-ui.md.
   Pixel-art rules that must not break: everything moves in steps(), one scale
   factor (--s) for the whole scene, image-rendering: pixelated everywhere, no
   CSS gradients (gradients are Bayer-dithered in the art instead). */

:root {
  --s: 3.4;
  --led: #7dff9e;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: #0a0a0c;
  overflow: hidden;
}

body {
  display: grid;
  place-items: center;
  font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: #6d7280;
  letter-spacing: .08em;
}

img { image-rendering: pixelated; display: block; }

/* --- wall ------------------------------------------------------------- */

.wall {
  position: fixed;
  inset: 0;
  z-index: 0;
  background-image: url("/auth-static/vault/wall_tile.png");
  background-repeat: repeat;
  background-size: calc(72px * var(--s)) calc(82px * var(--s));
  image-rendering: pixelated;
}

/* darken and vignette so the wall never competes with the door */
.wall::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 48%,
    rgba(0, 0, 0, .30) 0%, rgba(0, 0, 0, .72) 62%, rgba(0, 0, 0, .92) 100%);
}

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

.scene {
  position: relative;
  z-index: 1;
  width: calc(300px * var(--s));
  height: calc(200px * var(--s));
  /* nudged so the doorway lands on the viewport centre — the iris then
     grows from the middle of the screen and the hole at the same time */
  transform: translateX(calc(46px * var(--s)));
  /* The scene is scenery: clicks fall through it to .real-input beneath, and
     only the keypad buttons opt back in. This must be done with pointer-events
     rather than z-index — `transform` above makes .scene a stacking context, so
     a child's z-index can never outrank .real-input outside it, whatever the
     number says. Raising z-index here will silently make the keypad dead. */
  pointer-events: none;
}

/* --- door ------------------------------------------------------------- */

.doorway {
  position: absolute;
  left: calc(8px * var(--s));
  top: calc(4px * var(--s));
  width: calc(192px * var(--s));
  height: calc(192px * var(--s));
}

.doorway img { width: 100%; height: 100%; }

.void { position: absolute; inset: 0; }

.door {
  position: absolute;
  inset: 0;
  transition: transform 1s steps(10);
}

/* opens away from the keypad — swinging over it would put a wall-mounted
   panel behind a moving door, which reads as broken */
.open .door { transform: translateX(-86%) rotate(-3deg); }

/* the spinning wheel is only shown in motion, where resampling artefacts are
   invisible; at rest the untouched generated sprite is on screen */
.spin {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(112px * var(--s));
  height: calc(112px * var(--s));
  margin: calc(-56px * var(--s)) 0 0 calc(-56px * var(--s));
  background: url("/auth-static/vault/wheel_spin.png") no-repeat;
  background-size: calc(112px * var(--s) * 24) calc(112px * var(--s));
  /* Required here, not inherited: the `img` rule above only matches <img>
     elements, and this is a div with a background-image. Without it the sheet
     is bilinear-scaled from 112px to ~380px and the spinning wheel is a blur
     while the door behind it stays crisp. */
  image-rendering: pixelated;
  opacity: 0;
}

.spinning .spin { opacity: 1; }

/* --- keypad, mounted on the wall beside the door at reach height ------- */

.keypad {
  position: absolute;
  left: calc(196px * var(--s));
  top: calc(52px * var(--s));
  width: calc(80px * var(--s));
  height: calc(112px * var(--s));
  z-index: 2;                /* bolted to the wall, so always in front */
}

.keypad > img { width: 100%; height: 100%; }

.lcd {
  position: absolute;
  left: calc(26px * var(--s));
  top: calc(30px * var(--s));
  width: calc(27px * var(--s));
  height: calc(13px * var(--s));
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(1px * var(--s));
  overflow: hidden;
}

.cell {
  width: calc(3px * var(--s));
  height: calc(6px * var(--s));
  background: var(--led);
  box-shadow: 0 0 calc(2px * var(--s)) var(--led);
}

.cell.off { background: #1d2027; box-shadow: none; }

.lcdtext {
  font: 600 calc(5px * var(--s))/1 ui-monospace, Menlo, monospace;
  color: var(--led);
  text-shadow: 0 0 calc(2px * var(--s)) var(--led);
  letter-spacing: calc(.6px * var(--s));
  white-space: nowrap;
}

.statled {
  position: absolute;
  left: calc(37px * var(--s));
  top: calc(22px * var(--s));
  width: calc(4px * var(--s));
  height: calc(3px * var(--s));
  background: var(--led);
  box-shadow: 0 0 calc(3px * var(--s)) var(--led);
}

.keys {
  position: absolute;
  /* Measured off the mirrored sprite — see tools/vault/build_scene.py. */
  left: calc(23.5px * var(--s));
  top: calc(51.5px * var(--s));
  display: grid;
  grid-template-columns: repeat(3, calc(12px * var(--s)));
  grid-auto-rows: calc(12px * var(--s));
}

.keys button {
  all: unset;
  /* Must come after `all: unset` — pointer-events is inherited, so `unset`
     resolves it to the scene's `none` and the button stays untappable. */
  pointer-events: auto;
  cursor: pointer;
  width: 100%;
  height: 100%;
}

.keys button:active,
.keys button.dn {
  background: rgba(255, 255, 255, .16);
  box-shadow: inset 0 0 0 calc(1px * var(--s)) rgba(255, 255, 255, .28);
}

/* --- real password input, invisible but functional --------------------- */
/* Covers the viewport so tapping the art focuses it for typing. The scene is
   pointer-events:none and only the keypad buttons opt back in, so poking a key
   never focuses this and never summons the OS keyboard over the art on mobile,
   while poking anywhere else does. */

.real-input {
  position: fixed;
  inset: 0;
  /* Beneath the scene (z-index 1), above the wall (z-index 0, earlier in DOM).
     It has to be *below* the scene, not above: pointer-events:none lets clicks
     fall through the art to reach this, but a keypad button can only win a hit
     test if it actually paints above this element. */
  z-index: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: 0;
  background: transparent;
  font-size: 16px;   /* >=16px so iOS doesn't zoom on focus */
  caret-color: transparent;
}

.real-input:focus-visible { outline: none; }

/* --- error state (server-rendered, no-JS path) ------------------------- */

.error {
  position: fixed;
  bottom: 44px;
  left: 0;
  right: 0;
  z-index: 3;
  margin: 0;
  text-align: center;
  font-size: 11px;
  color: #ff5f6d;
  pointer-events: none;   /* text, not a target — let taps reach the input */
}

.fallback { position: fixed; bottom: 20px; right: 20px; z-index: 3; }

/* --- misc ------------------------------------------------------------- */

.hint {
  position: fixed;
  bottom: 16px;
  left: 0;
  right: 0;
  z-index: 2;
  margin: 0;
  text-align: center;
  font-size: 11px;
  color: #41464f;
  pointer-events: none;   /* text, not a target — let taps reach the input */
}

/* --- refusal jolt ------------------------------------------------------ */
/* The thump of a heavy mechanism slamming to a stop — deliberately tiny, so it
   reads as impact rather than as a form-validation wobble.

   Animates `translate`, NOT `transform`. .scene carries a transform for its
   centring offset; keyframing `transform` here would replace it and throw the
   whole scene ~156px sideways mid-shake. `translate` is a separate property and
   composes with it. */
/* Step count and keyframe positions have to agree. steps(n) samples progress at
   0, 1/n, 2/n … — with steps(2) that is only 0 and 0.5, which against a 25%/75%
   keyframe pair both evaluate to zero offset, so the shake does nothing at all.
   steps(4) lands on each of the four positions below. */
.shake { animation: shk .09s steps(4) 2; }

@keyframes shk {
  0%   { translate: 0 0; }
  25%  { translate: -4px 0; }
  50%  { translate: 0 0; }
  75%  { translate: 4px 0; }
  100% { translate: 0 0; }
}

/* --- lockout blackout -------------------------------------------------- */
/* Everything goes dark and only the countdown remains. Above every other
   layer, and it takes pointer events so the scene beneath cannot be poked. */

/* Same wall as the login scene, with the lights out — continuity of place,
   so it reads as "you are still at the door" rather than a browser error. */
.lockout {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background-color: #050506;
  background-image: url("/auth-static/vault/wall_tile.png");
  background-repeat: repeat;
  background-size: calc(72px * var(--s)) calc(82px * var(--s));
  image-rendering: pixelated;
}

.lockout[hidden] { display: none; }

/* Darkens the wall well below the login page's level and vignettes hard, so
   the timer is the only thing with any light on it. */
.lockout::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 50%,
    rgba(0, 0, 0, .52) 0%, rgba(0, 0, 0, .78) 58%, rgba(0, 0, 0, .93) 100%);
}

/* --- death flash ---
   The decelerating strobe from 8-bit death animations: fast at first, settling.
   It flashes *dark*, not bright — the darkness cuts in and out over the scene,
   each beat longer, until it stays. `.clear` drops the overlay so the scene
   shows through between beats; JS drives the timing.

   Flashing dark rather than white also keeps the luminance delta small, which
   is what the photosensitivity guidance is actually about. Reduced motion still
   skips the sequence entirely. */
.lockout.clear { opacity: 0; }

/* The flash frame is a deeper cut than the screen it settles into — the same
   wall with the light pulled further down. Without this the strobe alternates
   against the resting state itself and the last beat has nothing to land on;
   with it the sequence resolves upward into the dimmer, readable screen. */
.lockout.deep::before {
  background: radial-gradient(ellipse at 50% 50%,
    rgba(0, 0, 0, .88) 0%, rgba(0, 0, 0, .95) 55%, rgba(0, 0, 0, .99) 100%);
}

.timer {
  position: relative;   /* above ::before */
  display: flex;
  gap: calc(1px * var(--ts, 6));
  /* no-JS fallback styling; JS swaps the text for pixel glyphs */
  font: 600 calc(12px * var(--ts, 6))/1 ui-monospace, Menlo, monospace;
  color: #ff5f6d;
  letter-spacing: .1em;
}

.timer[hidden] { display: none; }

.tglyph {
  width: calc(12px * var(--ts, 6));
  height: calc(16px * var(--ts, 6));
  background-image: url("/auth-static/vault/digits.png");
  background-size: calc(132px * var(--ts, 6)) calc(16px * var(--ts, 6));
  background-repeat: no-repeat;
  image-rendering: pixelated;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- mobile portrait: door with the keypad below it ------------------- */
/* On narrow portrait screens the side-by-side composition clips the keypad
   off the right edge (it sits 172 units right of centre; the viewport isn't
   wide enough). Reflow instead of shrink: door stays centred and full-size,
   the keypad drops underneath it, and the composition anchors to the bottom
   of the screen — where a real keypad would be at reach height.

   The scene coordinate system is unchanged; only its box and the keypad's
   position are overridden. Door centre stays at 110 units (14 + 96) = the
   scene's own centre, so no translateX offset is needed. */
@media (max-width: 700px) and (orientation: portrait) {
  body {
    /* place-items = align-items justify-items — end for the bottom anchor,
       center for horizontal centring. Get the order wrong and the scene
       slides to the right edge instead of staying centred. */
    place-items: end center;
    padding-bottom: max(24px, 6vh);
  }

  .scene {
    width: calc(220px * var(--s));
    height: calc(320px * var(--s));
    transform: none;          /* door is already centred in the box */
  }

  .doorway {
    left: calc(14px * var(--s));   /* centre the door in the narrower box */
    top: calc(4px * var(--s));
  }

  .keypad {
    left: calc(70px * var(--s));   /* centred under the door */
    top: calc(204px * var(--s));   /* clears the door (bottom at 196) */
  }
}

/* The reduced-motion rule is deliberately minimal here: the JS path skips
   straight to the end state instead of relying on these transition kills. */
@media (prefers-reduced-motion: reduce) {
  .door, .spin, .shake { transition: none; animation: none; }
}
