/* The vault door on the sign-in page.
 *
 * Pure CSS, no images and no canvas, because this is the first paint of the site
 * and the offline desktop app has to draw it too.
 *
 * THE THREE THINGS THAT MAKE IT READ AS SOLID, all learned by getting them wrong:
 *
 * 1. **Depth is real Z, not shading.** Every plate, bolt, spoke and dial sits at
 *    its own `translateZ` inside a `preserve-3d` door, and the frame carries a
 *    permanent 9deg tilt. The tilt turns those Z offsets into visible parallax —
 *    the inner plates sit measurably off-centre from the outer ones — which is a
 *    cue no amount of radial-gradient shading can fake. The first version was
 *    concentric gradients on one flat disc and looked exactly like that.
 * 2. **No `filter` anywhere above a 3D child.** `filter` creates a containing
 *    block and FLATTENS `preserve-3d` beneath it, so the `filter: brightness()`
 *    that used to dim the door on opening was silently collapsing the entire
 *    depth stack into one plane. Dimming is now an overlay layer (`.vault-shade`)
 *    at the top of the stack. Same rule applies to `backdrop-filter`, which is
 *    why the scene's blur lives on its own sibling behind the stage.
 * 3. **The door has thickness.** A dense stack of discs behind the face, each
 *    darker than the last, extrudes a 54px rim on a 246px door. Under the tilt you
 *    see the sliver on one side; swung open you see the slab end-on, which is the
 *    view that decides whether this reads as a vault door or as a lid. The slab
 *    can only grow FORWARD — see `.vault-door` for the socket clearance that pins
 *    its back face — and its depth lives in `--slab-depth`, once.
 *
 * Everything is driven by one class per step on the scene, so the timing lives
 * here with the geometry and the JS stays a state machine.
 *
 * GEOMETRY, once, because errors here are invisible in the CSS and obvious on
 * screen. Let W be the frame's width. Radii are fractions of W: the door spans
 * inset 4% (radius 0.46W), bolts seat at 0.42W and withdraw to 0.33W, sockets sit
 * at 0.42W under the closed door, rivets at 0.365W.
 * `translateY` is a percentage of the element's OWN height, so placing an element
 * of height h·W at radius r·W needs translateY = -(r/h)·100%. An early version
 * divided by a fraction of the frame's half-width while the heights resolved
 * against its padding box — off by 1.82x, which put the sockets outside the door
 * as eight black notches around a *closed* vault. The frame has no padding now,
 * so there is exactly one basis.
 *   The visible sliders sit on the DOOR (0.92W). Their centres retract from
 * 0.37 to 0.30 of that box. Pins sit 0.06 of that box inward of each centre, at 0.31 then
 * 0.24: the cam's track endpoints. All share a 1600ms cubic-bezier and the cam
 * turns 30deg, so the pins stay in their Archimedean tracks through the drive.
 *   All four rings on the ring — sockets, bolts, slots, lugs — sit at
 * `i * 45 + 22.5`, and they moved together. At a plain `i * 45` there is a bolt
 * at 90deg, which is precisely where the hinge is: harmless while the bolts were
 * `opacity: 0`, a direct collision the moment heads appear on the face in front
 * of the hinge assembly. Offsetting two of the four rings and not the others is
 * worse than the collision it fixes, because the swing would then reveal bolts
 * that miss their own sockets. The 16 rivets are at odd multiples of 11.25deg,
 * so the offset heads still land exactly between each flanking pair.
 */

/* Vault tokens, from the site's own stone ramp and blue accent rather than a
   palette of its own. Both themes, because the sign-in page has both and a
   near-black vault dropped onto the light theme is a different website for two
   seconds. Defined on bare :root first so no value's only definition lives inside
   a [data-theme] block. */
:root {
  --vault-size: clamp(250px, 70vmin, 400px);
  --vault-lit: #ffffff;         /* the specular */
  --vault-hi: #e7e5e4;          /* upper-left face */
  --vault-mid: #d6d3d1;         /* body */
  --vault-low: #a8a29e;         /* lower-right face */
  --vault-deep: #6f6763;        /* rim, edge stack */
  --vault-shadow: #44403c;      /* deepest edge */
  --vault-jamb: #a8a29e;
  --vault-cavity: #44403c;      /* inside the open door */
  --vault-socket: #57534e;
  --vault-ground: #e7e5e4;      /* the scene behind everything */
  --vault-ground-deep: #d6d3d1;
  --vault-edge-lit: rgba(255, 255, 255, 0.9);
  --vault-edge-dark: rgba(28, 25, 23, 0.42);
  --vault-grid: rgba(68, 64, 60, 0.1);
  --vault-aura: rgba(37, 99, 235, 0.2);
}

[data-theme="dark"] {
  --vault-lit: #e7e5e4;
  --vault-hi: #8a8280;
  --vault-mid: #44403c;
  --vault-low: #292524;
  --vault-deep: #1c1917;
  --vault-shadow: #0c0a09;
  --vault-jamb: #292524;
  --vault-cavity: #0c0a09;
  --vault-socket: #0c0a09;
  --vault-ground: #0c0a09;
  --vault-ground-deep: #000000;
  --vault-edge-lit: rgba(214, 211, 209, 0.5);
  --vault-edge-dark: rgba(0, 0, 0, 0.72);
  --vault-grid: rgba(214, 211, 209, 0.075);
  --vault-aura: rgba(96, 165, 250, 0.28);
}

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

.vault-scene {
  --vault-drive-duration: 1600ms;
  --vault-drive-ease: cubic-bezier(0.45, 0, 0.2, 1);
  position: fixed;
  inset: 0;
  z-index: 400;
  display: grid;
  place-items: center;
  /* 1400px, and the origin above centre: a shallower perspective exaggerates the
     tilt into a distortion, a deeper one flattens the parallax back out. */
  perspective: 1400px;
  perspective-origin: 50% 42%;
}

/* The ground, and the focus pull. Its own element because `backdrop-filter` — like
   `filter` — flattens preserve-3d beneath it, and blurring the sign-in card away
   must not cost the door its depth. */
.vault-backdrop {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 40%, var(--vault-ground) 0%, var(--vault-ground-deep) 74%);
  animation: vault-ground 700ms var(--ease-out) both;
}

/* A quiet architectural chamber around the mechanism. The first version ended at
   a radial gradient, which made the detailed door feel like an isolated product
   render. These two planes give it a room: vertical wall ribs above and a floor
   grid falling away below. They stay very low contrast so the door remains the
   only control that asks to be pressed. */
