/* ============================ the seven secrets ==========================
   Nothing here runs on load. Each block wakes up only when its secret does.

   Two rules hold everywhere below:
     · every secret's trigger clears a 44 px thumb, even when the thing you
       see is a 20 px glyph — the hit area is grown with a pseudo-element so
       the layout never notices,
     · reduced motion removes the MOVEMENT, never the secret. You still get
       the colour, the text and the sound; nothing flies about.

   THE MOTION SWITCH IS AN ATTRIBUTE, not a media query: every suppression rule
   below reads `:root[data-motion="reduce"]`, which js/core/util.js writes from
   the OS preference OR the diagnostic panel's override. A media query cannot
   see that override, and half of one — the script obeying it while the
   stylesheets ignore it — is worse than none, because the effects then get
   built and immediately hidden.
   ========================================================================= */

/* the 7/7 seal is a third child of the stub — let it drop to its own line */
.pass-stub { flex-wrap: wrap; }

/* things you can tap that don't look like buttons */
#fromCode, #toCode, .port-flag, .route-plane {
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  position: relative;
}
/* the flag is 22 px of emoji; this is the other 22 px of thumb */
.port-flag::after, .route-plane::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 46px; height: 46px;
  transform: translate(-50%, -50%);
}

/* ---- the map ----------------------------------------------------------- */
.map-stage { position: relative; }
/* pan-y, not none: a thumb swiping up the page over the map must still
   scroll it. Only horizontal drags belong to secret 5. */
.map-stage .map-svg { touch-action: pan-y; }
#planeg { cursor: grab; }
.map-stage.grabbing #planeg { cursor: grabbing; }
#stars circle { fill: #FFF; }
#routeFull, #routeFlown, #ocean, #stars, #planet, #cdgMark, #land {
  transition: fill 500ms ease, stroke 500ms ease, opacity 500ms ease, d 700ms ease;
}

/* ---- secret 1 · to space ---------------------------------------------- */
.pass.offworld { --mx: #C9723B; }
/* "588 000 000 km" must not shove MEX and CDG off the card */
.route-km { max-width: 34vw; overflow: hidden; text-overflow: ellipsis; }
/* The note doubles as the way home: tap it and you are on the Paris flight
   again, without cycling all the way round the solar system. */
.world-note {
  grid-column: 1 / -1;
  margin: 0;
  padding: 10px clamp(14px, 3.6vw, 26px);
  min-height: 44px;
  width: 100%;
  font: inherit;
  font-size: 11.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--pass-soft);
  background: color-mix(in srgb, var(--mx) 10%, transparent);
  border: 0;
  border-top: 1px dashed var(--pass-rule);
  text-align: center;
  cursor: pointer;
  touch-action: manipulation;
}
.world-note[hidden] { display: none; }
.world-note b { color: var(--mx); }
.world-note .back {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 9px;
  border: 1px solid currentColor;
  border-radius: 999px;
  color: var(--pass-ink);
  font-weight: 800;
}