.vault-backdrop::before {
  content: "";
  position: absolute;
  inset: -18% -12%;
  background:
    repeating-linear-gradient(90deg,
      transparent 0 7.8%, var(--vault-grid) 7.9% 8%, transparent 8.1% 15.8%),
    repeating-linear-gradient(0deg,
      transparent 0 12.2%, var(--vault-grid) 12.3% 12.45%, transparent 12.6% 24.8%);
  -webkit-mask: radial-gradient(ellipse at 50% 44%, transparent 0 20%, #000 46%, transparent 78%);
  mask: radial-gradient(ellipse at 50% 44%, transparent 0 20%, #000 46%, transparent 78%);
  opacity: 0.72;
  transform: perspective(700px) rotateX(62deg) translateY(48%);
  transform-origin: 50% 68%;
}

/* Light shafts and the edge vignette share a paint layer. Multiple gradients are
   cheaper and more stable than blurring extra DOM nodes, and—critically—do not
   flatten the 3D scene the way a filter on an ancestor would. */
.vault-backdrop::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(112deg, transparent 0 22%, var(--vault-aura) 36%, transparent 51%),
    linear-gradient(248deg, transparent 0 24%, var(--vault-aura) 38%, transparent 53%),
    radial-gradient(ellipse at 50% 45%, transparent 0 34%, rgba(0, 0, 0, 0.18) 72%, rgba(0, 0, 0, 0.42) 100%);
  opacity: 0.5;
  animation: vault-atmosphere 9s ease-in-out infinite alternate;
}

@keyframes vault-ground {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes vault-atmosphere {
  from { opacity: 0.38; transform: scale(1); }
  to { opacity: 0.62; transform: scale(1.035); }
}

/* The stage carries the arrival. The door starts small and far away and settles
   forward — the abrupt version simply cross-faded a full-size door into place,
   which reads as a cut rather than as something coming to meet you. */
.vault-stage {
  position: relative;
  width: clamp(240px, 74vmin, 400px);
  aspect-ratio: 1;
  transform-style: preserve-3d;
  /* An animation, not a class-gated transition. The door is on screen from the
     first paint now, and a transition needs its start state painted before the
     class flips — which is the "appears abruptly, then jumps" this replaced. */
  animation: vault-arrive 820ms var(--ease-panel) both;
}

/* Two instrument rings sit behind the physical frame. Their opposite rotations
   add scale and anticipation without moving the control itself. A mask keeps each
   one a true ring rather than a translucent disc over the door. */
.vault-stage::before,
.vault-stage::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  transform: translateZ(-150px);
}

.vault-stage::before {
  inset: -18%;
  background: repeating-conic-gradient(from 4deg,
    var(--vault-grid) 0deg 0.8deg, transparent 0.8deg 7.5deg);
  -webkit-mask: radial-gradient(circle, transparent 0 68%, #000 68.3% 69.2%, transparent 69.5%);
  mask: radial-gradient(circle, transparent 0 68%, #000 68.3% 69.2%, transparent 69.5%);
  opacity: 0.74;
  animation: vault-orbit 48s linear infinite;
}

.vault-stage::after {
  inset: -8%;
  background: conic-gradient(from 24deg,
    transparent 0 20%, var(--vault-aura) 24% 25%, transparent 29% 70%,
    var(--vault-aura) 74% 75%, transparent 79%);
  -webkit-mask: radial-gradient(circle, transparent 0 71%, #000 72% 73%, transparent 74%);
  mask: radial-gradient(circle, transparent 0 71%, #000 72% 73%, transparent 74%);
  opacity: 0.68;
  animation: vault-orbit-reverse 32s linear infinite;
}

@keyframes vault-orbit { to { rotate: 1turn; } }
@keyframes vault-orbit-reverse { to { rotate: -1turn; } }

@keyframes vault-arrive {
  from { transform: translateZ(-340px) scale(0.84) rotateY(6deg); opacity: 0; }
  to { transform: translateZ(0) scale(1) rotateY(0deg); opacity: 1; }
}

/* The gimbal carries only environmental parallax. The frame below still owns the
   permanent 3D tilt, and the door still owns the hinge turn. */
.vault-rig {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: rotateX(var(--look-y, 0deg)) rotateY(var(--look-x, 0deg));
  transition: transform 420ms cubic-bezier(0.2, 0.75, 0.25, 1);
}

/* The lift is held for the whole sequence, not just the first beat. It used to
   name only the two early classes, so it silently dropped out the instant the
   next class was added and the rig sank 10px mid-mechanism. Every downstream
   state class is listed — and this selector is redefined in the flat-composition
   block at the bottom of the file, so it has to be extended in BOTH places. */
.vault-scene.is-reading .vault-rig,
.vault-scene.is-spinning .vault-rig,
.vault-scene.is-released .vault-rig,
.vault-scene.is-unbolting .vault-rig,
.vault-scene.is-clear .vault-rig,
.vault-scene.is-swinging .vault-rig,
.vault-scene.is-open .vault-rig,
.vault-scene.is-entering .vault-rig {
  transform: rotateX(var(--look-y, 0deg)) rotateY(var(--look-x, 0deg)) translateZ(10px) scale(1.012);
}

.vault-scene.is-reading .vault-stage::after,
.vault-scene.is-spinning .vault-stage::after {
  opacity: 1;
  animation-duration: 2.2s;
}

/* The camera move stays still until the leaf swings, then continues through the
   opening. The final class wins when skip adds every state together. */
.vault-scene.is-swinging .vault-stage,
.vault-scene.is-open .vault-stage { animation: vault-camera-push 1350ms cubic-bezier(0.16, 0.72, 0.18, 1) both; }
.vault-scene.is-entering .vault-stage { animation: vault-dolly 700ms cubic-bezier(0.45, 0, 0.85, 0.4) both; }

/* An anticipatory lean, not a push. The real move is the dolly below, and two
   big pushes back to back read as a zoom rather than as a camera going somewhere.
   Its end state is restated as the dolly's `from`, so there is no seam. */
@keyframes vault-camera-push {
  from { transform: translateZ(0) scale(1); }
  to { transform: translateZ(110px) scale(1.02); }
}

/* Through the aperture. Capped at 560px, and that cap is arithmetic, not taste:
   at rotateY(106deg) about `right center` the door's leading edge sits
   0.92W·sin(106deg) = 0.884W forward of its own origin plus its own translateZ,
   which is 452px at the `--vault-size` clamp maximum. Against `perspective:
   1400px` that leaves 388px of margin at every viewport size. 1080px would put
   the edge at 1532px — past the camera plane, where the projection blows up —
   and even at the clamp MINIMUM it lands at 1399px, i.e. a divide by zero.
   The curve ACCELERATES and must: a camera speeding up into an opening is how
   'going through' reads, and a decelerating one reads as arriving at a wall. */
@keyframes vault-dolly {
  from { transform: translateZ(110px) scale(1.02); }
  to { transform: translateZ(560px) scale(1.5); }
}

/* --------------------------------------------------------------- the frame */

.vault-frame {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  transform-style: preserve-3d;
  /* Off-axis, permanently. This is what converts every translateZ below into
     parallax you can see; face-on, the whole stack collapses to concentric rings. */
  transform: rotateY(-11deg) rotateX(4deg);
  box-shadow: 0 48px 90px rgba(0, 0, 0, 0.45);
}

/* The jamb must be an actual ring. A solid background on `.vault-frame` sits in
   front of its negative-Z cavity and silently covers the thing the opening is
   meant to reveal. Isolating the paint on this masked pseudo-element leaves a
   real optical opening while retaining the original machined-metal shading. */
.vault-frame::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  transform: translateZ(0);
  background: radial-gradient(circle at 34% 26%, var(--vault-jamb), var(--vault-deep) 82%);
  box-shadow:
    inset 0 3px 6px var(--vault-edge-lit),
    inset 0 -5px 12px var(--vault-edge-dark);
  -webkit-mask: radial-gradient(circle, transparent 0 90.8%, #000 91.4%);
  mask: radial-gradient(circle, transparent 0 90.8%, #000 91.4%);
  pointer-events: none;
}

/* The cavity: the back wall of the vault, a long way behind the opening. Its
   parallax against the rim is most of what sells the door as covering a hole. */
.vault-cavity {
  position: absolute;
  inset: 7%;
  border-radius: 50%;
  transform: translateZ(-120px);
  background: radial-gradient(circle at 50% 40%, var(--vault-cavity), #000 88%);
  box-shadow: inset 0 10px 40px rgba(0, 0, 0, 0.9);
  overflow: hidden;
  display: grid;
  place-items: center;
}

/* What the open door reveals is a DARK ROOM: the flat cavity, the eight lit
   sockets and the soft accent halo, and nothing else. A glowing core with orbit
   rings and a logo used to sit here; it made the reveal an illustration of a
   product rather than an opening, and it is gone. The thing that comes out of
   this darkness is `.vault-threshold` below — the page's own background colour,
   growing. */

/* The parallax half of the dolly, and the reason the stage is capped at 560px.
   Magnification goes as (1400 - stageZ + depth) / (1400 - stageZ): at stageZ 560
   with the cavity resting at -120px that ratio is 1.143, which is flat. Receding
   the back wall to -240px over the same 700ms makes it 1.296 — the rim sweeps
   past the viewport edges while the back wall holds centre, which is what passing
   THROUGH a doorway looks like as opposed to zooming at a picture of one. Buying
   it here rather than by overdriving the stage is what keeps the near edge of the
   door inside the camera plane. */
.vault-scene.is-entering .vault-cavity {
  transform: translateZ(-240px);
  transition: transform 700ms cubic-bezier(0.45, 0, 0.85, 0.4);
}

/* The handoff, and its whole correctness is one colour. `--bg` is defined in
   styles.css — it is the wiki's own page background, the exact hex both
   login.html and base.html paint on `html` and `body` before any stylesheet
   loads. So this disc is not "a light"; it is the destination, seen through the
   hole, growing out of the dark. On the dark theme --bg and --vault-cavity are
   the identical hex, which makes it literally the mechanism dissolving into the
   page's own ground.
     No translateZ: `.vault-cavity` does not declare preserve-3d, so its children
   are already flattened into its plane and a Z here would be inert. The cavity's
   existing `overflow: hidden` clips the bloom to exactly the aperture, which is
   the point — the threshold's job is to be the colour of the OPENING; the veil
   covers the screen. */
.vault-threshold {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--bg);
  box-shadow: 0 0 90px var(--accent-soft);
  transform: scale(0.02);
  opacity: 0;
  transition: transform 560ms var(--ease-out), opacity 240ms linear;
}

.vault-scene.is-entering .vault-threshold {
  transform: scale(1.5);
  opacity: 1;
}

/* The cylindrical wall between the rim and the cavity. A ring, shaded so the far
   side is lit and the near side falls away. */
.vault-wall {
  position: absolute;
  inset: 4%;
  border-radius: 50%;
  transform: translateZ(-58px);
  background: conic-gradient(from 200deg,
    var(--vault-shadow) 0deg, var(--vault-mid) 90deg,
    var(--vault-hi) 165deg, var(--vault-mid) 250deg, var(--vault-shadow) 360deg);
  -webkit-mask: radial-gradient(circle, transparent 0 62%, #000 63%);
  mask: radial-gradient(circle, transparent 0 62%, #000 63%);
  box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.7);
}

.vault-socket {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 4%;
  height: 1.2%;
  margin: -0.6% 0 0 -2%;
  border-radius: 1px;
  background: var(--vault-socket);
  /* A shallow recess at radius .42W, contained within the visible jamb.
     The previous .12W-long blocks crossed into the opening like projecting tabs. */
  transform: rotate(var(--angle)) translateY(-3500%) translateZ(-14px);
  pointer-events: none;
}

/* --------------------------------------------------------------- the hinge */

/* Three stationary knuckles on a full-height barrel, and two moving straps that
   turn between them. The stationary half belongs to the FRAME, not the door: a
   hinge's fixed leaf stays put while the door turns on it, and on the door it
   would swing away with the thing it holds.
     What this replaced was two plain blocks and a 36%-tall stub pin, which meant
   NOTHING at the axis moved during a 106deg swing — so the door read as pivoting
   about an invisible line drawn somewhere off to the right. The straps are the
   fix and they cost no animation at all; see `.vault-hinge-strap`. */
.vault-hinge {
  position: absolute;
  top: 50%;
  right: -5%;
  width: 16%;
  height: 38%;
  margin-top: -19%;
  transform: translateZ(16px);
  transform-style: preserve-3d;
  pointer-events: none;
}

.vault-hinge-knuckle {
  position: absolute;
  right: 0;
  width: 100%;
  height: 26%;
  border-radius: 4px;
  background: linear-gradient(104deg,
    var(--vault-deep) 0%, var(--vault-hi) 24%, var(--vault-mid) 56%, var(--vault-shadow) 100%);
  /* One flat line, reading as the barrel's parting edge. Not a gradient: the
     flat composition below is the visual law and a knuckle is a plain fill. */
  border-left: 1px solid var(--vault-deep);
  box-shadow:
    inset 0 2px 3px var(--vault-edge-lit),
    inset 0 -3px 6px var(--vault-edge-dark),
    -4px 6px 14px rgba(0, 0, 0, 0.5);
}

/* 0-26 / 37-63 / 74-100 of the assembly's height. The two gaps those leave —
   frame-y 40.88-45.06% and 54.94-59.12% — are exactly where the door's straps
   land, and the strap geometry is derived from these numbers. Change one and
   redo the conversion in `.vault-hinge-strap`. */
.vault-hinge-knuckle:nth-child(1) { top: 0; }
.vault-hinge-knuckle:nth-child(2) { top: 37%; }
.vault-hinge-knuckle:nth-child(3) { top: 74%; }

/* A full-height rod running BEHIND the knuckles, not a stub between two plates.
   That single change is what stops the plates reading as plates and makes them
   knuckles ON something. -3px inside the assembly = frame +13px, so the knuckles
   at +16 sit proud of it and the interleave is not the renderer's choice. */
.vault-hinge-pin {
  position: absolute;
  top: 2%;
  right: 30%;
  width: 22%;
  height: 96%;
  border-radius: 999px;
  background: linear-gradient(100deg, var(--vault-shadow), var(--vault-lit) 42%, var(--vault-deep));
  transform: translateZ(-3px);
  box-shadow: inset 0 1px 2px var(--vault-edge-lit), -2px 3px 9px rgba(0, 0, 0, 0.55);
}

/* The moving leaves, and the only new elements in this design that need no rule
   of their own to animate: they are children of `.vault-door`, so they inherit
   its rotation for free, and because they sit at door-local x ~100% while the
   door's `transform-origin` is `right center` they are essentially ON the axis.
   During the swing they turn in PLACE between the stationary knuckles instead of
   travelling, which is exactly what a knuckle does.
     GEOMETRY, converted through the door's 4% inset once: door-local y =
   (frame_y - 4) / 0.92, which puts the two straps at top 40.1% and 55.4% (their
   `top` is inline in login.html, next to the two numbers it is derived from);
   door-local x = (frame_x - 4) / 0.92 gives right -9.8%, width 17.4%.
     Z, STATED AS A SUBTRACTION, because this is where two designs got it wrong:
   44 - 24 = frame +20px, which is 4px PROUD of the frame's knuckles at +16 and
   7px proud of the pin at +13. Deliberately NOT coplanar. `.vault-frame` is
   preserve-3d, so coplanar members have their paint order chosen by the
   renderer, and this file already records that it chose wrong once and punched
   eight black notches through a shut door. 4px of separation makes the interleave
   deterministic and costs nothing.
     `--vault-low`, so the moving leaf is visibly a different piece from the
   `--vault-mid` stationary ones, in both themes, with no new hex. */
.vault-hinge-strap {
  position: absolute;
  right: -9.8%;
  width: 17.4%;
  height: 4.5%;
  border-radius: 3px;
  background: var(--vault-low);
  transform: translateZ(-24px);
}

/* Load transfer, one pixel. At the unseat the knuckles drop and the pin shifts:
   the leaf's weight coming off the jamb and onto the pin. Half of it is recovered
   over the swing. It is one pixel, and it is the difference between a door that
   swings and a door that is heavy. */
.vault-scene.is-clear .vault-hinge-knuckle {
  transform: translateY(1px);
  transition: transform 180ms cubic-bezier(0.3, 0, 0.2, 1);
}

.vault-scene.is-clear .vault-hinge-pin {
  transform: translateZ(-3px) translateX(-1px);
  transition: transform 180ms cubic-bezier(0.3, 0, 0.2, 1);
}

.vault-scene.is-swinging .vault-hinge-knuckle {
  transform: translateY(0.5px);
  transition: transform 900ms cubic-bezier(0.3, 0, 0.2, 1);
}

.vault-scene.is-swinging .vault-hinge-pin {
  transform: translateZ(-3px) translateX(-0.5px);
  transition: transform 900ms cubic-bezier(0.3, 0, 0.2, 1);
}

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

.vault-door {
  position: absolute;
  inset: 4%;
  border-radius: 50%;
  transform-style: preserve-3d;
  /* Hinged on the RIGHT, matching where the assembly is drawn. The two must agree:
     a door turning about the edge opposite its hinge is the one mistake here that
     a viewer spots instantly without being able to say why.
     The 8px at rest also settles a paint-order ambiguity — the frame is
     preserve-3d, so a door coplanar with the sockets had its stacking chosen by
     the renderer, and it chose to punch eight black notches through a shut door. */
  /* ---- the slab, in one place ----------------------------------------------
     Thickness has one free end and one pinned end, so it can only grow FORWARD.
     The BACK is pinned: the frame's bolt sockets sit at -14px in frame space, and
     a disc that reaches past them interleaves with them inside the frame's own
     `preserve-3d` — which is the eight-black-notches-through-a-shut-door bug, and
     this clearance is its only defence. The FRONT is free, and a vault door
     standing proud of its jamb is what a real one does.
       back face = --slab-rest - --slab-depth = 44 - 54 = -10px, 4px clear of -14.
     Change either number and re-check that subtraction; nothing else enforces it.
     The hinge's +16px is already this slab's mid-depth (44 - 54/2 = 17), so its
     leaves read as bolted to the door's edge rather than to its face. */
  --slab-depth: 54px;
  --slab-rest: 44px;
  transform: translateZ(var(--slab-rest));
  transform-origin: right center;
  transition: transform 980ms cubic-bezier(0.36, 0, 0.16, 1);
}

/* THE UNSEAT. The door pulls 24px straight out of its jamb before anything
   turns, and counter-rotates 1.2deg INTO the hinge so the first degrees of the
   swing are recovering a load rather than starting from rest. Face-on the growing
   cast shadow on `.vault-face` is the read; the 1.8% perspective growth only
   confirms it.
     BACK-FACE ARITHMETIC, stated, because nothing else enforces it:
       rest      44 - 54 = -10px  (4px clear of the sockets at -14)
       unseated  68 - 54 = +14px  (28px clear)
       open      98 - 54 = +44px
   The back face only ever moves AWAY from the socket plane, and the -14px
   relationship is never tightened at any point in the sequence.
     A TRANSITION, not an animation, and that pairing is deliberate: skip() adds
   every state class in one go, so the terminal state has to be the one that wins,
   and an animation with `both` fill outranks a transition. Hence the swing below
   is the animation and this is the transition. */
.vault-scene.is-clear .vault-door {
  transform: translateZ(68px) rotateY(-1.2deg);
  transition: transform 320ms cubic-bezier(0.55, 0, 0.25, 1);
}

.vault-scene.is-swinging .vault-door,
.vault-scene.is-open .vault-door {
  animation: vault-swing 1350ms both;
}

/* The leaf. A heavy slow-in (a loaded door breaks away reluctantly), a long
   decelerating arc past the mark, a restrained rebound, and a short settle — plus
   0.55deg of rotateZ droop as follow-through, the leaf sagging on its pin now
   that the jamb no longer carries it.
     98px is written as `calc(var(--slab-rest) + var(--slab-depth))`, not as the
   literal it currently evaluates to: the clearance scales with the slab, not with
   a number that happened to work when the door was 18px thick. */
@keyframes vault-swing {
  0% {
    transform: translateZ(68px) rotateY(-1.2deg) rotateZ(0deg);
    animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);
  }
  84% {
    transform: translateZ(calc(var(--slab-rest) + var(--slab-depth))) rotateY(106.4deg) rotateZ(0.6deg);
    animation-timing-function: cubic-bezier(0.35, 0, 0.5, 1);
  }
  93% {
    transform: translateZ(calc(var(--slab-rest) + var(--slab-depth))) rotateY(105.8deg) rotateZ(0.5deg);
    animation-timing-function: cubic-bezier(0.25, 0.75, 0.35, 1);
  }
  100% {
    transform: translateZ(calc(var(--slab-rest) + var(--slab-depth))) rotateY(106deg) rotateZ(0.55deg);
  }
}

/* The whole door presses. The sensor says WHERE, but a 21%-wide circle is a small
   target — so the door carries the cursor and the hit area, and the sensor keeps
   the affordance. Reverts to default once the mechanism is running. */
.vault-door { cursor: pointer; }
.vault-scene.is-spinning .vault-door,
.vault-scene.is-reading .vault-door { cursor: progress; }

/* While the platform sheet is up, the door itself shows it is listening — the
   feedback should be on the thing that was pressed, not only on the small circle. */
.vault-scene.is-reading .vault-face {
  animation: vault-listening 1.6s ease-in-out infinite;
}

/* The cast shadow is repeated in both keyframes on purpose: `box-shadow` is one
   property, so omitting it here would blink the door's shadow off for the whole
   1.6s the platform sheet is up. */
@keyframes vault-listening {
  0%, 100% { box-shadow: inset 0 4px 9px var(--vault-edge-lit),
                         inset 0 -10px 24px var(--vault-edge-dark),
                         0 6px 18px rgba(0, 0, 0, 0.28); }
  50% { box-shadow: inset 0 4px 9px var(--vault-edge-lit),
                    inset 0 -10px 24px var(--vault-edge-dark),
                    inset 0 0 40px var(--accent-soft),
                    0 6px 18px rgba(0, 0, 0, 0.28); }
}

/* The extruded rim — the door's thickness, and the whole reason it reads as a slab
   rather than as a lid. A stack of discs, and the count is not cosmetic: swung to
   106deg the rim is seen nearly edge-on, so the stack must be dense enough that the
   gaps between discs never open up AND that the darkness ramp below reads as a
   gradient rather than as stripes — the second constraint is the binding one, and it
   is why this is 28 discs and not 8. 28 across 54px is ~1.9px apart, and each step
   darkens by 0.32/28 = 0.011, under what the eye picks out as a band.
     Each disc carries only `--d`, its depth as a 0..1 fraction. The actual
   thickness lives once, in `--slab-depth` on the door, so the template says how
   MANY discs and the CSS says how DEEP — they cannot drift apart. `--d` also drives
   the darkness ramp: the far end of a cylinder is in its own shadow, and that ramp
   is most of what makes the stack read as one turned piece of steel.
     The overlay is a flat translucent black, deliberately NOT `filter: brightness()`
   — filter creates a containing block and flattens `preserve-3d` beneath it, which
   is what made this door look 2D once already. */
.vault-edge {
  position: absolute;
  inset: -1%;
  border-radius: 50%;
  transform: translateZ(calc(var(--d) * var(--slab-depth) * -1));
  /* FLAT, not a radial gradient, and that is the whole trick. A per-disc highlight
     is invisible while the door is shut — the face covers it — but swung open the
     rim is seen end-on and the same highlight repeats once per disc, which reads as
     corduroy down the steel. Flat discs plus the depth ramp give one smooth shade
     across the rim instead, which is what a turned cylinder actually looks like.
     Base is `--vault-deep`, not `--vault-low`: on the light theme `--vault-low` is
     the jamb's own colour, so the shut door's rim would dissolve into the frame.
     The ramp stops at 0.32 because 0.55 put a near-black slab on a pale grey face —
     right on the dark theme, a hole punched in the light one. */
  background:
    linear-gradient(rgba(0, 0, 0, calc(var(--d) * 0.32)), rgba(0, 0, 0, calc(var(--d) * 0.32))),
    var(--vault-deep);
}

/* The face — and the door's cast shadow, which lives HERE and not on
   `.vault-door`. The door is the preserve-3d PARENT of the whole stack, so its
   own painted box would be coplanar with this one, which is the exact paint-order
   ambiguity that produced the eight-black-notches bug. `.vault-face` is a real
   child at the front plane and already carries the inset shading, so the drop
   shadow rides along with it for free.
     Face-on — and the flat composition below sets `.vault-frame { transform:
   none }`, so face-on is the only view — a beat carried by translateZ alone is a
   sub-2% scale change. The unseat's read is this shadow ballooning, not the size. */
.vault-face {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle at 27% 19%, var(--vault-lit) 0%, transparent 17%),
    radial-gradient(circle at 32% 24%,
      var(--vault-hi) 0%, var(--vault-mid) 34%, var(--vault-low) 62%, var(--vault-deep) 88%,
      var(--vault-shadow) 100%);
  box-shadow:
    inset 0 4px 9px var(--vault-edge-lit),
    inset 0 -10px 24px var(--vault-edge-dark),
    0 6px 18px rgba(0, 0, 0, 0.28);
  transition: box-shadow 320ms cubic-bezier(0.55, 0, 0.25, 1);
}

.vault-scene.is-clear .vault-face {
  box-shadow:
    inset 0 4px 9px var(--vault-edge-lit),
    inset 0 -10px 24px var(--vault-edge-dark),
    0 18px 46px rgba(0, 0, 0, 0.42);
}

/* Machined plates, each a step forward. The Z gaps are what the eye reads as
   turned steps; the shading only confirms them. */
.vault-plate {
  position: absolute;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  transform: translate(-50%, -50%) translateZ(var(--z));
  background: radial-gradient(circle at 32% 24%,
    var(--vault-hi) 0%, var(--vault-mid) 42%, var(--vault-low) 78%, var(--vault-deep) 100%);
  box-shadow:
    inset 0 3px 7px var(--vault-edge-lit),
    inset 0 -5px 12px var(--vault-edge-dark),
    0 3px 10px rgba(0, 0, 0, 0.35);
}

.vault-plate--outer { width: 86%; height: 86%; }
.vault-plate--mid { width: 62%; height: 62%; }
.vault-plate--inner { width: 44%; height: 44%; }

.vault-bolt {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 5%;
  height: 15%;
  margin: -7.5% 0 0 -2.5%;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--vault-lit) 0%, var(--vault-mid) 44%, var(--vault-deep) 100%);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6), inset 0 1px 0 var(--vault-edge-lit);
  opacity: 0;
  /* The back of the leaf is a closed slab. A bolt face must not show through
     it when the hinge turns beyond 90 degrees. */
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  /* Bolt height is 15% of the DOOR (0.138W); seated at r=0.42W → -(0.42/0.138)·100%.
     Z is the SLAB'S BACK FACE, not a fixed offset, and that is load-bearing: a bolt
     protrudes ~9px past the door's edge so it can reach the jamb, so whatever plane
     it sits on decides whether a SHUT door shows eight pale tabs sticking out over
     its frame. At the back face it lands 4px off the sockets (-14px in frame space)
     — behind the frame's own face, which is what hides it — and it reads as passing
     through the full thickness of the door, which is where a real bolt lives.
     Pinned to `--slab-depth` so thickening the slab cannot strand it mid-air. */
  transform: rotate(var(--angle)) translateY(-304%) translateZ(calc(var(--slab-depth) * -1));
  transition: transform var(--vault-drive-duration) var(--vault-drive-ease), opacity 100ms linear;
}

.vault-scene.is-unbolting .vault-bolt,
.vault-scene.is-swinging .vault-bolt,
.vault-scene.is-open .vault-bolt {
  /* withdrawn to r=0.33W: clear of the jamb, still inside the door body */
  transform: rotate(var(--angle)) translateY(-239%) translateZ(calc(var(--slab-depth) * -1));

}

.vault-scene.is-swinging .vault-bolt,
.vault-scene.is-open .vault-bolt { opacity: 1; }

/* ---------------------------------------------------- the visible boltwork */

/* The beat captioned "bolts clear" used to be a literally blank 480ms: the real
   bolts are `opacity: 0` until the door is already swinging, so the only thing on
   screen was a wheel turning. These are the heads, on the door's exposed face,
   withdrawing along machined tracks — the thing the caption was naming all along.
   (Which is why the caption is now "Lock released": you can SEE the bolts clear,
   so the word that is needed is the one naming the cause.)
     A moving object with no track is a moving object; a moving object in a slot
   is a mechanism. Hence the slots, which are static and exist only to be the run
   the head travels along.
     Z IS LOAD-BEARING on both. The head at 12px is IN FRONT of
   `.vault-plate--outer` at z 6px, whose 0.396 radius would otherwise swallow the
   withdrawn end of the travel — at 4px, behind that plate, more than half the
   motion of the beat invented to fix invisible bolts is itself invisible. The
   track at 8px sits between the two.
     `--vault-shadow` for the track, not `--vault-deep`: on the dark theme
   --vault-deep (#1c1917) on the --vault-low (#292524) outer plate is about 1.3:1
   and the track simply vanishes. */
.vault-slot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 5.4%;
  height: 16.2%;
  margin: -8.1% 0 0 -2.7%;
  border-radius: 3px;
  /* h = 0.162 of the door box, centre at r 0.3726 → spans 0.298 to 0.447 */
  transform: rotate(var(--angle)) translateY(-250%) translateZ(8px);
  background: var(--vault-shadow);
}

.vault-lug {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 3.2%;
  height: 16%;
  margin: -8% 0 0 -1.6%;
  border-radius: 3px;
  background: var(--vault-hi);
  border: 0;
  outline: 1px solid var(--vault-deep);
  box-sizing: border-box;
  transform: rotate(var(--angle)) translateY(-231.25%) translateZ(28px);
  transition: transform var(--vault-drive-duration) var(--vault-drive-ease);
  pointer-events: none;
}

/* The centre of this pin is 87.5% down its .16W slider: radius .31W at
   rest, .24W withdrawn, exactly the two endpoints engraved in the cam. */
.vault-lug::after {
  content: "";
  position: absolute;
  width: 70%;
  aspect-ratio: 1;
  left: 50%;
  top: 87.5%;
  translate: -50% -50%;
  border: 1px solid var(--vault-deep);
  border-radius: 50%;
  background: var(--vault-mid);
}

.vault-lug::before {
  content: "";
  position: absolute;
  top: 4%;
  left: -30%;
  width: 160%;
  height: 32%;
  box-sizing: border-box;
  border: 1px solid var(--vault-deep);
  border-radius: 2px;
  background: var(--vault-hi);
}

.vault-scene.is-unbolting .vault-lug,
.vault-scene.is-clear .vault-lug,
.vault-scene.is-swinging .vault-lug,
.vault-scene.is-open .vault-lug {
  transform: rotate(var(--angle)) translateY(-187.5%) translateZ(28px);
}

/* ------------------------------------------------------------- handwheel */

.vault-spokes {
  position: absolute;
  inset: 29%;
  transform: translateZ(32px) rotate(0deg);
  transform-style: preserve-3d;
  transition: transform var(--vault-drive-duration) var(--vault-drive-ease);
  pointer-events: none;
}

/* Free play first, then the drive. The wheel is the one part a hand would
   touch, so it is the one part that moves FIRST: on `is-spinning` it turns 8deg
   alone — the keyway slack being taken up — and the cam holds still until
   `is-unbolting`. That lag is what says the wheel DRIVES the cam rather than
   both being animated, and it lives on the wheel because it cannot live on the
   cam: pin-in-track means cam angle and lug radius are rigidly coupled, so the
   cam, lugs and hidden bolts must share one duration and easing to the frame.
   During the drive the wheel turns the same 30deg on the same curve, 8deg
   ahead, so once the slack is out the two parts move as one piece. */
.vault-scene.is-spinning .vault-spokes {
  transform: translateZ(32px) rotate(8deg);
  transition-duration: 240ms;
  transition-timing-function: cubic-bezier(0.2, 0.6, 0.3, 1);
}

.vault-scene.is-unbolting .vault-spokes {
  transform: translateZ(32px) rotate(38deg);
  transition-duration: var(--vault-drive-duration);
  transition-timing-function: var(--vault-drive-ease);
}

.vault-spoke {
  position: absolute;
  left: 50%;
  top: 0;
  /* 8.5% of the wheel box is 3.6% of the door: the same bar weight as the lug
     sliders. At 5% the wheel read as wire in front of the machinery it drives. */
  width: 8.5%;
  height: 50%;
  margin-left: -4.25%;
  background: var(--vault-hi);
  border: 1px solid var(--vault-deep);
  border-radius: 2px;
  transform-origin: 50% 100%;
  transform: rotate(var(--spoke));
}

.vault-grip {
  position: absolute;
  top: -5%;
  left: 50%;
  width: 130%;
  aspect-ratio: 1;
  translate: -50% 0;
  border: 1px solid var(--vault-deep);
  border-radius: 50%;
  background: var(--vault-hi);
}

.vault-dial {
  position: absolute;
  inset: 29%;
  border-radius: 50%;
  transform: translateZ(31px);
  pointer-events: none;
}

/* Fixed at twelve: a single state signal, filled only after all bolts clear. */
.vault-index {
  position: absolute;
  top: 24%;
  left: 50%;
  width: 1.5%;
  height: 1.5%;
  translate: -50% -50%;
  border: 1px solid var(--vault-ink);
  border-radius: 50%;
  background: var(--vault-mid);
  transform: translateZ(36px);
}

.vault-scene.is-released .vault-index { background: var(--accent); border-color: var(--accent); }

/* --------------------------------------------------------------- the sensor */

/* The button you actually press, so it is a real <button> with a real focus ring —
   the theatre never costs the affordance. Accent-coloured, which is also how it
   matches the rest of the site rather than the door. */
.vault-sensor {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 21%;
  height: 21%;
  margin: -10.5% 0 0 -10.5%;
  transform: translateZ(50px);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 32%, var(--vault-low), var(--vault-shadow) 78%);
  color: var(--accent);
  box-shadow:
    inset 0 2px 5px var(--vault-edge-lit),
    inset 0 -4px 10px var(--vault-edge-dark),
    0 0 0 1px var(--accent-soft),
    0 0 26px var(--accent-soft),
    0 6px 16px rgba(0, 0, 0, 0.5);
  cursor: pointer;
  transition: box-shadow 220ms var(--ease-out), transform 120ms var(--ease-out);
}

.vault-sensor:hover {
  box-shadow:
    inset 0 2px 5px var(--vault-edge-lit),
    inset 0 -4px 10px var(--vault-edge-dark),
    0 0 0 1px var(--accent),
    0 0 40px var(--accent-soft),
    0 6px 16px rgba(0, 0, 0, 0.5);
}

.vault-sensor:active { transform: translateZ(46px) scale(0.97); }

.vault-sensor:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.vault-sensor svg { width: 58%; height: 58%; }

.vault-sensor::before {
  content: "";
  position: absolute;
  inset: -18%;
  border: 1px solid var(--accent);
  border-radius: 50%;
  opacity: 0;
  transform: scale(0.84);
  pointer-events: none;
}

.vault-sensor.is-scanning::before {
  animation: vault-sensor-pulse 1.5s var(--ease-out) infinite;
}

.vault-scan {
  position: absolute;
  left: 14%;
  right: 14%;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  box-shadow: 0 0 12px var(--accent);
  opacity: 0;
  top: 24%;
}

.vault-sensor.is-scanning .vault-scan {
  opacity: 1;
  animation: vault-scan 1.1s ease-in-out infinite;
}

@keyframes vault-scan {
  0% { top: 24%; opacity: 0.2; }
  50% { top: 72%; opacity: 1; }
  100% { top: 24%; opacity: 0.2; }
}