/* ---- secret 2 · the X-ray flip ---------------------------------------- */
.pass { transform-style: preserve-3d; }
.pass.flip { transition: transform 380ms cubic-bezier(.45, 0, .2, 1); }
.xray {
  position: absolute;
  inset: 0;
  z-index: 6;
  overflow: hidden;
  background: #04121D;
  color: #9FE6FF;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 3vw, 18px);
  padding: 18px;
  text-align: center;
}
.xray[hidden] { display: none; }
.xray::before {                     /* scanner raster */
  content: "";
  position: absolute; inset: 0;
  background: repeating-linear-gradient(0deg,
    rgba(120, 220, 255, .07) 0 2px, transparent 2px 5px);
  pointer-events: none;
}
.xray-head {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: #5FD0F5;
}
.xray-items {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: clamp(10px, 4vw, 26px);
  font-size: clamp(30px, 10vw, 52px);
  line-height: 1;
}
/* any emoji, turned into a glowing white silhouette */
.xray-items span {
  position: relative;
  filter: brightness(0) invert(1) drop-shadow(0 0 7px #6FD6FF);
  opacity: 0;
  transition: opacity 200ms ease;
}
.xray-items span.seen { opacity: .92; }
.xray-items span.suspect {
  outline: 2px solid #FF5A4E;
  outline-offset: 6px;
  border-radius: 4px;
  filter: brightness(0) invert(1) drop-shadow(0 0 9px #FF5A4E);
  animation: suspect 500ms steps(1) infinite;
}
@keyframes suspect { 50% { outline-color: transparent; } }
.xray-foot { font-size: 11.5px; color: #6FB6D0; letter-spacing: .06em; max-width: 30ch; min-height: 2.4em; }
.xray-foot.clear {
  color: #6BE39A;
  font-weight: 800;
  letter-spacing: .18em;
  text-transform: uppercase;
  border: 2px solid #6BE39A;
  border-radius: 6px;
  padding: 4px 12px;
  transform: rotate(-6deg);
  animation: stampin 420ms cubic-bezier(.2, 1.7, .4, 1);
}
@keyframes stampin {
  from { transform: rotate(-6deg) scale(2.2); opacity: 0; }
  to   { transform: rotate(-6deg) scale(1);   opacity: 1; }
}
.xray-bar {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, #8FE9FF, transparent);
  box-shadow: 0 0 16px 3px rgba(120, 225, 255, .55);
  animation: xraysweep 1.5s linear infinite;
}
@keyframes xraysweep { from { top: -2%; } to { top: 102%; } }

/* ---- secret 3 · the boarding call -------------------------------------- */
.laserline {
  position: fixed;
  z-index: 70;
  height: 3px;
  background: linear-gradient(90deg, transparent, #FF3B30, #FF7A70, #FF3B30, transparent);
  box-shadow: 0 0 14px 3px rgba(255, 60, 45, .7);
  pointer-events: none;
  animation: laser 1.15s cubic-bezier(.5, 0, .5, 1);
}
@keyframes laser {
  0%   { transform: translateY(-14px); opacity: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translateY(52px); opacity: 0; }
}
.chip-gate.calling {
  animation: gateflash 640ms steps(1) infinite;
}
@keyframes gateflash {
  50% { background: var(--ok); color: #04220F; }
}
.ticker {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 65;
  background: var(--ink);
  color: var(--pass);
  font-size: clamp(11px, 3.2vw, 13px);
  font-weight: 800;
  letter-spacing: .22em;
  text-transform: uppercase;
  padding: 8px 0;
  padding-top: calc(8px + env(safe-area-inset-top));
  overflow: hidden;
  white-space: nowrap;
  border-bottom: 2px solid var(--mx);
}
.ticker[hidden] { display: none; }
/* Driven by script, not by a CSS animation: a keyframe animation cannot be
   replayed reliably and is switched off entirely under reduced-motion, which
   is exactly how this ended up as an empty black bar the first time. */
.ticker i {
  display: inline-block;
  font-style: normal;
  will-change: transform;
}
.ticker.still {                     /* the reduced-motion version: just read it */
  text-align: center;
}
.ticker.still i { transform: none !important; }

/* ---- secret 4 · turbulence -------------------------------------------- */
/* Shake the cards, never <body> — a transform on body would break every
   position:fixed child, the masks and the ticker included. */
.turb .pass, .turb .map-card, .turb .lounge, .turb header.strip, .turb footer.strip-foot {
  animation: shake 320ms linear infinite;
}
@keyframes shake {
  0%   { transform: translate(0, 0) rotate(0deg); }
  20%  { transform: translate(-2px, 1px) rotate(-.22deg); }
  40%  { transform: translate(2px, -1px) rotate(.26deg); }
  60%  { transform: translate(-1px, -2px) rotate(-.16deg); }
  80%  { transform: translate(2px, 1px) rotate(.2deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}
.chip-status.alarm { color: var(--alert); }
.chip-status.alarm::after { content: " ⚠"; }

.masks {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 58;
  display: flex;
  justify-content: space-evenly;
  pointer-events: none;
}
.mask {
  display: flex;
  flex-direction: column;
  align-items: center;
  transform-origin: top center;
  animation: maskdrop 620ms cubic-bezier(.25, 1.5, .45, 1) both,
             maskswing 2.4s 620ms ease-in-out infinite;
}
.mask .cord {
  width: 2px;
  height: clamp(34px, 8vh, 74px);
  background: var(--ink-faint);
}
.mask .cup {
  width: 34px; height: 26px;
  border-radius: 46% 46% 50% 50%;
  background: #F0D68A;
  border: 2px solid #B99A45;
  box-shadow: inset 0 -5px 7px -5px rgba(0,0,0,.5);
}
@keyframes maskdrop { from { transform: translateY(-130%); } to { transform: translateY(0); } }
@keyframes maskswing {
  0%, 100% { rotate: -4deg; }
  50%      { rotate: 4deg; }
}
.seatbelt {
  position: fixed;
  top: calc(10px + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 59;
  background: #14171C;
  color: #F2C14E;
  border: 2px solid #F2C14E;
  border-radius: 6px;
  padding: 6px 14px;
  font-size: clamp(10px, 3vw, 12px);
  font-weight: 800;
  letter-spacing: .2em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
  animation: signblink 880ms steps(1) infinite;
}
@keyframes signblink { 50% { opacity: .35; } }

/* ---- secret 5 · time travel ------------------------------------------- */
.scrubbing .details, .scrubbing .route { filter: blur(1.7px); opacity: .7; }
.scrubbing .count { filter: saturate(.35); }
.scrub-note {
  position: absolute;
  left: 50%;
  bottom: 8px;
  transform: translateX(-50%);
  z-index: 4;
  background: var(--ink);
  color: var(--pass);
  font-size: clamp(10px, 3vw, 12px);
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: 5px 12px;
  border-radius: 999px;
  white-space: nowrap;
  pointer-events: none;
}
.scrub-note[hidden] { display: none; }

/* Pulled past the start of the wait or past the landing. The date stops
   changing on its own, so without a cue the drag just feels broken — this says
   "there is nothing there" instead. */
.map-stage.at-limit .scrub-note {
  background: var(--warn);
  color: #1B1000;
  animation: atlimit 460ms ease-out;
}
.map-stage.at-limit .route-line,
.map-stage.at-limit #routeFlown { filter: saturate(.4); }
@keyframes atlimit {
  0%   { transform: translateX(-50%) scale(1); }
  35%  { transform: translateX(-50%) scale(1.09); }
  100% { transform: translateX(-50%) scale(1); }
}
:root[data-motion="reduce"] .map-stage.at-limit .scrub-note { animation: none; }

/* ---- secret 6 · change of colours -------------------------------------- */
body { transition: background-color 700ms ease; }
body.nat-mx {
  --mx: #006847; --fr: #CE1126; --accent: #006847;
  --sky: #E9F3EC; --sky-2: #F4FAF5; --sky-3: #D6E9DB;
}
body.nat-fr {
  --mx: #0055A4; --fr: #EF4135; --accent: #0055A4;
  --sky: #E8EEF7; --sky-2: #F3F7FC; --sky-3: #D4E0F0;
}
@media (prefers-color-scheme: dark) {
  body.nat-mx:not([data-theme="light"]) { --sky: #061109; --sky-2: #0A1A0F; --sky-3: #0C2113; }
  body.nat-fr:not([data-theme="light"]) { --sky: #040C18; --sky-2: #08142A; --sky-3: #0A1930; }
}
/* Three bands wipe down the screen in sequence, hold with the emblem, then
   wipe away upward. A title card, rather than a rectangle that shrinks. */
.bands {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  pointer-events: none;
}
.bands i {
  flex: 1 1 0;
  transform: translateY(-101%);
  animation: bandin 380ms cubic-bezier(.3, .8, .2, 1) forwards,
             bandout 420ms cubic-bezier(.6, 0, .8, .4) 1150ms forwards;
}
@keyframes bandin  { to { transform: translateY(0); } }
@keyframes bandout { from { transform: translateY(0); } to { transform: translateY(-101%); } }
.bands em {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: clamp(58px, 22vw, 130px);
  font-style: normal;
  line-height: 1;
  opacity: 0;
  animation: emblem 1100ms ease-out 340ms forwards;
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, .35));
}
@keyframes emblem {
  0%   { opacity: 0; transform: scale(.5) rotate(-14deg); }
  35%  { opacity: 1; transform: scale(1)  rotate(0deg); }
  75%  { opacity: 1; transform: scale(1)  rotate(0deg); }
  100% { opacity: 0; transform: scale(1.4) rotate(6deg); }
}
.greet {
  font-weight: 700;
  letter-spacing: .1em;
  opacity: .8;
  border-left: 1px solid color-mix(in srgb, var(--pass) 40%, transparent);
  padding-left: 10px;
}
.greet[hidden] { display: none; }
.confetti {
  position: fixed;
  top: -24px;
  z-index: 57;
  width: 8px; height: 14px;
  pointer-events: none;
  animation: fall linear forwards;
}
@keyframes fall {
  from { transform: translateY(0) rotate(0deg); opacity: 1; }
  to   { transform: translateY(108vh) rotate(760deg); opacity: .85; }
}

/* ---- secret 7 · the airshow --------------------------------------------
   The visible payoff is the full-screen fly-past built in script; all this
   does is get the little glyph out of the way so the launch reads.          */
.route-plane { display: inline-block; }
.route-plane.launch { animation: launch 850ms cubic-bezier(.3, 0, .4, 1); }
@keyframes launch {
  0%   { transform: translate(0, 0) rotate(0deg) scale(1); opacity: 1; }
  30%  { transform: translate(10px, -8px) rotate(-18deg) scale(1.5); opacity: 1; }
  60%  { transform: translate(46px, -30px) rotate(-16deg) scale(.6); opacity: 0; }
  61%  { transform: translate(-30px, 14px) rotate(0deg) scale(.6); opacity: 0; }
  100% { transform: translate(0, 0) rotate(0deg) scale(1); opacity: 1; }
}
.smoke {
  position: absolute;
  left: 50%; top: 50%;
  width: 5px; height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: var(--pass-soft);
  pointer-events: none;
  animation: puff 950ms ease-out forwards;
}
@keyframes puff {
  from { opacity: .5;  transform: scale(1); }
  to   { opacity: 0;   transform: scale(5); }
}
.shock {
  position: absolute;
  left: 50%; top: 50%;
  width: 8px; height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  border: 2px solid var(--mx);
  pointer-events: none;
  animation: shock 620ms cubic-bezier(.2, .8, .3, 1) forwards;
}
@keyframes shock {
  from { opacity: .9; transform: scale(1); }
  to   { opacity: 0;  transform: scale(11); border-width: 1px; }
}

/* ---- 7 / 7 · the flight departs ----------------------------------------
   A sequence, not a single effect: the card pushes back from the gate, an
   aircraft crosses the whole screen, the sky flashes, the stamp lands and the
   colours of both countries come down. Roughly nine seconds end to end.     */
.pass.pushback {
  animation: pushback 2200ms cubic-bezier(.4, 0, .3, 1) both;
}
@keyframes pushback {
  0%   { transform: perspective(1600px) rotateX(0deg) translateY(0) scale(1); }
  40%  { transform: perspective(1600px) rotateX(7deg) translateY(6px) scale(.985); }
  100% { transform: perspective(1600px) rotateX(0deg) translateY(0) scale(1); }
}
.pass.airborne {
  animation: airborne 2600ms cubic-bezier(.35, 0, .25, 1) both;
  box-shadow: 0 40px 70px -40px rgba(16, 32, 52, .8), var(--shadow);
}
@keyframes airborne {
  0%   { transform: perspective(1600px) rotateX(0deg)  translateY(0)     scale(1); }
  45%  { transform: perspective(1600px) rotateX(-9deg) translateY(-14px) scale(1.02); }
  100% { transform: perspective(1600px) rotateX(0deg)  translateY(0)     scale(1); }
}

.flyby {
  position: fixed;
  left: 0; top: 0;
  z-index: 62;
  font-size: clamp(46px, 14vw, 100px);
  line-height: 1;
  pointer-events: none;
  will-change: transform;
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, .3));
}
.contrail {
  position: fixed;
  z-index: 61;
  height: 5px;
  width: 5px;
  border-radius: 50%;
  background: var(--ink-faint);
  pointer-events: none;
  animation: trailfade 1600ms ease-out forwards;
}
@keyframes trailfade {
  from { opacity: .5; transform: scale(1); }
  to   { opacity: 0;  transform: scale(7); }
}
.skyflash {
  position: fixed;
  inset: 0;
  z-index: 63;
  background: #FFF;
  pointer-events: none;
  animation: skyflash 900ms ease-out forwards;
}
@keyframes skyflash {
  0%   { opacity: 0; }
  12%  { opacity: .82; }
  100% { opacity: 0; }
}
.unit.zeroed .unit-num { color: var(--ok); }

.stamp {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.stamp b {
  border: 4px double #C8A227;
  color: #C8A227;
  background: color-mix(in srgb, #C8A227 10%, transparent);
  padding: 10px clamp(16px, 5vw, 30px);
  border-radius: 10px;
  font-size: clamp(19px, 6.5vw, 34px);
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
  transform: rotate(-13deg);
  animation: stampdown 620ms cubic-bezier(.2, 1.7, .35, 1) both;
}
@keyframes stampdown {
  from { transform: rotate(-13deg) scale(2.6); opacity: 0; }
  to   { transform: rotate(-13deg) scale(1);   opacity: 1; }
}
.seal {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .14em;
  color: #C8A227;
  border: 1px solid #C8A227;
  border-radius: 999px;
  padding: 3px 9px;
  white-space: nowrap;
}
.seal[hidden] { display: none; }
.panel li.done .num { color: #C8A227; }

/* ---- secret −1 · boarding the stub -------------------------------------
   The way in is a CODE, not a count: tap the two printed codes in the
   boarding order — OAX · CDG · CDG · OAX — and the stub arms. Wrong order,
   silent restart. There is NO visual feedback while the code goes in: a
   printed line stays a printed line, each tap is a quiet key-click with one
   pitch per airport, and that is all.

   Armed, the stub stops pretending: the airport two-note chime, a green
   READY TO BOARD badge, a slow glow. Swipe the section left to right and it
   feeds through the reader — the scan light sweeps it, the reader
   double-bips, a stamp comes down — and the hatch is open.

   `-webkit-tap-highlight-color` stays transparent: it SUPPRESSES the mobile
   tap flash the browser would otherwise add on every tap of the code.

   `touch-action: none` on the armed section matters. The stub normally lets
   the page scroll under a thumb; while boarding is live it must not, or a
   left-to-right drag is a scroll gesture and the swipe never reaches the
   reader. */
.stub-codes {
  -webkit-tap-highlight-color: transparent;
  border-radius: 4px;
}

/* armed: the gate is open. The badge reads as airline furniture on purpose —
   a passenger sees theming; only whoever typed the code knows the section
   will now take a swipe. `--scanp` is the reader head, driven by the script
   while the finger drags. */
.pass-stub.boarding {
  touch-action: none;
  z-index: 2;
  transform: translateX(calc(var(--scanp, 0) * 10px));
  animation: stubarmed 1.9s ease-in-out infinite;
}
@keyframes stubarmed {
  0%, 100% { box-shadow: 0 0 0 0 rgba(79, 191, 139, 0); }
  50%      { box-shadow: 0 2px 14px -4px rgba(79, 191, 139, .55); }
}

.stub-badge, .stub-scan, .stub-stamp { display: none; }

.pass-stub.boarding > .stub-badge {
  display: block;
  position: absolute;
  left: 50%; top: -10px;
  transform: translateX(-50%);
  padding: 3px 10px;
  border: 1px solid #4FBF8B;
  border-radius: 999px;
  background: color-mix(in srgb, #4FBF8B 16%, var(--pass-2));
  color: #35A878;
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 9px;
  font-style: normal;
  font-weight: 700;
  letter-spacing: .14em;
  white-space: nowrap;
  pointer-events: none;
  animation: badge-in 420ms cubic-bezier(.2, .9, .3, 1.3) both,
             badge-pulse 1.9s ease-in-out 420ms infinite;
}
@keyframes badge-in {
  0%   { opacity: 0; transform: translateX(-50%) translateY(6px) scale(.8); }
  100% { opacity: 1; transform: translateX(-50%); }
}
@keyframes badge-pulse {
  0%, 100% { filter: none; }
  50%      { filter: brightness(1.22); }
}

/* the reader head: parked at the left edge while armed, riding under the
   finger during the scan */
.pass-stub.boarding > .stub-scan {
  display: block;
  position: absolute;
  top: 2px; bottom: 2px;
  left: calc(var(--scanp, 0) * 96%);
  width: 3px;
  background: linear-gradient(180deg, transparent, #4FBF8B 30%, #4FBF8B 70%, transparent);
  box-shadow: 0 0 12px 3px rgba(79, 191, 139, .5);
  pointer-events: none;
}

/* ---- the section boards --------------------------------------------------
   One keyframe run, out AND home, so a tab hidden halfway through leaves
   nothing stuck mid-animation — the rule the whole site follows. The stub
   feeds right, dims as it sits in the reader, and comes back stamped. */
.pass-stub.boarded {
  animation: boarded 1.6s cubic-bezier(.36, 0, .22, 1) both;
  z-index: 3;
}
@keyframes boarded {
  0%   { transform: none; opacity: 1; }
  18%  { transform: translateX(26px) rotate(.6deg); }
  38%  { transform: translateX(46px); opacity: .8; }
  55%  { transform: translateX(46px); opacity: .8; }
  82%  { transform: translateX(4px); opacity: 1; }
  100% { transform: none; opacity: 1; }
}
/* the white sweep of the read head at the moment it takes the code */
.pass-stub.boarded > .stub-scan {
  display: block;
  position: absolute;
  top: 2px; bottom: 2px;
  left: 0;
  width: 3px;
  background: linear-gradient(180deg, transparent, #FFF 30%, #FFF 70%, transparent);
  box-shadow: 0 0 16px 5px rgba(255, 255, 255, .8);
  pointer-events: none;
  animation: scan-sweep 700ms ease-in-out 140ms both;
}
@keyframes scan-sweep {
  0%   { left: 0; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { left: calc(100% - 3px); opacity: 0; }
}

/* the stamp: green, crooked the way a real one lands, gone a moment later */
.pass-stub.stamped > .stub-stamp {
  display: block;
  position: absolute;
  left: 50%; top: 50%;
  padding: 3px 10px;
  border: 2px solid #35A878;
  border-radius: 6px;
  color: #35A878;
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  font-style: normal;
  font-weight: 800;
  letter-spacing: .18em;
  white-space: nowrap;
  transform: translate(-50%, -50%) rotate(-8deg);
  pointer-events: none;
  z-index: 4;
  animation: stamp-in 2.4s ease-out 800ms both;
}
@keyframes stamp-in {
  0%   { opacity: 0; transform: translate(-50%, -50%) rotate(-8deg) scale(1.8); }
  7%   { opacity: 1; transform: translate(-50%, -50%) rotate(-8deg) scale(.96); }
  11%  { transform: translate(-50%, -50%) rotate(-8deg) scale(1.02); }
  15%  { transform: translate(-50%, -50%) rotate(-8deg) scale(1); }
  78%  { opacity: 1; }
  100% { opacity: 0; }
}

/* boarding still happens and still sounds; it simply does not travel */
:root[data-motion="reduce"] .pass-stub.boarding { animation: none; transform: none; }
:root[data-motion="reduce"] .pass-stub.boarding > .stub-badge { animation: none; }
:root[data-motion="reduce"] .pass-stub.boarded { animation: none; }
:root[data-motion="reduce"] .pass-stub.boarded > .stub-scan { display: none; }
:root[data-motion="reduce"] .pass-stub.stamped > .stub-stamp { animation: stamp-calm 900ms ease-out both; }
@keyframes stamp-calm {
  0%       { opacity: 0; }
  25%, 75% { opacity: 1; }
  100%     { opacity: 0; }
}

/* ---- secret 0 · the hidden panel, and −1 · the diagnostic one ----------- */
.panel { font-size: 11.5px; line-height: 1.65; }
.panel pre { margin: 0 0 10px; white-space: pre-wrap; overflow-x: auto; font: inherit; }
.panel ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }
.panel li { display: flex; gap: 10px; align-items: baseline; }
.panel .num {
  flex: 0 0 auto;
  width: 1.2em;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.panel .nm { font-weight: 700; }
.panel .tr { color: var(--ink-faint); }
.panel li.got .num, .panel li.got .nm { color: var(--ok); }
.panel li.not { opacity: .55; }
.panel h3 {
  margin: 12px 0 6px;
  font-size: 10.5px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* the second tier: the eighteen hidden in the games. Same idiom as the list of
   seven above it, plus the game each belongs to — that is the only clue given. */
.pan-note {
  margin: 0 0 8px;
  color: var(--ok);
  max-width: 46ch;
}
.pan-games .num { color: var(--ink-faint); }
.pan-games li.got .num { color: #C8A227; }
.pan-games .gm {
  flex: 0 0 auto;
  min-width: 9.5em;
  color: var(--ink-faint);
  letter-spacing: .04em;
}
/* the game name is useful next to a found egg and just clutter next to ??? */
.pan-games li.not .gm { opacity: .8; }
@media (max-width: 480px) {
  .dbg-games li { flex-wrap: wrap; }
  .dbg-games .gm { min-width: 0; }
}

/* the switches. Destructive ones are red and sit last, so a thumb reaching
   for "clear scores" cannot land on "clear everything" by mistake. */
.dbg-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.dbg-btn {
  font: inherit;
  padding: 9px 11px;
  min-height: 44px;                 /* the house thumb target, same as the rest */
  border: 1px solid var(--pass-rule);
  border-radius: 8px;
  background: color-mix(in srgb, var(--pass-ink) 6%, transparent);
  color: var(--pass-ink);
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease;
}
.dbg-btn:hover { background: color-mix(in srgb, var(--pass-ink) 12%, transparent); }
.dbg-btn:active { transform: translateY(1px); }
.dbg-btn.danger {
  border-color: color-mix(in srgb, var(--alert) 55%, transparent);
  color: var(--alert);
}
.dbg-btn.danger:hover { background: color-mix(in srgb, var(--alert) 14%, transparent); }
.dbg-flash {
  margin: 8px 0 0;
  color: var(--ok);
  font-weight: 700;
}

/* ---------------------------------------------------------------- calm --- */
/* Reduced motion takes away the movement, not the secret. Everything above
   still fires, still counts and still makes its noise; it simply arrives
   already in place instead of flying in. */
/* ---------------------------------------------------------------- calm ---
   A secret you deliberately triggered is content you asked for, so it still
   plays. What reduced motion switches off here is the involuntary stuff: the
   page shaking under you, and the animations that loop forever.
   ------------------------------------------------------------------------- */
/* looping, ambient, never-ending — these are the genuinely unpleasant ones */
:root[data-motion="reduce"] :is(
  .xray-bar, .xray-items span.suspect, .chip-gate.calling, .seatbelt,
  .turb .pass, .turb .map-card, .turb .lounge,
  .turb header.strip, .turb footer.strip-foot) {
  animation: none !important;
}
/* drops, then holds still */
:root[data-motion="reduce"] .mask { animation: maskdrop 620ms ease-out both; }
:root[data-motion="reduce"] :is(#routeFull, #routeFlown, #ocean, #stars, #planet, #cdgMark, #land) {
  transition: none;
}
:root[data-motion="reduce"] .feed { scroll-behavior: auto; }