@keyframes vault-sensor-pulse {
  0% { opacity: 0.72; transform: scale(0.84); }
  72%, 100% { opacity: 0; transform: scale(1.42); }
}

.vault-sensor.is-scanning {
  box-shadow:
    inset 0 2px 5px var(--vault-edge-lit),
    inset 0 -4px 10px var(--vault-edge-dark),
    0 0 0 1px var(--accent),
    0 0 52px var(--accent),
    0 6px 16px rgba(0, 0, 0, 0.5);
}

.vault-sensor.is-rejected {
  color: var(--red);
  box-shadow:
    inset 0 2px 5px var(--vault-edge-lit),
    inset 0 -4px 10px var(--vault-edge-dark),
    0 0 0 1px var(--red),
    0 0 34px var(--red-soft);
  animation: vault-shake 320ms ease;
}

@keyframes vault-shake {
  0%, 100% { transform: translateZ(50px) translateX(0); }
  20% { transform: translateZ(50px) translateX(-5px); }
  45% { transform: translateZ(50px) translateX(4px); }
  70% { transform: translateZ(50px) translateX(-2px); }
}

/* Fade the sensor and fixed release indicator during the 320ms unseat,
   finishing before the leaf begins its swing. */
.vault-scene.is-clear .vault-sensor,
.vault-scene.is-clear .vault-index {
  opacity: 0;
  transition: opacity 220ms var(--ease-in);
  pointer-events: none;
}

/* ------------------------------------------------------- sheen and shading */

/* The moving specular: the cue that the surface is metal. */
.vault-sheen {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  transform: translateZ(54px) translateX(-14%);
  background: linear-gradient(112deg,
    transparent 26%,
    var(--vault-edge-lit) 43%,
    transparent 66%);
  opacity: 0.55;
  transition: transform 980ms cubic-bezier(0.36, 0, 0.16, 1), opacity 980ms linear;
}

.vault-scene.is-swinging .vault-sheen,
.vault-scene.is-open .vault-sheen {
  transform: translateZ(54px) translateX(62%);
  opacity: 0;
}

/* Dimming as the door turns away from the light. An overlay, NOT `filter:
   brightness()` — a filter on the door would create a containing block and
   flatten every translateZ beneath it, which is precisely what made the first
   version look like a painted disc. */
.vault-shade {
  position: absolute;
  inset: -1%;
  border-radius: 50%;
  pointer-events: none;
  transform: translateZ(56px);
  background: #05070c;
  opacity: 0;
  transition: opacity 980ms cubic-bezier(0.36, 0, 0.16, 1);
}

/* Ease-IN, deliberately: light falls off as the cosine of the face angle, so a
   turning door dims slowly at first and then quickly. An ease-out here darkens
   fastest while the door is still nearly flat on, which is backwards. */
.vault-scene.is-swinging .vault-shade,
.vault-scene.is-open .vault-shade {
  opacity: 0.62;
  transition: opacity 1250ms cubic-bezier(0.5, 0, 0.2, 1);
}

/* Light escaping the opening. Sits in the frame, so it is revealed rather than
   carried away by the door. */
.vault-glow {
  position: absolute;
  inset: -14%;
  border-radius: 50%;
  pointer-events: none;
  transform: translateZ(-40px);
  background: radial-gradient(circle,
    var(--accent) 0%, var(--accent-soft) 34%, transparent 64%);
  opacity: 0;
  transition: opacity 950ms var(--ease-out);
}

.vault-glow::before {
  content: "";
  position: absolute;
  inset: -34%;
  border-radius: 50%;
  background: repeating-conic-gradient(from 8deg,
    transparent 0 10deg, rgba(96, 165, 250, 0.34) 11deg 12deg, transparent 13deg 25deg);
  -webkit-mask: radial-gradient(circle, transparent 0 23%, #000 49%, transparent 72%);
  mask: radial-gradient(circle, transparent 0 23%, #000 49%, transparent 72%);
  opacity: 0;
}

/* Pre-lit at the unseat, on the base transition above: light escaping a door
   that is 24px out of its jamb but not yet turned. */
.vault-scene.is-clear .vault-glow { opacity: 0.14; }

/* The reveal is its OWN beat, on `is-open`, rather than firing under the turn.
   Held stillness with the camera parked in front of a lit doorway is the payoff;
   competing with a 106deg swing is not. */
.vault-scene.is-open .vault-glow {
  opacity: 0.62;
  animation: vault-reveal-flare 1100ms cubic-bezier(0.12, 0.82, 0.18, 1) both;
}

.vault-scene.is-open .vault-glow::before {
  opacity: 0.72;
  animation: vault-orbit-reverse 20s linear infinite;
}

/* The 0% is 0.14, NOT 0, and that is the pre-lit value above. This animation has
   `both` fill, so it outranks the transitioned opacity the instant it is applied
   — starting it from 0 snapped the glow back to black for one frame, a visible
   flicker at exactly the moment the door is meant to be revealing something. */
@keyframes vault-reveal-flare {
  0% { opacity: 0.14; transform: translateZ(-40px) scale(0.62); }
  42% { opacity: 0.95; transform: translateZ(-40px) scale(1.12); }
  100% { opacity: 0.62; transform: translateZ(-40px) scale(1); }
}

/* Dropped one beat before the dolly magnifies it, while there is still other
   motion to hide its going. `0 48px 90px` about to be scaled ~2.5x is a
   several-thousand-pixel blurred rectangle re-rastered every frame, and a real
   dropped-frame risk in WKWebView, which is where the desktop app runs. */
.vault-scene.is-open .vault-frame { box-shadow: none; }

/* ------------------------------------------------------------------ the veil */

/* A full-viewport plane of the destination colour, covering the corners the
   aperture cannot reach and taking the HUD, the caption and the skip chip with
   it. `--bg` again, from styles.css: by the time this is opaque the screen is a
   uniform field of exactly the hex the next document paints on `html` and
   `body`, so the document swap has nothing to see.
     It MUST be a SIBLING of `.vault-scene`, not a child. The scene is z-index 400
   and therefore its own stacking context, so a child at z-index 999 still paints
   below `.vault-hud` at 401 — the caption and the skip chip would float on top of
   a fully opaque veil. 403 also clears `.vault-theme` at 402. The file already
   has precedent for the sibling selector: `.vault-hud.is-unlocking ~ .vault-theme`. */
.vault-veil {
  position: fixed;
  inset: 0;
  z-index: 403;
  background: var(--bg);
  opacity: 0;
  pointer-events: none;
}

#vault.is-entering ~ .vault-veil {
  opacity: 1;
  transition: opacity 460ms var(--ease-out) 240ms;
}

/* ------------------------------------------------------------ the bfcache reset */

/* Back can still land here with history intact — a session-expiry redirect, a
   typed URL — and the browser restores the DOM exactly as it was left: an opened
   vault under an opaque veil. vault.js strips the state classes on `pageshow`,
   and this is what makes that a SNAP rather than a 980ms reverse swing. */
.vault-scene.is-resetting * {
  transition-duration: 0s !important;
  animation: none !important;
}

/* ------------------------------------------------------------------ caption */

.vault-status {
  margin: 0;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.vault-status:empty { display: none; }

.vault-skip {
  position: absolute;
  top: max(4vh, 20px);
  right: max(4vw, 20px);
  padding: 6px 12px;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
}

.vault-skip:hover { color: var(--text); border-color: var(--border-strong); }

/* ------------------------------------------------ the card, on its way out */

/* --------------------------------------------------------------------- HUD */

/* A layer over the scene, not inside it: the scene is a 3D context and text
   parented into one inherits the tilt and renders skewed. */
.vault-hud {
  position: fixed;
  inset: 0;
  z-index: 401;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: max(5vh, 26px) 20px max(4vh, 22px);
  pointer-events: none;
  text-align: center;
}

.vault-hud > * { pointer-events: auto; }

.vault-hud-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  pointer-events: auto;
}

/* `min-height` on .vault-status is what reserves the line, so filling it mid-
   sequence does not shove the pill downward. An `order` was the first attempt and
   it did something else entirely: it moved the status to the end of the flex line,
   below the Devices link. */
.vault-hud-foot .vault-devices { margin-top: 3px; }

.vault-title {
  margin: 0;
  font-family: "Newsreader", "Inter", serif;
  font-weight: 600;
  font-size: clamp(17px, 3.2vw, 21px);
  letter-spacing: 0.01em;
  color: var(--text-secondary);
}

.vault-title-lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  animation: vault-title-arrive 760ms var(--ease-out) 300ms both;
}

@keyframes vault-title-arrive {
  from { opacity: 0; transform: translateY(-8px); letter-spacing: 0.08em; }
  to { opacity: 1; transform: translateY(0); letter-spacing: 0; }
}

/* ------------------------------------------------------- the password pill */

/* The input IS the control. There is no button in front of it, because a button
   that reveals an input needs script to do the revealing and this is the recovery
   route — it has to work with vault.js absent. At rest it is a pill the width of
   its own placeholder; `:focus-within` widens it and brings out the submit arrow,
   so clicking it is the same gesture the old button was, and Tab reaches it too. */
.vault-key {
  display: inline-flex;
  align-items: center;
  height: 38px;
  padding: 0 4px 0 0;
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: width 320ms var(--ease-panel), background 200ms var(--ease-out),
              border-color 200ms var(--ease-out), box-shadow 200ms var(--ease-out);
}

.vault-key:hover { border-color: var(--border-strong); }

.vault-key:focus-within {
  background: var(--bg-elevated);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-md);
}

/* The visible label is the placeholder; this one exists for the accessibility
   tree, which a placeholder does not reliably serve. */
.vault-key-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.vault-key-input {
  width: 132px;
  height: 100%;
  padding: 0 16px;
  font: inherit;
  font-size: 13.5px;
  text-align: center;
  color: var(--text);
  background: none;
  border: 0;
  border-radius: 999px;
  transition: width 320ms var(--ease-panel), text-align 0s;
}

.vault-key-input::placeholder {
  color: var(--text-tertiary);
  letter-spacing: 0.04em;
}

.vault-key-input:focus { outline: none; }

.vault-key:focus-within .vault-key-input {
  width: min(232px, calc(100vw - 120px));
  text-align: left;
}

/* Collapsed to nothing at rest, so the pill is exactly as wide as the word in it.
   It stays in the DOM and submittable throughout — Enter in the field posts the
   form whether or not this is visible. */
.vault-key-go {
  display: grid;
  place-items: center;
  width: 0;
  height: 30px;
  padding: 0;
  overflow: hidden;
  color: var(--on-accent);
  background: var(--accent);
  border: 0;
  border-radius: 999px;
  opacity: 0;
  cursor: pointer;
  transition: width 320ms var(--ease-panel), opacity 200ms var(--ease-out) 80ms;
}

.vault-key:focus-within .vault-key-go {
  width: 30px;
  opacity: 1;
}

.vault-key-go:hover { background: var(--accent-hover); }
.vault-key-go svg { width: 15px; height: 15px; }

/* --------------------------------------------------------------- the quiet bits */

/* Empty at rest, both of them. The status fills in during the unlock sequence and
   the hint only when the sensor cannot be used — a page whose default state is
   three lines of instructions is not a minimal page. */
.vault-status {
  margin: 0;
  min-height: 15px;
  font-size: 11.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.vault-hint {
  margin: 0;
  max-width: 34ch;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-tertiary);
}

.vault-hint:empty { display: none; }

.vault-devices {
  font-size: 11.5px;
  color: var(--text-tertiary);
  text-decoration: none;
  opacity: 0.75;
  transition: opacity 160ms var(--ease-out), color 160ms var(--ease-out);
}

.vault-devices:hover { opacity: 1; color: var(--accent); }

.vault-theme {
  position: fixed;
  top: max(3vh, 16px);
  right: max(3vw, 16px);
  z-index: 402;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  color: var(--text-tertiary);
  background: none;
  border: 1px solid transparent;
  border-radius: 50%;
  cursor: pointer;
  transition: color 160ms var(--ease-out), border-color 160ms var(--ease-out);
}

.vault-theme:hover { color: var(--text); border-color: var(--border); }
.vault-theme svg { width: 16px; height: 16px; }
.vault-theme-sun { display: none; }
[data-theme="dark"] .vault-theme-moon { display: none; }
[data-theme="dark"] .vault-theme-sun { display: block; }

/* ---------------------------------------------------------- flat composition */

/* The visual direction is intentionally flat: depth comes from real Z offsets,
   borders and shadows, never colour ramps. Ambient rings, light shafts and the
   moving sheen are removed so the circular mechanism is the only illustration. */
.vault-backdrop { background: var(--vault-ground); }
.vault-backdrop::before,
.vault-backdrop::after,
.vault-stage::before,
.vault-stage::after,
.vault-sheen,
.vault-glow::before { display: none; }

.vault-stage { width: var(--vault-size); }
.vault-rig { transform: none; }
/* The same list as the main section's rig-lift rule, and it has to be: this
   redefines that selector, so a class named there and missed here drops the lift
   the moment the sequence reaches it. */
.vault-scene.is-reading .vault-rig,
.vault-scene.is-spinning .vault-rig,
.vault-scene.is-released .vault-rig,
.vault-scene.is-unbolting .vault-rig,
.vault-scene.is-clear .vault-rig,
.vault-scene.is-swinging .vault-rig,
.vault-scene.is-open .vault-rig,
.vault-scene.is-entering .vault-rig { transform: translateZ(10px) scale(1.012); }

/* A neutral rest pose keeps the back slab, face, dial and sensor perfectly
   concentric. The hinge animation still supplies the 3D turn after unlock. */
.vault-frame { transform: none; }
.vault-frame::before { background: var(--vault-jamb); }
.vault-cavity { background: var(--vault-cavity); }
.vault-wall { background: var(--vault-deep); }
.vault-hinge-knuckle { background: var(--vault-mid); }
.vault-hinge-pin { background: var(--vault-deep); }
.vault-edge { background: var(--vault-deep); }
.vault-face { background: var(--vault-mid); }
.vault-plate { background: var(--vault-mid); }
.vault-plate--outer { background: var(--vault-low); }
.vault-plate--inner { background: var(--vault-hi); }
.vault-bolt { background: var(--vault-mid); }
/* One material rule for the boltwork: everything that MOVES — lug sliders,
   spokes, grip — is --vault-hi with an ink border, and every static face is
   mid. Mid-on-mid spokes left only their 1px borders visible, which made the
   driver of the whole mechanism its least material part. */
.vault-spoke { background: var(--vault-hi); }
.vault-dial {
  box-sizing: border-box;
  background: transparent;
  border: clamp(4px, 1.1vmin, 7px) solid var(--vault-hi);
  outline: 1px solid var(--vault-deep);
  -webkit-mask: none;
  mask: none;
}
.vault-sensor { background: var(--vault-shadow); }
.vault-scan { background: var(--accent); }
.vault-glow {
  background: transparent;
  box-shadow: 0 0 72px var(--accent-soft);
}

/* The cam is the mechanism: one plate, eight tracks, eight follower pins.
   Cam and followers use the same duration/easing so their positions agree at
   every frame. No decorative rotation or stagger can separate a pin from its slot. */
:root { --vault-ink: var(--vault-deep); }
[data-theme="dark"] { --vault-ink: var(--vault-hi); }

.vault-transmission {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: translateZ(24px);
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  pointer-events: none;
}

.vault-annulus {
  fill: var(--vault-low);
  stroke: var(--vault-ink);
  stroke-width: 1;
  stroke-linejoin: round;
}

.vault-carrier,
.vault-cam {
  transform-box: view-box;
  transform-origin: 200px 200px;
  transform: rotate(0deg);
  transition: transform var(--vault-drive-duration) var(--vault-drive-ease);
}

.vault-carrier-race { fill: none; stroke: var(--vault-ink); stroke-width: 1; }
.vault-carrier-arm { stroke: var(--vault-hi); stroke-width: 5; }
.vault-cam-plate { fill: var(--vault-mid); stroke: var(--vault-ink); stroke-width: 1; }
.vault-cam-track {
  fill: none;
  stroke: var(--vault-deep);
  stroke-width: 7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Geometry and ratios come from scripts/vault_gearing.py. Both gear contacts
   roll without slip: the outer ring holds still while the carrier orbits 13.5deg
   and each planet spins -135deg in door space during the 30deg cam drive. */
.vault-gear {
  transform-box: view-box;
  transform-origin: 0 0;
  transform: rotate(var(--gear-start));
  transition: transform var(--vault-drive-duration) var(--vault-drive-ease);
}

.vault-gear-body {
  fill: var(--vault-hi);
  stroke: var(--vault-deep);
  stroke-width: 1;
  stroke-linejoin: round;
}

.vault-gear-axle { fill: var(--vault-deep); }
.vault-gear-mark { stroke: var(--vault-deep); stroke-width: 1.5; stroke-linecap: round; }
.vault-scene.is-unbolting .vault-cam { transform: rotate(var(--cam-turn)); }
.vault-scene.is-unbolting .vault-carrier { transform: rotate(var(--carrier-turn)); }
.vault-scene.is-unbolting .vault-gear { transform: rotate(calc(var(--gear-start) + var(--gear-drive))); }

/* Flat faces and precise edges. One restrained cast shadow anchors the object;
   repeated inset bevels would make the plate stack look inflated. */
.vault-frame { box-shadow: 0 22px 40px rgba(0, 0, 0, 0.16); }
.vault-frame::before,
.vault-hinge-knuckle,
.vault-hinge-pin,
.vault-face,
.vault-plate,
.vault-spoke,
.vault-dial { box-shadow: none; }
.vault-face { border: 1px solid var(--vault-deep); }
.vault-scene.is-reading .vault-face { animation: none; }
.vault-scene.is-clear .vault-face { box-shadow: 0 12px 26px rgba(0, 0, 0, 0.2); }
.vault-plate--outer { background: var(--vault-low); }
.vault-plate--mid,
.vault-plate--inner { background: var(--vault-mid); }
.vault-hinge-knuckle { border: 1px solid var(--vault-deep); }
.vault-sensor,
.vault-sensor:hover,
.vault-sensor.is-inert,
.vault-sensor.is-scanning {
  box-shadow: 0 0 0 1px var(--vault-ink);
}
.vault-sensor { color: var(--vault-hi); }
.vault-scene.is-spinning .vault-sensor { color: var(--accent); }
.vault-sensor svg { width: 48%; height: 48%; }

/* Title, mechanism and recovery control now use one explicit centre line. The
   positions derive from the same `--vault-size`, so changing viewport size cannot
   make the HUD drift away from the door. */
.vault-hud {
  display: block;
  padding: 0;
}

.vault-title-lockup {
  position: absolute;
  top: calc(50% - var(--vault-size) / 2 - 46px);
  left: 50%;
  translate: -50% 0;
  width: max-content;
  max-width: calc(100vw - 80px);
}

.vault-hud-foot {
  position: absolute;
  top: calc(50% + var(--vault-size) / 2 + 26px);
  left: 50%;
  translate: -50% 0;
  width: min(320px, calc(100vw - 40px));
}

/* Everything but the status steps back once the door starts opening. */
.vault-hud.is-unlocking .vault-title-lockup,
.vault-hud.is-unlocking .vault-key,
.vault-hud.is-unlocking .vault-hint,
.vault-hud.is-unlocking .vault-devices,
.vault-hud.is-unlocking ~ .vault-theme {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 320ms var(--ease-in), transform 420ms var(--ease-panel);
  pointer-events: none;
}

.vault-title-lockup,
.vault-key,
.vault-hint,
.vault-devices { transition: opacity 240ms var(--ease-out), transform 240ms var(--ease-out); }

/* A browser with no platform authenticator, or a host with nothing enrolled: the
   sensor stays visible — it is part of the door — but stops claiming it can be
   pressed. */
.vault-sensor.is-inert {
  cursor: default;
  color: var(--text-tertiary);
  box-shadow: 0 0 0 1px var(--vault-ink);
}

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

/* ------------------------------------------------------------ reduced motion */

/* The door still opens — it just stops performing. A 3s mechanical sequence is
   exactly the motion this setting exists to switch off. */
@media (prefers-reduced-motion: reduce) {
  .vault-scene.is-reading .vault-face { animation: none; }
  .vault-cam,
  .vault-carrier,
  .vault-gear,
  .vault-stage,
  .vault-stage::before,
  .vault-stage::after,
  .vault-rig,
  .vault-door,
  .vault-face,
  .vault-sheen,
  .vault-shade,
  .vault-bolt,
  .vault-lug,
  .vault-slot,
  .vault-dial,
  .vault-spokes,
  .vault-grip,
  .vault-index,
  .vault-hinge-knuckle,
  .vault-hinge-pin,
  .vault-hinge-strap,
  .vault-cavity,
  .vault-threshold,
  .vault-veil,
  .vault-backdrop,
  .vault-backdrop::after,
  .vault-glow,
  .vault-glow::before,
  .vault-title-lockup {
    transition-duration: 1ms !important;
    transition-delay: 0s !important;
    animation-duration: 1ms !important;
    /* No delayed effects may survive the reduced-motion navigation. */
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
  }
  .vault-stage,
  .vault-backdrop { animation-duration: 1ms !important; }
  .vault-scene.is-swinging .vault-door,
  .vault-scene.is-open .vault-door { transform: none; opacity: 0; }
  .vault-sensor.is-scanning .vault-scan,
  .vault-sensor.is-scanning::before,
  .vault-sensor.is-rejected { animation: none; }
  body.is-unlocking .lock-card { transition-duration: 1ms; }
}

@media (max-height: 640px) {
  .vault-hud { padding-top: 18px; padding-bottom: 14px; }
  .vault-hud-foot { gap: 6px; }
}

/* ------------------------------------------------------- the devices page */

.passkey-page { max-width: 760px; }

.passkey-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.6rem;
  margin: 0 0 1rem;
  font-size: 0.85rem;
}

.passkey-fact-key {
  margin-right: 0.5rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.passkey-note {
  margin: 0 0 1.4rem;
  padding: 0.7rem 0.9rem;
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--text-secondary);
  background: var(--amber-soft);
  border-left: 3px solid var(--amber);
  border-radius: var(--radius-sm);
}

.passkey-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
  margin-bottom: 0.9rem;
}

.passkey-add {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 1rem;
  font: inherit;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--on-accent);
  background: var(--accent);
  border: 0;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease-out);
}

.passkey-add:hover:not(:disabled) { background: var(--accent-hover); }
.passkey-add:disabled { opacity: 0.6; cursor: default; }
.passkey-add svg { width: 17px; height: 17px; }

.passkey-label-input {
  flex: 1 1 240px;
  min-width: 0;
  font: inherit;
  font-size: 0.9rem;
  padding: 0.55rem 0.8rem;
  color: var(--text);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.passkey-message {
  margin: 0 0 1.2rem;
  padding: 0.65rem 0.85rem;
  font-size: 0.87rem;
  border-radius: var(--radius-sm);
}

.passkey-message.is-info { color: var(--text-secondary); background: var(--bg-subtle); }
.passkey-message.is-ok { color: var(--green); background: var(--green-soft); }
.passkey-message.is-error { color: var(--red); background: var(--red-soft); }

.passkey-subhead {
  margin: 1.8rem 0 0.6rem;
  font-size: 1rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border);
}

.passkey-list { list-style: none; margin: 0; padding: 0; }
.passkey-list--muted { opacity: 0.7; }

.passkey-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem 0.9rem;
  margin-bottom: 0.4rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.passkey-item-main { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.9rem; }
.passkey-item-main .muted { font-size: 0.76rem; }

.passkey-remove {
  flex: none;
  padding: 0.35rem 0.7rem;
  font: inherit;
  font-size: 0.8rem;
  color: var(--red);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.passkey-remove:hover:not(:disabled) { border-color: var(--red); background: var(--red-soft); }
.passkey-empty { font-size: 0.9rem; }

.passkey-env {
  width: 100%;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.76rem;
  line-height: 1.5;
  padding: 0.7rem 0.85rem;
  color: var(--text-secondary);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  resize: vertical;
}

.passkey-copy {
  margin-top: 0.5rem;
  padding: 0.4rem 0.9rem;
  font: inherit;
  font-size: 0.82rem;
  color: var(--text-secondary);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.passkey-copy:hover { border-color: var(--accent); color: var(--accent); }

