/* ChronoShell stage layer — see CLAUDE_CODE_BRIEF.md
   Loaded AFTER style.css. Everything here is scoped to .hero-stage so the
   modern era face and the rest of the site are untouched. */

/* The stage is deliberately wider than the viewport — it covers rather than
   letterboxes — so the document must be told not to grow to fit it. On a
   390px phone the stage computes to 1387px, which gave the whole page a
   horizontal scrollbar and left the front door sitting off to one side.
   `clip` rather than `hidden`: hidden would make <html> a scroll container
   and break `position: sticky` elsewhere. */
/* Both, and it has to be both. style.css already puts `overflow: hidden` on
   body, and that is not enough: the stage covers rather than letterboxes and
   computes to 1387px on a 390px phone, which grew the document to 889 and gave
   the front door a horizontal scrollbar to hide behind. Measured — `body`
   alone leaves scrollWidth at 889.

   present.css does NOT do this, deliberately: clipping the root there while
   fixed animated layers sat inside it left Chrome holding a stale narrow paint
   of the page behind the live one. Nothing on that page overflows any more, so
   it needs no clip at all. This page genuinely has something wider than the
   window in it and has no such choice. */
/* The root is NOT clipped. Clipping <html> while fixed, animated layers live
   inside it leaves Chrome holding a stale narrow paint of the page behind the
   live one — the same artifact that had to be removed from the 2026 deck, and
   it came back here as a thin strip down the left edge of the loading screen.

   `.hero-frame` contains the stage instead. `overflow: clip` on a
   non-transformed ancestor does not clip fixed-position descendants, so the
   expanded shell is untouched. */
body { overflow-x: clip; }

.hero-frame {
  position: relative;
  /* The frame has to CENTRE the stage, not merely contain it. <body> is the
     flex box that used to do that, and wrapping the stage in this made the
     frame its flex child instead: the frame filled the viewport, the stage sat
     at its left edge, and the whole 16:9 overflow went off the right-hand side
     in one piece. Everything welded to the frame by percentage - the terminal
     above all - then sat off-centre against a brass panel that had been
     cropped from one side only. Same three properties as <body>. */
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: clip;
}

/* This stylesheet inherits the legacy sheet's box model, which is content-box.
   That is why `width: min(30rem, 100%)` plus `padding-inline: 1.1rem` came out
   34px wider than the phone and the door hung off both edges. Scoped to the
   door rather than set globally: the 1888 stage's measured overlays are laid
   out in the old box model and must stay there. */
#cs-warp,
#cs-warp *,
#cs-warp *::before,
#cs-warp *::after { box-sizing: border-box; }

/* ---------- Time-transit loader ----------
   Covers the hero video download. Styled in this file but positioned `fixed`
   and outside the stage on purpose: it must be correct before the stage exists.
   Everything is CSS — no images, no library — so it paints on the first frame. */
#cs-warp {
  position: fixed;
  inset: 0;
  z-index: 9999;
  /* One stacking context for the whole door. Without it the galaxy canvas, the
     vortex, the nebula and the copy each composite against whatever is behind
     them, and a stale paint from the layer below can show through as a seam. */
  isolation: isolate;
  display: grid;
  /* An `auto` column sizes to its item's max-content, so a long heading made
     the column wider than the phone and the panel hung off both edges. A
     minmax(0, 100%) column cannot exceed the viewport, whatever is in it. */
  grid-template-columns: minmax(0, 100%);
  place-items: center;
  overflow: hidden;
  background: radial-gradient(ellipse at center, #10161f 0%, #07090c 62%, #04050700 100%), #07090c;
  transition: opacity .7s ease, visibility .7s;
}

#cs-warp.gone { opacity: 0; visibility: hidden; pointer-events: none; }

/* --- the front door ---
   A game main-menu, not a web form: liquid aurora behind, two glass panels in
   front, a sheen that travels across the one you are pointing at. All CSS, so
   it costs nothing on a screen that is already waiting for a video. */
#cs-warp::before {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(38% 34% at 22% 30%, rgba(143, 211, 255, .22), transparent 70%),
    radial-gradient(34% 30% at 78% 26%, rgba(226, 189, 118, .20), transparent 70%),
    radial-gradient(46% 40% at 62% 78%, rgba(120, 90, 200, .18), transparent 70%),
    radial-gradient(30% 26% at 18% 76%, rgba(80, 200, 190, .14), transparent 70%);
  filter: blur(30px) saturate(120%);
  animation: cs-aurora 22s ease-in-out infinite alternate;
}

@keyframes cs-aurora {
  0%   { transform: translate3d(-2%, -1%, 0) scale(1); }
  50%  { transform: translate3d(3%, 2%, 0) scale(1.08) rotate(2deg); }
  100% { transform: translate3d(-1%, 3%, 0) scale(1.04) rotate(-2deg); }
}

.cs-gate {
  position: relative;
  z-index: 4;
  width: min(70rem, 90vw);
  text-align: center;
}

.cs-gate-eyebrow {
  margin: 0 0 1rem;
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: clamp(9px, .72vw, 11px);
  letter-spacing: .42em;
  text-transform: uppercase;
  color: rgba(143, 211, 255, .75);
}

.cs-gate-title {
  margin: 0 0 clamp(1.6rem, 4vh, 3rem);
  font-family: "IM Fell English", Georgia, serif;
  font-weight: 400;
  font-size: clamp(30px, 5vw, 62px);
  line-height: 1.02;
  color: #fff6e4;
  text-shadow: 0 0 40px rgba(143, 211, 255, .25);
}

.cs-gate-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: clamp(1rem, 2.4vw, 1.8rem);
}

.cs-gate-card {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: clamp(1.4rem, 3vw, 2.2rem);
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: #f6ecd8;
  border: 0;
  border-radius: 20px;
  background: linear-gradient(160deg, rgba(255, 255, 255, .13), rgba(255, 255, 255, .04) 42%, rgba(255, 255, 255, .02));
  backdrop-filter: blur(18px) saturate(150%);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .35),      /* the lit top edge of glass */
    inset 0 0 0 1px rgba(255, 255, 255, .10),
    0 22px 50px rgba(0, 0, 0, .55);
  transition: transform .5s cubic-bezier(.22, .9, .3, 1), box-shadow .5s ease;
}

/* the travelling sheen — this is the "liquid" part */
.cs-gate-card::before {
  content: "";
  position: absolute;
  inset: -40% -60%;
  z-index: -1;
  background: linear-gradient(115deg, transparent 42%, rgba(255, 255, 255, .22) 50%, transparent 58%);
  transform: translateX(-70%);
  transition: transform 1.1s cubic-bezier(.22, .9, .3, 1);
}

/* the era's own colour, pooled in the corner and spreading on hover */
.cs-gate-card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(120% 90% at 12% 8%, rgba(226, 189, 118, .30), transparent 62%);
  opacity: .5;
  transition: opacity .5s ease, transform .8s ease;
}

.cs-gate-card[data-go="present"]::after {
  background: radial-gradient(120% 90% at 12% 8%, rgba(143, 211, 255, .32), transparent 62%);
}

.cs-gate-card:hover,
.cs-gate-card:focus-visible {
  transform: translateY(-8px) scale(1.012);
  outline: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .5),
    inset 0 0 0 1px rgba(255, 255, 255, .18),
    0 30px 70px rgba(0, 0, 0, .6),
    0 0 60px rgba(226, 189, 118, .22);
}

.cs-gate-card[data-go="present"]:hover,
.cs-gate-card[data-go="present"]:focus-visible {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .5),
    inset 0 0 0 1px rgba(255, 255, 255, .18),
    0 30px 70px rgba(0, 0, 0, .6),
    0 0 60px rgba(143, 211, 255, .24);
}

.cs-gate-card:hover::before,
.cs-gate-card:focus-visible::before { transform: translateX(70%); }

.cs-gate-card:hover::after,
.cs-gate-card:focus-visible::after { opacity: 1; transform: scale(1.15); }

.cs-gate-card:active { transform: translateY(-3px) scale(.998); }

/* the year, set large and low-contrast like a HUD plate number */
.cs-gate-year {
  display: block;
  font-family: ui-monospace, monospace;
  font-size: clamp(34px, 5vw, 58px);
  line-height: 1;
  letter-spacing: .04em;
  color: rgba(255, 246, 228, .92);
  text-shadow: 0 0 30px rgba(226, 189, 118, .45);
  margin-bottom: clamp(.6rem, 1.4vh, 1rem);
}

.cs-gate-card[data-go="present"] .cs-gate-year { text-shadow: 0 0 30px rgba(143, 211, 255, .5); }

.cs-gate-card b {
  display: block;
  font-family: "IM Fell English", Georgia, serif;
  font-weight: 400;
  font-size: clamp(18px, 1.9vw, 27px);
  line-height: 1.2;
  margin-bottom: .6rem;
}

.cs-gate-note {
  display: block;
  font-family: "IM Fell English", Georgia, serif;
  font-size: clamp(12.5px, 1.02vw, 15px);
  line-height: 1.62;
  color: rgba(246, 236, 216, .68);
  margin-bottom: clamp(1rem, 2.4vh, 1.6rem);
}

.cs-gate-cta {
  display: inline-flex;
  align-items: center;
  gap: .5em;
  padding: .55em 1.1em;
  border-radius: 999px;
  font-family: ui-monospace, monospace;
  font-size: clamp(9.5px, .78vw, 11.5px);
  letter-spacing: .24em;
  text-transform: uppercase;
  color: #0c0a06;
  background: linear-gradient(180deg, #ffe6b0, #d9b46a);
  box-shadow: 0 6px 18px rgba(226, 189, 118, .3);
  transition: transform .4s ease, box-shadow .4s ease;
}

.cs-gate-card[data-go="present"] .cs-gate-cta {
  background: linear-gradient(180deg, #cfeaff, #7fc4f2);
  box-shadow: 0 6px 18px rgba(143, 211, 255, .3);
}

.cs-gate-card:hover .cs-gate-cta { transform: translateX(4px); }

.cs-gate-small {
  margin: clamp(1rem, 2.6vh, 1.8rem) 0 0;
  font-family: ui-monospace, monospace;
  font-size: clamp(9px, .72vw, 11px);
  letter-spacing: .12em;
  color: rgba(246, 236, 216, .38);
  min-height: 1.2em;
}

/* --- the phone -----------------------------------------------------------
   `#cs-warp` is `overflow: hidden` because the tunnel behind it must not be
   scrollable, so the front door carries its own scroll instead of relying on
   the page. `svh` rather than `vh`: on iOS Safari `vh` is the *expanded*
   viewport, which put the second card under the address bar. --- */
.cs-gate {
  max-height: 100svh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: max(1.2rem, env(safe-area-inset-top)) .25rem max(1.2rem, env(safe-area-inset-bottom));
}

@media (max-width: 820px) {
  .cs-gate { width: min(34rem, 92vw); }
  .cs-gate-eyebrow { font-size: 10px; letter-spacing: .3em; }
  .cs-gate-title { font-size: clamp(26px, 7.6vw, 40px); margin-bottom: 1.4rem; }
  .cs-gate-cards { gap: .8rem; }
  .cs-gate-card { padding: 1.15rem 1.2rem; border-radius: 16px; }
  .cs-gate-year { font-size: clamp(24px, 8vw, 34px); }
  .cs-gate-note { font-size: .86rem; }
  /* no hover on a touch screen, so the sheen and the lift only cost paint */
  .cs-gate-card::before { display: none; }
}

/* the refusal shown when the past is chosen on a phone */
.cs-gate-notice {
  display: grid;
  justify-items: center;
  gap: .9rem;
  width: min(34rem, 100%);
  padding-inline: 1.25rem;
  text-align: center;
}
/* the heading here is a sentence, not a two-word title, so it needs its own
   (smaller) scale rather than the door's */
.cs-gate-notice .cs-gate-title {
  font-size: clamp(21px, 5.4vw, 34px);
  line-height: 1.15;
  margin-bottom: .2rem;
  text-wrap: balance;
}
.cs-gate-notice .cs-gate-note { max-width: 34ch; text-align: center; }
.cs-gate-go {
  display: inline-block;
  margin-top: .6rem;
  padding: .7rem 1.4rem;
  border-radius: 999px;
  text-decoration: none;
  color: #08111d;
  background: linear-gradient(120deg, #8fd3ff, #b9a6ff);
  box-shadow: 0 10px 28px rgba(143, 211, 255, .3);
}

/* short screens: no scrollbars, just tighter */
@media (max-height: 620px) {
  .cs-gate-title { margin-bottom: 1rem; }
  .cs-gate-note { display: none; }
  .cs-gate-year { font-size: clamp(26px, 4vw, 38px); }
}

/* The pointer response used to be switched off here under reduce-motion.
   `.cs-gate-card:hover { transform: none }` outranks the second-pass tilt
   rule by one pseudo-class, so it killed the tilt at exactly the moment it
   exists. The door is toned down through `.calm` instead — see the bottom of
   this file. */

/* `:not([hidden])` matters: a bare `display: grid` here beat the hidden
   attribute, so the tunnel copy, the year bar and the skip button all bled
   through behind the front door. */
.cs-warp-run:not([hidden]) { position: absolute; inset: 0; display: grid; place-items: center; }
[hidden] { display: none !important; }

/* receding brass rings — ten elements on one keyframe, staggered */
.cs-warp-tunnel {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  perspective: 620px;
}

.cs-warp-tunnel span {
  position: absolute;
  width: 78vmin;
  height: 78vmin;
  border-radius: 50%;
  border: 2px solid rgba(226, 189, 118, .5);
  box-shadow: 0 0 46px rgba(120, 190, 255, .18), inset 0 0 60px rgba(226, 189, 118, .10);
  animation: cs-warp-ring 3.6s linear infinite;
  opacity: 0;
}

.cs-warp-tunnel span:nth-child(1) { animation-delay: 0s; }
.cs-warp-tunnel span:nth-child(2) { animation-delay: -.36s; }
.cs-warp-tunnel span:nth-child(3) { animation-delay: -.72s; }
.cs-warp-tunnel span:nth-child(4) { animation-delay: -1.08s; }
.cs-warp-tunnel span:nth-child(5) { animation-delay: -1.44s; }
.cs-warp-tunnel span:nth-child(6) { animation-delay: -1.8s; }
.cs-warp-tunnel span:nth-child(7) { animation-delay: -2.16s; }
.cs-warp-tunnel span:nth-child(8) { animation-delay: -2.52s; }
.cs-warp-tunnel span:nth-child(9) { animation-delay: -2.88s; }
.cs-warp-tunnel span:nth-child(10) { animation-delay: -3.24s; }

/* rings rush toward the viewer: small and far, then past the camera */
@keyframes cs-warp-ring {
  0%   { transform: translateZ(-1500px) rotate(0deg); opacity: 0; }
  12%  { opacity: .85; }
  85%  { opacity: .5; }
  100% { transform: translateZ(420px) rotate(24deg); opacity: 0; }
}

/* the artifact, on top of the CSS fallback */
.cs-warp-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  z-index: 2;
  background: transparent;
  /* it is decoration during a wait, not something to tab into or scroll */
  pointer-events: none;
}

.cs-warp-copy {
  position: relative;
  text-align: center;
  color: #f4e2be;
  padding: 0 6vw;
}

.cs-warp-years {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .8em;
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: clamp(13px, 1.5vw, 22px);
  letter-spacing: .22em;
  color: #8fd3ff;
}

.cs-warp-years b { font-weight: 500; }
.cs-warp-years b:last-child { color: #e2bd76; }

/* the travelling dash between the two years */
.cs-warp-years i {
  width: clamp(40px, 8vw, 120px);
  height: 1px;
  background: linear-gradient(90deg, #8fd3ff, #e2bd76);
  position: relative;
  overflow: visible;
}

.cs-warp-years i::after {
  content: "";
  position: absolute;
  top: -2px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #fff3d6;
  box-shadow: 0 0 10px #ffd98a;
  animation: cs-warp-dot 1.9s ease-in-out infinite;
}

@keyframes cs-warp-dot { 0% { left: 0; } 100% { left: 100%; } }

.cs-warp-line {
  margin: .9em 0 1.1em;
  min-height: 2.6em;
  font-family: "IM Fell English", Georgia, serif;
  font-size: clamp(16px, 2vw, 30px);
  line-height: 1.3;
  transition: opacity .35s ease, transform .35s ease;
}

.cs-warp-line.swap { opacity: 0; transform: translateY(6px); }

.cs-warp-bar {
  width: min(320px, 62vw);
  height: 2px;
  margin: 0 auto;
  background: rgba(226, 189, 118, .2);
  overflow: hidden;
}

.cs-warp-bar span {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, #8fd3ff, #e2bd76);
  transition: width .4s ease;
}

/* --- the two doors, shown only once the transit has finished --- */
.cs-warp-choice { margin-top: 1.6em; }

.cs-warp-ask {
  margin: 0 0 1em;
  font-family: "IM Fell English", Georgia, serif;
  font-size: clamp(14px, 1.4vw, 20px);
  color: #ffd98a;
}

.cs-warp-doors {
  display: flex;
  gap: 1em;
  justify-content: center;
  flex-wrap: wrap;
}

.cs-warp-doors button {
  position: relative;
  flex: 0 1 15em;
  padding: 1em 1.1em;
  text-align: left;
  color: #f4e2be;
  background: rgba(226, 189, 118, .06);
  border: 1px solid rgba(226, 189, 118, .42);
  border-radius: 10px;
  cursor: pointer;
  font: inherit;
  transition: background .25s ease, transform .25s ease, box-shadow .25s ease;
}

.cs-warp-doors button:hover,
.cs-warp-doors button:focus-visible {
  background: rgba(226, 189, 118, .14);
  transform: translateY(-2px);
  box-shadow: 0 0 26px rgba(255, 200, 110, .28);
  outline: none;
}

.cs-warp-doors button:focus-visible { outline: 2px solid #ffc45c; outline-offset: 3px; }

.cs-warp-doors b {
  display: block;
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: 1.5em;
  letter-spacing: .1em;
  color: #8fd3ff;
  margin-bottom: .3em;
}

.cs-warp-doors [data-era="past"] b { color: #e2bd76; }

.cs-warp-doors span {
  display: block;
  font-family: "IM Fell English", Georgia, serif;
  font-size: .95em;
  line-height: 1.45;
  opacity: .78;
}

.cs-warp-wip {
  display: inline-block;
  margin-top: .6em;
  padding: .12em .55em;
  font: 700 .62em/1.6 ui-monospace, monospace;
  letter-spacing: .16em;
  text-transform: uppercase;
  font-style: normal;
  color: #0d0a05;
  background: linear-gradient(180deg, #e8c88a, #c9a259);
  border-radius: 999px;
}

/* the ONLY thing that ends the journey early */
.cs-warp-skip {
  position: absolute;
  right: 4vw;
  bottom: 4vh;
  z-index: 3;
  padding: .5em 1em;
  font: 500 clamp(11px, .9vw, 13px)/1 ui-monospace, monospace;
  letter-spacing: .1em;
  color: rgba(244, 226, 190, .6);
  background: rgba(12, 8, 4, .5);
  border: 1px solid rgba(226, 189, 118, .3);
  border-radius: 999px;
  cursor: pointer;
}

.cs-warp-skip:hover { color: #f4e2be; border-color: rgba(226, 189, 118, .7); }

.cs-warp-sub {
  display: block;
  margin-top: 1.1em;
  font-family: ui-monospace, monospace;
  font-size: clamp(9px, .8vw, 12px);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: rgba(244, 226, 190, .45);
}

@media (prefers-reduced-motion: reduce) {
  .cs-warp-tunnel span,
  .cs-warp-years i::after { animation: none; }
  .cs-warp-tunnel span:nth-child(4) { opacity: .5; }
}

/* ---------- §2 shared 16:9 stage ---------- */
.hero-stage {
  position: relative;
  /* Smallest 16:9 box that COVERS the viewport — no bars on normal screens.
     Scaling the whole stage (not just the video) is what keeps every overlay
     locked to the frame: hotspots are % of this box, so they crop with it and
     the terminal stays welded to the glass panel at any size.

     On a 16:9 screen all three terms are equal, so it lands on an exact fit and
     nothing is cropped — identical to before.

     Capped at 1.22x: at most 22% cropped, split evenly 11% top / 11% bottom.
     That budget is set by the two things that must survive — the glass panel
     starts at 24.54% (so 11% off the top is safe) and the journal ends at
     98.15% (so 11% off the bottom clips only the front desk edge). Covers every
     ratio up to ~2.17:1; past that thin bars return rather than eat the frame.

     Nested min/max, NOT clamp(): in clamp() the lower bound wins outright when
     it exceeds the upper, so `clamp(100vw, …, cap)` would blow straight past
     the cap on an ultrawide screen. */
  width: min(max(100vw, calc(100vh * 16 / 9)), calc(100vh * 16 / 9 * 1.22));
  aspect-ratio: 16 / 9;
  flex: 0 0 auto;
  overflow: hidden;
  background: #0a0806;
}

/* §2 letterbox against a dark background on non-16:9 screens — never crop */
body:has(.hero-stage) {
  margin: 0;
  /* height, NOT min-height: the body must stay exactly one viewport tall or it
     grows to fit an over-tall stage, and the whole overflow ends up at the
     bottom instead of splitting evenly. */
  height: 100vh;
  display: flex;
  /* Symmetric: overflow splits evenly, a little off the ceiling machines and a
     little off the front desk edge. The 1.18 cap above is what keeps both the
     glass panel and the journal inside the visible band. */
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: #0a0806;
}

.hero-stage { z-index: 1; }

/* override style.css's fixed/viewport video — stage is already 16:9 */
.hero-stage .banner-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}

.hero-stage .bg-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
  z-index: 1;
  transition: filter .45s ease;
}

.hero-stage .bg-video.dimmed {
  filter: brightness(.4) saturate(.9);
}

/* Brief §2 puts this at z-index 3, under the DOM overlays. In practice a bubble
   that renders under the terminal is a bubble nobody sees, and bubbles are
   transient, so this layer sits on top. Children re-enable pointer events. */
#label-layer {
  position: absolute;
  inset: 0;
  z-index: 9;
  pointer-events: none;
}

/* Stacking order inside the stage:
   1 video · 4 hotspots · 5 coins · 7 parchment panels · 8 terminal · 9 bubbles
   The terminal must outrank the hotspots and the coins — expanded it covers
   88% of the stage, and anything above it swallows clicks meant for the shell.
   The z-index lives on .container's own rule below, NOT here: this line used to
   set 8 and the block below then set 4, later in the file, so 4 won and the
   coins drew on top of the expanded shell. */
.hero-stage .steampunk-floating-panels { z-index: 7; }

/* Every scene hotspot sits in here, so their ordering relative to each other
   cannot escape and climb over the terminal or an opened badge. */
.cs-hotspot-layer {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;   /* children re-enable individually */
}

/* An opened badge or book owns the pointer. Without this a hotspot underneath
   still answered clicks meant for the verify link. */
.hero-stage:has(.cs-plate-wrap.open) .cs-hotspot-layer,
.hero-stage:has(.cs-plate-wrap.open) .cs-coin,
.hero-stage:has(.cs-book-wrap.open) .cs-hotspot-layer,
.hero-stage:has(.cs-book-wrap.open) .cs-coin {
  pointer-events: none;
}

/* Nothing on the desk is live while the shell has the stage.
   Keyed off the terminal's OWN classes, not a class this script sets: the
   shell can also be opened by main.js's maximize button, which sets
   `fullscreen` and never told us — so clicks were still landing on hotspots
   hidden behind a full-screen terminal and popping their bubbles. */
.hero-stage:has(#terminal.expanded) .hotspot,
.hero-stage:has(#terminal.expanded) .cs-coin,
.hero-stage:has(#terminal.fullscreen) .hotspot,
.hero-stage:has(#terminal.fullscreen) .cs-coin,
.hero-stage.cs-expanded .hotspot,
.hero-stage.cs-expanded .cs-coin {
  pointer-events: none;
  opacity: 0;
}

/* ---------- §3 terminal in the measured glass rect ---------- */
/* glass panel: x 567→1331, y 265→688 of 1920×1080 */
.hero-stage .container {
  position: absolute;
  inset: 0;
  display: block;
  height: auto;
  z-index: 8;
  pointer-events: none;
}

/* `.hero-stage .container #terminal` on purpose, not `.hero-stage #terminal`.
   main.js is obfuscated and adds its own state classes (fullscreen, flip, AI
   mode); a rule like `#terminal.whatever` (1,1,0) would outrank the shorter
   form and drag the panel off the glass. This is (1,3,0) and cannot lose. */
.hero-stage .container #terminal {
  position: absolute;
  /* Glass measured NUMERICALLY off poster_day.jpg, not read off a grid by eye.
     The screen is blue where the brass is warm, so a `B > R + 12` mask isolates
     it cleanly: x 31.46–70.00, y 17.87–57.78.
     Overhangs each edge by ~0.2% on purpose — the electrical arcs sit ON the
     glass border (brief §11) and the terminal background is what covers them.
     Any gap here shows an arc, which is what "alignment issue" looked like. */
  left: 31.25%;
  top: 17.65%;
  width: 39.0%;
  height: 40.4%;
  aspect-ratio: auto;
  margin: 0;
  padding: 0;
  /* style.css animates #terminal with a transform; it would displace the
     measured rect. Flip/minimize states re-enable it below. */
  transform: none;
  pointer-events: auto;
  /* B7 — sit inside the machine rather than on top of it.
     The baked housing already provides the brass; what was missing was the
     transition between it and the panel. Three cheap layers do it: a dark
     seated ring where the glass meets the bezel, a warm inner reflection from
     the surrounding metal, and the blue arc spill the brief describes in §11
     (which is real in the video, so matching it makes the edge continuous). */
  border: 1px solid rgba(184, 134, 59, .22);
  /* elliptical: more curve on the left/right edges, to sit with the brass
     machine face's rounded glass rather than reading as a flat web rectangle */
  border-radius: 3.2% / 5.6%;
  overflow: auto;
  background: rgba(0, 0, 0, .55);
  box-shadow:
    inset 0 0 50px rgba(0, 150, 255, .18),          /* arc spill, as in the video */
    inset 0 0 0 1px rgba(0, 0, 0, .85),             /* the seam where glass meets brass */
    inset 0 2px 14px -4px rgba(0, 0, 0, .95),       /* bezel casting down onto the glass */
    inset 0 -10px 26px -14px rgba(255, 186, 92, .5),/* warm bounce off the lower housing */
    inset 6px 0 18px -14px rgba(255, 186, 92, .35), /* left key light, per brief §6 */
    inset -6px 0 18px -14px rgba(120, 190, 255, .3);/* cool window rim from the right */
  font-size: clamp(6px, .55vw, 11px);
  color: #d8e6f0;
  cursor: pointer;
  transition: left .45s cubic-bezier(.4, 0, .2, 1), top .45s cubic-bezier(.4, 0, .2, 1),
    width .45s cubic-bezier(.4, 0, .2, 1), height .45s cubic-bezier(.4, 0, .2, 1),
    font-size .45s ease, background .45s ease;
}

.hero-stage .container #terminal.expanded {
  /* Anchored to the VIEWPORT, not the stage. The stage is deliberately larger
     than the window (it covers rather than letterboxes), so a percentage of it
     put the expanded shell's top edge above the fold — fine in fullscreen,
     clipped in a normal browser window with chrome. Fixed insets always fit. */
  position: fixed;
  left: 3vw;
  top: 3vh;
  width: 94vw;
  height: 94vh;
  border-radius: 12px;
  background: rgba(6, 5, 4, .94);
  box-shadow: 0 0 0 3px #b8863b, 0 0 80px rgba(0, 0, 0, .8);
  font-size: clamp(11px, 1.05vw, 17px);
  cursor: auto;
}

/* two-state banner: ASCII art only fits when expanded (§3 typography) */
.hero-stage #terminal .ascii-art-container { display: none; }
.hero-stage #terminal.expanded .ascii-art-container { display: block; }
/* The banner is ~110 characters wide. It was left at the terminal's own font
   size and simply ran off the edge — "RISHAB D..." and nothing more — which
   looked like the typing animation had stalled. Sized to FIT instead of
   relying on a horizontal scroll nobody finds: at 1920 this is ~11px, so
   110 chars of monospace come to ~730px inside an 1690px panel. */
.hero-stage #terminal .ascii-art {
  white-space: pre;
  overflow-x: auto;
  font-size: clamp(3px, 0.62vw, 11px);
  line-height: 1.15;
  margin: 0 0 .6em;
}

.hero-stage #terminal.expanded .cs-compact-id { display: none; }

.cs-compact-id {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  color: #d8b06a;
  letter-spacing: .08em;
  line-height: 1.5;
}

.cs-compact-id .cs-tag { color: #8fd3ff; }

.cs-caret {
  display: inline-block;
  width: .55em;
  height: 1em;
  background: #64f105;
  vertical-align: text-bottom;
  animation: cs-blink 1.1s steps(1) infinite;
}

@keyframes cs-blink { 50% { opacity: 0; } }

.cs-affordance {
  position: absolute;
  right: .5em;
  bottom: .35em;
  z-index: 6;
  font: inherit;
  font-size: 1.05em;
  color: #ffc45c;
  background: rgba(0, 0, 0, .45);
  border: 1px solid rgba(255, 196, 92, .45);
  border-radius: 4px;
  padding: .1em .45em;
  cursor: pointer;
}

.cs-affordance:focus-visible { outline: 2px solid #ffc45c; }

/* ---------- §4 desk hotspots ---------- */
.hotspot {
  position: absolute;
  z-index: 4;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  border-radius: 8px;
  transition: box-shadow .3s ease, background .3s ease;
}

/* A rectangle can only look aligned if the rect is perfect. A soft radial glow
   has no edges to misalign, so it reads as "this thing is lit" rather than
   "here is a box that does not quite fit the thing". Same rects, no corners. */
.hotspot::after {
  content: "";
  position: absolute;
  inset: -12%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
      rgba(255, 214, 140, .40) 0%,
      rgba(255, 196, 92, .22) 42%,
      rgba(255, 196, 92, .07) 68%,
      rgba(255, 196, 92, 0) 100%);
  opacity: 0;
  transition: opacity .28s ease, transform .28s ease;
  transform: scale(.86);
  pointer-events: none;
}

.hotspot:hover::after,
.hotspot:focus-visible::after {
  opacity: 1;
  transform: scale(1);
}

.hotspot:hover,
.hotspot:focus-visible {
  background: none;
  box-shadow: none;
  outline: none;
}

/* keyboard focus still needs a hard, visible marker — a glow alone is not one */
.hotspot:focus-visible {
  outline: 2px solid #ffc45c;
  outline-offset: 2px;
  border-radius: 12px;
}

/* --- the click "feel" ---
   Adventure games sell an object by answering the pointer twice: a warm lift on
   hover, and a ring that leaves the object on click. Both are cheap; without
   them an invisible rect reads as dead scenery. */
/* the lift stays; the box does not — the glow is drawn by ::after above */
.hotspot:hover { transform: scale(1.035); }

.hotspot:active { transform: scale(.985); }

/* expanding ring, spawned on click */
.cs-ping {
  position: absolute;
  z-index: 8;
  border: 2px solid rgba(255, 210, 130, .85);
  border-radius: 50%;
  pointer-events: none;
  animation: cs-ping .55s cubic-bezier(.2, .7, .3, 1) forwards;
}

@keyframes cs-ping {
  from { transform: translate(-50%, -50%) scale(.25); opacity: .9; }
  to   { transform: translate(-50%, -50%) scale(1);   opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .hotspot:hover, .hotspot:active { transform: none; }
  .cs-ping { display: none; }
}

/* measured — CLAUDE_CODE_BRIEF.md §4, do not recalculate */
/* Re-measured against poster_day.jpg / poster_night.jpg — the day and night
   frames share one geometry (phase-correlation displacement 0.1–4 px, under
   0.25% of frame width), so there is one table, not two. */
/* Night defaults; chronoshell-stage.js swaps in the day rects on switch.
   All five measured — see measurements/SCENE_MEASURED.json. */
.hotspot[data-panel="trivia"]  { left: 24.79%; top: 69.26%; width: 15.89%; height: 19.53%; }
.hotspot[data-panel="joke"]    { left: 59.06%; top: 74.81%; width: 10.31%; height: 11.11%; }
.hotspot[data-panel="weather"] { left: 78.85%; top: 60.28%; width:  7.65%; height: 25.93%; }
.hotspot[data-cmd="help"]      { left: 44.53%; top: 83.52%; width: 12.03%; height:  5.93%; }
.hotspot[data-cmd="sound"]     { left:  0.42%; top: 53.06%; width: 14.27%; height: 26.48%; }

/* ---------- §5 hint bubbles ---------- */
.cs-bubble {
  position: absolute;
  z-index: 5;
  max-width: 22ch;
  padding: .7em .9em;
  transform: translate(-50%, -100%) scale(.85);
  opacity: 0;
  pointer-events: auto;
  background: #f6e6c4;
  color: #4a3418;
  border: 2px solid #8a6534;
  border-radius: 14px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .55);
  font-family: "Dancing Script", Georgia, serif;
  font-size: clamp(11px, 1.1vw, 19px);
  line-height: 1.25;
  text-align: center;
  transition: opacity .3s ease, transform .3s cubic-bezier(.34, 1.3, .64, 1);
}

.cs-bubble.show { opacity: 1; transform: translate(-50%, -100%) scale(1); }

/* tail pointing down at the object */
.cs-bubble::after,
.cs-bubble::before {
  content: "";
  position: absolute;
  /* set per bubble in makeBubble, so an edge-clamped bubble still points at
     the object rather than at its own middle */
  left: var(--tail, 50%);
  top: 100%;
  border: 11px solid transparent;
  border-top-color: #8a6534;
  transform: translateX(-50%);
}

.cs-bubble::after {
  border-width: 9px;
  border-top-color: #f6e6c4;
  margin-top: -2px;
}

/* click-to-read object bubbles: same component, a touch wider for prose */
.cs-bubble--info { max-width: 30ch; }

/* The Gurudev bubble is the same component as every other object bubble — same
   face, same size, same fill. Only the width is raised, because it carries two
   full sentences where the others carry one line. */
.cs-bubble--reverent { max-width: 42ch; }

/* flipped: object sits near the top of the frame, so the bubble hangs below it
   and the tail points up */
.cs-bubble--below { transform: translate(-50%, 0) scale(.85); }
.cs-bubble--below.show { transform: translate(-50%, 0) scale(1); }

.cs-bubble--below::before,
.cs-bubble--below::after {
  top: auto;
  bottom: 100%;
  border-top-color: transparent;
}

.cs-bubble--below::before { border-bottom-color: #8a6534; }

.cs-bubble--below::after {
  border-bottom-color: #f6e6c4;
  margin-top: 0;
  margin-bottom: -2px;
}

/* `hotspots` terminal command draws every box so the rects can be nudged
   against the real frame instead of guessed at */
.hero-stage.cs-calibrate .hotspot {
  outline: 1px dashed rgba(255, 196, 92, .9);
  background: rgba(255, 196, 92, .10);
}

/* calibration must win over the expanded-terminal stand-down rule, or the boxes
   vanish the moment the shell is open */
.hero-stage.cs-calibrate .hotspot {
  opacity: 1;
  pointer-events: auto;
}

.cs-calibrate-banner {
  position: fixed;
  left: 50%;
  bottom: 16px;
  z-index: 999;
  transform: translateX(-50%);
  padding: 7px 14px;
  font: 12px/1.3 ui-monospace, monospace;
  color: #ffe6b0;
  background: rgba(20, 12, 4, .92);
  border: 1px solid #b8863b;
  border-radius: 6px;
  pointer-events: none;
}

.hero-stage.cs-calibrate .hotspot::after {
  content: attr(data-label);
  position: absolute;
  left: 0;
  top: 0;
  font: 10px/1.2 ui-monospace, monospace;
  color: #ffc45c;
  background: rgba(0, 0, 0, .7);
  padding: 1px 3px;
  white-space: nowrap;
}

/* ---------- §7 portrait frame (covers the sparkle watermark) ---------- */
.portrait {
  position: absolute;
  left: 86.5%;
  top: 72%;
  width: 8.5%;
  margin: 0;
  z-index: 4;
}

/* Slightly narrower and taller than the rect in §7 — the supplied photograph is
   a 0.84 portrait and the original near-square rect cropped his head. Still
   covers the watermark (x 89.17–92.14, y 79.81–87.12) with margin on all sides:
   this runs x 86.5–95, y 72–92. */
/* Clickable at Rishab's request, overriding brief §7's "no hover, no click",
   and answering the pointer exactly like every other object in the room. */
.portrait-frame {
  position: relative;
  display: block;
  padding: 0;
  cursor: pointer;
  transition: box-shadow .3s ease, transform .3s ease;
  width: 100%;
  /* exactly the photograph's ratio — 470x560 — so `cover` has nothing to crop.
     aspect-ratio rather than a padding-bottom hack: percentage padding resolves
     against the CONTENT width, so the border threw the ratio out by ~1%. */
  aspect-ratio: 470 / 560;
  border-radius: 2px;

  /* A gilt moulding is never one flat band — it is a stack of steps, each
     catching light on its upper-left and shading on its lower-right. Six
     box-shadow rings do that for free: dark rebate, gold bead, dark valley,
     broad gold ogee, dark backing, then the drop shadow onto the books. */
  border: .18vw solid #2a1c0c;
  box-shadow:
    0 0 0 .10vw #6b4a1c,
    0 0 0 .20vw #e8c88a,
    0 0 0 .30vw #8a6534,
    0 0 0 .62vw #c9a259,
    0 0 0 .74vw #4a3316,
    0 6px 20px rgba(0, 0, 0, .8),
    inset 0 0 14px rgba(0, 0, 0, .55);
}

/* the ogee's own highlight: warm along the top-left, falling away bottom-right,
   matching the room's key light (§6) so the moulding reads as carved */
.portrait-frame::before {
  content: "";
  position: absolute;
  inset: -.68vw;
  border-radius: 3px;
  z-index: -1;
  background: linear-gradient(128deg,
      rgba(255, 232, 180, .55) 0%, rgba(255, 210, 130, .12) 26%,
      rgba(0, 0, 0, .10) 58%, rgba(0, 0, 0, .45) 100%);
  pointer-events: none;
}

.portrait-frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  /* the photograph has a cold blue studio backdrop; the room is amber and lit
     by candles. Sepia carries the blue over to a warm brown so it reads as a
     period print on the desk rather than a modern JPEG pasted into the scene. */
  filter: brightness(.82) sepia(.42) contrast(1.06) saturate(.85);
}

/* same warm answer the hotspots give, sitting outside the gilt rings so the
   moulding is not washed out */
.portrait-frame:hover,
.portrait-frame:focus-visible {
  transform: scale(1.035);
  outline: none;
}

.portrait-frame:hover { filter: drop-shadow(0 0 26px rgba(255, 200, 110, .55)); }
.portrait-frame:focus-visible { filter: drop-shadow(0 0 0 2px #ffc45c) drop-shadow(0 0 26px rgba(255, 200, 110, .55)); }
.portrait-frame:active { transform: scale(.985); }

@media (prefers-reduced-motion: reduce) {
  .portrait-frame:hover,
  .portrait-frame:focus-visible,
  .portrait-frame:active { transform: none; }
}

/* candlelight falling across the glass, warm from the left as §6 describes */
.portrait-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(118deg,
      rgba(255, 206, 140, .22) 0%, rgba(255, 206, 140, .05) 38%,
      rgba(0, 0, 0, .16) 72%, rgba(0, 0, 0, .34) 100%);
}

/* a small hand-inked card at the foot of the frame — part of the room, not UI */
.portrait-caption {
  display: block;
  margin-top: .35em;
  text-align: center;
  line-height: 1.25;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .9);
}

.portrait-name {
  display: block;
  font-family: Georgia, serif;
  font-size: clamp(6px, .58vw, 11px);
  letter-spacing: .04em;
  color: #e8c98a;
}

.portrait-line {
  display: block;
  font-family: "Dancing Script", Georgia, serif;
  font-size: clamp(8px, .78vw, 15px);
  color: #f4e2be;
  opacity: .92;
}

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

/* The three parchment panels are never shown any more — the teacup and the
   lantern answer in object bubbles and the journal opens as the book. They stay
   in the document because main.js writes their content and we read it back.

   Hidden HERE, in CSS, not by the script: style.css parks the weather panel at
   top-right and main.js sets an inline display on all three, so until the stage
   script ran there was a visible flash of the old popup on every page load.
   `!important` because the inline style would otherwise win. */
#trivia-dropdown,
#joke-dropdown,
#weather-popup {
  display: none !important;
}

/* ---------- §8 parchment panels: closed by default, beside their object ---------- */
.hero-stage .steampunk-floating-panels { z-index: 6; }

.hero-stage .steampunk-floating-panels .info-dropdown,
.hero-stage .steampunk-floating-panels #weather-popup {
  position: absolute;
  transform-origin: var(--cs-origin, 50% 100%);
  animation: cs-unfurl .35s cubic-bezier(.34, 1.3, .64, 1);
}

@keyframes cs-unfurl {
  from { transform: scale(.7); opacity: 0; }
  to   { transform: scale(1);  opacity: 1; }
}

/* left/right thirds only — never over the glass rect (29.5%→69.3%) */
.hero-stage #trivia-dropdown {
  left: 2%;
  top: 30%;
  right: auto;
  bottom: auto;
  --cs-origin: 40% 100%;
  max-height: 42%;
  overflow-y: auto;
}

.hero-stage #joke-dropdown {
  left: auto;
  right: 2%;
  top: auto;
  bottom: 18%;
  --cs-origin: 20% 100%;
}

.hero-stage #weather-popup.victorian-card.poster {
  left: auto;
  right: 2%;
  top: 12%;
  bottom: auto;
  --cs-origin: 30% 100%;
}

/* brass rail scrollbar (§8) */
.hero-stage #trivia-dropdown::-webkit-scrollbar { width: 8px; }
.hero-stage #trivia-dropdown::-webkit-scrollbar-track { background: rgba(60, 42, 20, .35); border-radius: 4px; }
.hero-stage #trivia-dropdown::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, #e2bd76, #8a6534);
  border-radius: 4px;
}

/* ---------- The journal, opening at centre stage ----------
   CSS 3D, not WebGL. Brief §6 reserves WebGL for the coins, where a metallic
   specular is the entire point; parchment has no specular to miss, and §7's
   reasoning applies here too — clean WebGL output sits badly against baked
   lighting and grain. This gets the same trick for no dependency and degrades
   to a plain fade when 3D transforms are unavailable. */
.cs-book-wrap {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  perspective: 1800px;
  background: radial-gradient(ellipse at center, rgba(0, 0, 0, .45) 0%, rgba(0, 0, 0, .82) 75%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s ease;
}

.cs-book-wrap.open { opacity: 1; pointer-events: auto; }

.cs-book {
  position: relative;
  width: 58%;
  height: 62%;
  transform-style: preserve-3d;
  /* flies up out of the journal on the desk, then settles facing the viewer */
  transform: translateY(26%) rotateX(52deg) scale(.42);
  transition: transform .62s cubic-bezier(.22, .9, .3, 1.05);
}

.cs-book-wrap.open .cs-book { transform: translateY(0) rotateX(8deg) scale(1); }

.cs-page {
  position: absolute;
  top: 0;
  width: 50%;
  height: 100%;
  overflow: hidden;
  /* vintage_paper.jpg is a whole decorative spread with a central ornament, so
     stretch one copy across BOTH pages — each page shows its own half and the
     ornament lands in the gutter where a real binding would put it, instead of
     being duplicated twice under the text. */
  background:
    linear-gradient(180deg, rgba(70, 48, 20, .13), rgba(70, 48, 20, .03) 22%, rgba(70, 48, 20, .16)),
    url("vintage_paper.jpg") no-repeat, #efe0bd;
  background-size: auto, 200% 100%, auto;
  box-shadow: inset 0 0 60px rgba(96, 66, 26, .45);
  backface-visibility: hidden;
}

.cs-page--l { background-position: center, left center, center; }
.cs-page--r { background-position: center, right center, center; }

/* keep the ornament from competing with the writing — the wash sits on the
   page, under the inner, so it lightens the texture without touching the text
   (the trivia HTML arrives as bare text nodes, so it cannot be raised above a
   wash placed inside the same element) */
.cs-page::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  /* papyrus wash — heavy enough that the ornament reads as watermark under the
     text rather than competing with it */
  background:
    radial-gradient(ellipse at 50% 45%, rgba(246, 233, 202, .93) 0%, rgba(243, 226, 189, .82) 55%, rgba(232, 210, 165, .62) 100%);
  pointer-events: none;
}

/* Torn outer edge, Victorian-tome style. Reuses the #v-wavy turbulence filter
   already defined in index.html — the displacement chews an irregular line out
   of a plain strip. It sits on its own layer so the text is never distorted. */
.cs-page::after {
  content: "";
  position: absolute;
  top: -2%;
  height: 104%;
  width: 5%;
  z-index: 2;
  background: #efe0bd;
  filter: url(#v-wavy);
  pointer-events: none;
}

.cs-page--l::after { left: -2.5%; }
.cs-page--r::after { right: -2.5%; }

/* the torn lip catches the candlelight along its ragged line */
.cs-page--l { box-shadow: inset 0 0 60px rgba(96, 66, 26, .45), inset 10px 0 22px -14px rgba(60, 38, 10, .8); }
.cs-page--r { box-shadow: inset 0 0 60px rgba(96, 66, 26, .45), inset -10px 0 22px -14px rgba(60, 38, 10, .8); }

.cs-page-inner { position: relative; z-index: 1; }

.cs-page--l {
  left: 0;
  transform-origin: right center;
  border-radius: 10px 3px 3px 10px;
  transform: rotateY(88deg);
  transition: transform .62s cubic-bezier(.22, .9, .3, 1.05) .12s;
}

.cs-page--r {
  right: 0;
  transform-origin: left center;
  border-radius: 3px 10px 10px 3px;
  transform: rotateY(-88deg);
  transition: transform .62s cubic-bezier(.22, .9, .3, 1.05) .12s;
}

.cs-book-wrap.open .cs-page--l { transform: rotateY(6deg); }
.cs-book-wrap.open .cs-page--r { transform: rotateY(-6deg); }

/* the gutter shadow is what sells it as one bound object rather than two cards */
.cs-book::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: 7%;
  height: 100%;
  transform: translateX(-50%);
  background: linear-gradient(90deg,
      rgba(0, 0, 0, 0), rgba(60, 40, 14, .55) 45%, rgba(60, 40, 14, .55) 55%, rgba(0, 0, 0, 0));
  pointer-events: none;
}

/* Readability beats atmosphere. The body was set in Dancing Script — a
   handwriting face — over a busy texture, which is unreadable at any size for
   prose. Script is now reserved for the title block; the entries are set in a
   plain old-style serif, larger, looser and in darker ink. */
.cs-page-inner {
  padding: 8% 9%;
  height: 100%;
  overflow-y: auto;
  color: #2b1a08;
  /* IM Fell English — a real English press face still in use through the
     Victorian era. Period-correct and readable as prose, unlike a script.
     It runs small and light for its point size, so it gets a size and weight
     bump and a touch more line-height than a modern serif would need. */
  font-family: "IM Fell English", Georgia, "Iowan Old Style", "Palatino Linotype", serif;
  font-size: clamp(14px, 1.28vw, 21px);
  line-height: 1.78;
  letter-spacing: .004em;
}

.cs-page--l .cs-page-inner { text-align: center; }

/* the script survives here, where it is three words and decorative */
/* Tangerine is English roundhand — the copperplate a Victorian would actually
   have written a title page in, where Dancing Script is a modern brush hand.
   Already loaded for the sticky note, so it costs no extra request. It is a
   very light face, hence 700 weight and the larger sizes below. */
.cs-book-title,
.cs-book-sub,
.cs-book-date {
  font-family: Tangerine, "Dancing Script", cursive;
  font-weight: 700;
}

.cs-book-title {
  font-size: 3em;
  font-weight: 700;
  line-height: 1.15;
  margin-bottom: .35em;
  color: #4a2c0c;
}

.cs-book-sub { font-size: 1.75em; line-height: 1.15; opacity: .8; }

/* --- the old-mystical dressing: gilt edges, candlelight, ink --- */
.cs-book-rule {
  width: 62%;
  height: 2px;
  margin: .9em auto;
  background: linear-gradient(90deg, transparent, #a3762f 18%, #e0bb74 50%, #a3762f 82%, transparent);
}

.cs-book-rule::after {
  content: "❦";
  display: block;
  margin-top: -.75em;
  color: #8a6534;
  font-size: 1.1em;
  background: transparent;
}

.cs-book-date {
  margin-top: 1.1em;
  font-size: 1.9em;
  color: #6b4a1c;
  letter-spacing: .04em;
}

/* gilded page edges — the thin bright line that says "this book is old and cared for" */
.cs-page--l { box-shadow: inset 0 0 60px rgba(96, 66, 26, .45), inset 6px 0 0 -3px #c9a259; }
.cs-page--r { box-shadow: inset 0 0 60px rgba(96, 66, 26, .45), inset -6px 0 0 -3px #c9a259; }

/* candle throw from the desk below, plus the burn at the page corners */
.cs-page-inner {
  background:
    radial-gradient(ellipse at 50% 120%, rgba(255, 196, 92, .28), transparent 60%),
    radial-gradient(ellipse at 15% 8%, rgba(90, 58, 20, .22), transparent 45%),
    radial-gradient(ellipse at 88% 92%, rgba(90, 58, 20, .26), transparent 50%);
}

/* entries are the thing people actually read — give them room and separation */
.cs-book-body { text-align: left; }
.cs-book-body br + br { line-height: 2.4; }

/* the book throws its own light into the room while it is open */
.cs-book {
  filter: drop-shadow(0 26px 46px rgba(0, 0, 0, .8))
          drop-shadow(0 0 60px rgba(255, 190, 100, .22));
}

/* motes drifting in the candlelight — one element, no JS */
.cs-book-wrap.open::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    radial-gradient(2px 2px at 20% 30%, rgba(255, 226, 170, .55), transparent 60%),
    radial-gradient(2px 2px at 72% 22%, rgba(255, 226, 170, .40), transparent 60%),
    radial-gradient(1.5px 1.5px at 38% 74%, rgba(255, 226, 170, .45), transparent 60%),
    radial-gradient(1.5px 1.5px at 84% 66%, rgba(255, 226, 170, .35), transparent 60%),
    radial-gradient(2px 2px at 56% 48%, rgba(255, 226, 170, .30), transparent 60%);
  animation: cs-motes 14s ease-in-out infinite alternate;
}

@keyframes cs-motes {
  from { transform: translate3d(0, 0, 0); opacity: .5; }
  to   { transform: translate3d(-18px, -26px, 0); opacity: .95; }
}

.cs-page-inner::-webkit-scrollbar { width: 7px; }
.cs-page-inner::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, #b08a4a, #6b4a1c);
  border-radius: 4px;
}

/* --- turning leaf ---
   One leaf does the whole job: it carries the current right-hand page on its
   front and the next left-hand page on its back, swings on the spine, and when
   it lands the two static pages are swapped to the new spread. A real stack of
   leaves would be prettier and about ten times the code. */
.cs-leaf {
  position: absolute;
  top: 0;
  left: 50%;
  width: 50%;
  height: 100%;
  z-index: 3;
  transform-origin: left center;
  transform-style: preserve-3d;
  transition: transform .78s cubic-bezier(.42, .02, .34, 1);
  pointer-events: none;
  display: none;
}

.cs-leaf.turning { display: block; transform: rotateY(-176deg); }
.cs-leaf.turning-back { display: block; transform: rotateY(0deg); }
.cs-leaf.at-left { transform: rotateY(-176deg); }

.cs-leaf-face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  backface-visibility: hidden;
  background:
    linear-gradient(180deg, rgba(70, 48, 20, .13), rgba(70, 48, 20, .03) 22%, rgba(70, 48, 20, .16)),
    url("vintage_paper.jpg") no-repeat, #efe0bd;
  background-size: auto, 200% 100%, auto;
  box-shadow: inset 0 0 60px rgba(96, 66, 26, .45);
}

.cs-leaf-front { background-position: center, right center, center; border-radius: 3px 10px 10px 3px; }

.cs-leaf-back {
  transform: rotateY(180deg);
  background-position: center, left center, center;
  border-radius: 10px 3px 3px 10px;
}

.cs-leaf-face::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at 50% 45%, rgba(246, 233, 202, .93) 0%, rgba(243, 226, 189, .82) 55%, rgba(232, 210, 165, .62) 100%);
}

/* the shading that sells paper bending rather than a flat card rotating */
.cs-leaf-face::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(60, 40, 14, .38), rgba(60, 40, 14, 0) 32%);
  opacity: 0;
  transition: opacity .78s ease;
}

.cs-leaf.turning .cs-leaf-face::after,
.cs-leaf.turning-back .cs-leaf-face::after { opacity: 1; }

/* corner tabs — the affordance that says the pages move */
.cs-turn {
  position: absolute;
  bottom: 3%;
  z-index: 4;
  padding: .35em .8em;
  font: 1em/1 "Dancing Script", Georgia, serif;
  color: #f6e6c4;
  background: linear-gradient(180deg, rgba(107, 74, 28, .92), rgba(51, 34, 13, .92));
  border: 1px solid #c9a259;
  border-radius: 6px;
  cursor: pointer;
  opacity: .85;
}

.cs-turn:hover { opacity: 1; }
.cs-turn[disabled] { opacity: .25; cursor: default; }
.cs-turn--prev { left: 4%; }
.cs-turn--next { right: 4%; }

.cs-book-folio {
  position: absolute;
  bottom: 3.2%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  font: .8em/1 Georgia, serif;
  color: #6b4a1c;
  opacity: .8;
}

@media (prefers-reduced-motion: reduce) {
  .cs-leaf { transition: none; }
}

.cs-book-close {
  position: absolute;
  top: -4%;
  right: -3%;
  z-index: 2;
  width: 2.2em;
  height: 2.2em;
  font: 700 1em/1 ui-monospace, monospace;
  color: #f6e6c4;
  background: linear-gradient(180deg, #6b4a1c, #33220d);
  border: 1px solid #e2bd76;
  border-radius: 50%;
  cursor: pointer;
}

@media (max-width: 768px) {
  .cs-book { width: 92%; height: 70%; }
  .cs-page-inner { font-size: 15px; padding: 10% 8%; }
}

@media (prefers-reduced-motion: reduce) {
  /* The opening is the point of the book, so it survives here — same call as
     the background video. Only the ambient drift is dropped. */
  .cs-book-wrap.open::after { animation: none; }
}

/* ---------- Certification coins ----------
   The coins are baked into the video, but the model rendered their lettering
   wrong (`GGP`, `Aeuro`, `LeerCedo`). These PNGs sit exactly on top of the
   baked ones, so the desk reads correctly. Positioned in % of the stage, which
   is what welds them to the video at any size — the stage scales, they scale
   with it. */
.cs-coin {
  position: absolute;
  z-index: 5;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  transition: transform .28s cubic-bezier(.34, 1.3, .64, 1), filter .28s ease;
  /* the baked coins are lit warm from the left; match or they read as stickers */
  filter: drop-shadow(0 3px 6px rgba(0, 0, 0, .75)) saturate(.92) brightness(.96);
}

.cs-coin img {
  width: 100%;
  height: 100%;
  /* `fill`, not `contain`. The baked coins are seen at a desk angle, so they
     are ELLIPSES — 111x93 px at 1080p. `contain` fitted the round PNG to the
     shorter side and left it 93x93, too small to cover the garbled lettering
     underneath, and unevenly so per coin. Filling stretches the circle into
     the same ellipse the video already shows. */
  object-fit: fill;
  display: block;
  pointer-events: none;
}

.cs-coin:hover,
.cs-coin:focus-visible {
  transform: translateY(-6%) scale(1.06);
  filter: drop-shadow(0 8px 14px rgba(0, 0, 0, .8)) drop-shadow(0 0 18px rgba(255, 200, 110, .6));
  outline: none;
}

.cs-coin:active { transform: translateY(-2%) scale(1.01); }

/* --- the opened plate, hovering at centre stage --- */
.cs-plate-wrap {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  perspective: 1600px;
  background: radial-gradient(ellipse at center, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .86) 75%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s ease;
}

.cs-plate-wrap.open { opacity: 1; pointer-events: auto; }

.cs-plate-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3vh;
}

.cs-plate {
  position: relative;
  width: min(34vw, 46vh);
  aspect-ratio: 1;
  transform-style: preserve-3d;
  /* two motions at once: a slow turn, and a gentle float so it reads as
     hovering rather than mounted on a spindle */
  animation: cs-plate-spin 9s linear infinite, cs-plate-float 4.5s ease-in-out infinite;
}

@keyframes cs-plate-spin {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}

@keyframes cs-plate-float {
  0%, 100% { translate: 0 -1.2%; }
  50%      { translate: 0 1.2%; }
}

.cs-plate-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  display: grid;
  place-items: center;
}

.cs-plate-face img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 0 26px rgba(255, 200, 110, .35));
}

/* The reverse. A flat image turned 180° would show a mirrored front, which
   looks like a mistake — so the back is its own face: engraved brass carrying
   the credential, exactly as brief §6 specified for Face B. */
.cs-plate-back {
  transform: rotateY(180deg);
  border-radius: 50%;
  padding: 12%;
  text-align: center;
  color: #f0d9a4;
  background:
    radial-gradient(circle at 32% 28%, #8a6a33 0%, #5b431d 45%, #33240e 100%);
  box-shadow: inset 0 0 40px rgba(0, 0, 0, .7), 0 0 0 .4vh #c9a259;
  font-family: Georgia, serif;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .6);
}

.cs-plate-back b { display: block; font-size: 1.35em; margin-bottom: .35em; }
.cs-plate-back span { display: block; opacity: .8; font-size: .95em; }

/* the mirrored reverse: same art, flipped, slightly cooled so it reads as the
   far side of one object rather than a second copy pasted on */
.cs-plate-back--mirror {
  background: none;
  box-shadow: none;
  padding: 0;
  border-radius: 0;
}

.cs-plate-back--mirror img {
  transform: scaleX(-1);
  filter: brightness(.72) saturate(.8) drop-shadow(0 0 22px rgba(255, 200, 110, .22));
}

/* a certificate is landscape; it gets a plate that is not square */
.cs-plate--cert { width: min(52vw, 74vh); aspect-ratio: 1.4; }
.cs-plate--cert .cs-plate-back { border-radius: 6px; }

.cs-plate-caption {
  max-width: min(60vw, 60ch);
  text-align: center;
  font-family: "IM Fell English", Georgia, serif;
  color: #f4e2be;
  font-size: clamp(13px, 1.25vw, 21px);
  line-height: 1.5;
  text-shadow: 0 2px 6px rgba(0, 0, 0, .9);
}

.cs-plate-caption b { color: #ffd98a; }
.cs-plate-caption em { display: block; opacity: .78; font-style: normal; font-size: .85em; }

.cs-plate-verify {
  display: inline-block;
  margin-top: .6em;
  padding: .3em .9em;
  color: #2b1a08;
  background: linear-gradient(180deg, #e8c88a, #c9a259);
  border: 1px solid #8a6534;
  border-radius: 4px;
  text-decoration: none;
  font-size: .85em;
}

.cs-plate-verify:hover { background: linear-gradient(180deg, #f6dda8, #d9b46a); }

.cs-plate-close {
  position: absolute;
  top: 4%;
  right: 4%;
  z-index: 2;
  width: 2.2em;
  height: 2.2em;
  font: 700 1em/1 ui-monospace, monospace;
  color: #f6e6c4;
  background: linear-gradient(180deg, #6b4a1c, #33220d);
  border: 1px solid #e2bd76;
  border-radius: 50%;
  cursor: pointer;
}

@media (max-width: 768px) {
  .cs-coin { display: none; }          /* §9 — 25px coins are untappable */
  .cs-plate { width: 78vw; }
  .cs-plate--cert { width: 92vw; }
}

@media (prefers-reduced-motion: reduce) {
  /* The turn keeps its normal speed. Slowing it to 14s here was meant as a
     courtesy and backfired: it reads as a static badge rather than a turning
     one, which is the whole feature. Same call already made for the hero video
     and the book. Only the idle float and the pointer lift are dropped. */
  .cs-plate { animation: cs-plate-spin 9s linear infinite; }
  .cs-coin:hover, .cs-coin:focus-visible, .cs-coin:active { transform: none; }
}

/* ---------- the credit line ----------
   It shipped as a big yellow pill parked across the middle of the desk, which
   is exactly where the coins sit (x 32–71%, y 89–98%). Shrunk and moved into
   the bottom-left corner, clear of the coin band, and dimmed until hovered —
   it is a credit, not a call to action. */
body:has(.hero-stage) .footer.center-footer {
  position: absolute;
  left: 1.2%;
  right: auto;
  bottom: 1%;
  z-index: 6;
  margin: 0;
  padding: 0;
  width: auto;
  background: none;
  text-align: left;
  opacity: .5;
  transition: opacity .3s ease;
}

body:has(.hero-stage) .footer.center-footer:hover { opacity: 1; }

body:has(.hero-stage) .footer.center-footer p {
  margin: 0;
  padding: .25em .7em;
  font-family: Georgia, serif;
  font-size: clamp(8px, .68vw, 12px);
  color: #e8c98a;
  background: rgba(12, 8, 4, .55);
  border: 1px solid rgba(184, 134, 59, .35);
  border-radius: 999px;
  white-space: nowrap;
}

body:has(.hero-stage) .footer.center-footer a { color: #ffd98a; }

@media (max-width: 768px) {
  body:has(.hero-stage) .footer.center-footer { left: 50%; transform: translateX(-50%); }
}

/* ---------- §9 mobile ---------- */
.cs-dock {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 6;
  gap: 10px;
  justify-content: center;
  padding: 8px;
  background: linear-gradient(0deg, rgba(10, 8, 6, .92), rgba(10, 8, 6, 0));
}

.cs-dock button {
  font-size: 20px;
  line-height: 1;
  padding: 10px 14px;
  color: #f6e6c4;
  background: linear-gradient(180deg, #6b4a1c, #3a2711);
  border: 1px solid #e2bd76;
  border-radius: 10px;
  cursor: pointer;
}

@media (max-width: 768px) {
  .hero-stage { aspect-ratio: auto; height: 100vh; height: 100dvh; }
  .hero-stage .bg-video { object-fit: cover; }
  /* must match the desktop rule's specificity or it will never apply */
  .hero-stage .container #terminal,
  .hero-stage .container #terminal.expanded {
    left: 4%;
    top: 4%;
    width: 92%;
    height: 74%;
    font-size: clamp(11px, 3.2vw, 15px);
  }
  .hero-stage #terminal .ascii-art-container { display: block; }
  .hero-stage .cs-compact-id,
  .cs-affordance,
  .hotspot,
  .portrait { display: none; }
  .cs-dock { display: flex; }
  .hero-stage .steampunk-floating-panels .info-dropdown,
  .hero-stage .steampunk-floating-panels #weather-popup {
    left: 4%;
    right: 4%;
    top: 10%;
    bottom: auto;
    max-height: 60%;
    overflow-y: auto;
  }
}

/* ---------- §10 reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  /* DELIBERATE DEVIATION from brief §10: the brief says swap the video for the
     poster here. Rishab wants the background video playing regardless — it is a
     slow, deliberate loop (§11), not a motion hazard. The poster still covers
     first paint, and every *interface* animation below is still disabled. */
  /* Only the UNPROMPTED hint bubbles are suppressed (brief §5). A bubble the
     visitor opened by clicking an object is a response, not motion — killing
     those made every object look broken. */
  .cs-bubble:not(.cs-bubble--info) { display: none !important; }
  .cs-bubble--info { transition: none; }
  .hero-stage #terminal,
  .hero-stage .steampunk-floating-panels .info-dropdown,
  .hero-stage .steampunk-floating-panels #weather-popup {
    transition: none;
    animation: none;
  }
}


/* ===========================================================================
   THE FRONT DOOR, SECOND PASS

   The first version was two flat panels and a sheen. This adds the things a
   game main-menu actually has: a portal breathing behind the copy, a rotating
   light around each card's edge, an era emblem that moves (brass gear for
   1888, orbiting bodies for 2026), a spotlight that follows the pointer, and a
   card that tilts towards it.

   All of it is CSS. The only thing JavaScript contributes is four numbers per
   card — --mx, --my, --rx, --ry — so nothing here waits on a script to paint.
   These rules come after the originals on purpose: same specificity, later
   wins, and the first pass stays readable as the fallback it now is.
   ========================================================================= */

/* --- the portal: concentric rings easing outward, forever ---------------- */
#cs-warp::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(150vh, 150vw);
  aspect-ratio: 1;
  translate: -50% -50%;
  z-index: 1;
  pointer-events: none;
  opacity: .5;
  background: repeating-radial-gradient(circle at center,
      rgba(143, 211, 255, .085) 0 1px,
      transparent 1px 74px);
  mask-image: radial-gradient(circle at center, #000 8%, transparent 62%);
  -webkit-mask-image: radial-gradient(circle at center, #000 8%, transparent 62%);
  animation: cs-portal 9s cubic-bezier(.4, 0, .5, 1) infinite;
}

@keyframes cs-portal {
  0%   { transform: scale(.55); opacity: 0; }
  22%  { opacity: .55; }
  100% { transform: scale(1.45); opacity: 0; }
}

/* --- title: letters arrive one at a time, then the line catches the light - */
.cs-gate-title .ltr {
  display: inline-block;
  opacity: 0;
  transform: translateY(.5em) rotateX(-70deg);
  animation: cs-drop .62s cubic-bezier(.2, .8, .25, 1) forwards;
}
.cs-gate-title .ltr.sp { width: .32em; }

@keyframes cs-drop {
  to { opacity: 1; transform: none; }
}

.cs-gate-title {
  position: relative;
  background: linear-gradient(100deg, #fff6e4 30%, #ffe6b0 44%, #8fd3ff 52%, #fff6e4 66%);
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: cs-sheen 7s linear infinite 1.2s;
}
@keyframes cs-sheen {
  0%   { background-position: 130% 0; }
  100% { background-position: -130% 0; }
}

/* --- the cards ----------------------------------------------------------- */
.cs-gate-card {
  transform-style: preserve-3d;
  transform:
    perspective(1000px)
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    translateY(var(--lift, 0px));
  transition: transform .35s cubic-bezier(.22, .9, .3, 1), box-shadow .5s ease;
  will-change: transform;
}
.cs-gate-card:hover,
.cs-gate-card:focus-visible { --lift: -6px; }
.cs-gate-card:active { --lift: -2px; }

/* the rotating edge light. A conic gradient the size of the card, masked down
   to a 1px ring by two backgrounds clipped differently — no SVG, no filter. */
.cs-card-ring {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  padding: 1px;
  pointer-events: none;
  opacity: .55;
  background: conic-gradient(from var(--a, 0deg),
      transparent 0deg,
      rgba(226, 189, 118, .95) 26deg,
      rgba(143, 211, 255, .85) 52deg,
      transparent 90deg,
      transparent 360deg);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  -webkit-mask-composite: xor;
  animation: cs-ring 6s linear infinite;
}
.cs-gate-card[data-go="present"] .cs-card-ring {
  background: conic-gradient(from var(--a, 0deg),
      transparent 0deg,
      rgba(143, 211, 255, .95) 26deg,
      rgba(167, 139, 250, .85) 52deg,
      transparent 90deg,
      transparent 360deg);
  animation-direction: reverse;
}
.cs-gate-card:hover .cs-card-ring { opacity: 1; }

/* `--a` has to be registered for a conic gradient's angle to animate at all;
   without @property this steps rather than sweeps. Browsers without it simply
   get a still ring, which is a perfectly good card edge. */
@property --a {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
@keyframes cs-ring { to { --a: 360deg; } }

/* the spotlight — coordinates come from the pointer, so it only exists on a
   device that has one */
.cs-card-glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity .35s ease;
  background: radial-gradient(14rem 14rem at var(--mx, 50%) var(--my, 50%),
      rgba(255, 236, 190, .18), transparent 68%);
}
.cs-gate-card[data-go="present"] .cs-card-glow {
  background: radial-gradient(14rem 14rem at var(--mx, 50%) var(--my, 50%),
      rgba(160, 210, 255, .20), transparent 68%);
}
.cs-gate-card:hover .cs-card-glow { opacity: 1; }

/* keep the copy above both decorative layers */
.cs-gate-year,
.cs-gate-card b,
.cs-gate-note,
.cs-gate-cta { position: relative; z-index: 2; }

/* --- era emblems --------------------------------------------------------- */
.cs-emblem {
  position: absolute;
  right: clamp(.9rem, 2.4vw, 1.6rem);
  top: clamp(.9rem, 2.4vw, 1.6rem);
  width: clamp(2.6rem, 5vw, 3.6rem);
  aspect-ratio: 1;
  z-index: 1;
  pointer-events: none;
  opacity: .55;
  transition: opacity .4s ease, transform .6s cubic-bezier(.22,.9,.3,1);
}
.cs-gate-card:hover .cs-emblem { opacity: 1; transform: scale(1.08); }

/* 1888 — a brass gear. Teeth are a conic gradient, the hub is a mask. */
.cs-emblem-gear i {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
      #e2bd76 0 5.5%, transparent 5.5% 12.5%,
      #e2bd76 12.5% 18%, transparent 18% 25%,
      #e2bd76 25% 30.5%, transparent 30.5% 37.5%,
      #e2bd76 37.5% 43%, transparent 43% 50%,
      #e2bd76 50% 55.5%, transparent 55.5% 62.5%,
      #e2bd76 62.5% 68%, transparent 68% 75%,
      #e2bd76 75% 80.5%, transparent 80.5% 87.5%,
      #e2bd76 87.5% 93%, transparent 93% 100%);
  animation: cs-gear 11s linear infinite;
}
.cs-emblem-gear i:last-child {
  inset: 22%;
  background: radial-gradient(circle, transparent 38%, rgba(226, 189, 118, .85) 39% 78%, transparent 79%);
  animation-direction: reverse;
  animation-duration: 7s;
}
@keyframes cs-gear { to { transform: rotate(360deg); } }

/* 2026 — two bodies on tilted orbits around a small sun */
.cs-emblem-orbit i {
  position: absolute;
  inset: 0;
  border: 1px solid rgba(143, 211, 255, .5);
  border-radius: 50%;
  animation: cs-gear 9s linear infinite;
}
.cs-emblem-orbit i:nth-child(2) { inset: 20%; animation-duration: 5.5s; animation-direction: reverse; }
.cs-emblem-orbit i::after {
  content: "";
  position: absolute;
  top: -3px; left: 50%;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: #8fd3ff;
  box-shadow: 0 0 12px 2px rgba(143, 211, 255, .8);
}
.cs-emblem-orbit i:nth-child(2)::after { background: #c3b0ff; box-shadow: 0 0 12px 2px rgba(167,139,250,.8); }
.cs-emblem-orbit b {
  position: absolute;
  inset: 41%;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #fff, #e2bd76 45%, #b06a1c);
  box-shadow: 0 0 18px 4px rgba(226, 189, 118, .45);
}

/* the year glows on hover */
.cs-gate-year { transition: text-shadow .4s ease, letter-spacing .4s ease; }
.cs-gate-card:hover .cs-gate-year { letter-spacing: .02em; text-shadow: 0 0 26px currentColor; }

/* the click: one expanding ring from where the pointer landed, so the choice
   registers before the page changes under it */
.cs-ripple {
  position: absolute;
  z-index: 3;
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, rgba(255,255,255,.5), transparent 70%);
  animation: cs-ripple .62s ease-out forwards;
}
@keyframes cs-ripple {
  to { transform: scale(34); opacity: 0; }
}

/* the ticking line under the cards */
.cs-gate-small { font-variant-numeric: tabular-nums; }
.cs-gate-small .tick { color: rgba(143, 211, 255, .75); }

/* "Reduce motion" slows the door's ambient loops rather than stopping them;
   the letters, the tilt and the ripple are all responses to something the
   visitor did, and stay. `.calm` is set on <html> by chronoshell-stage.js. */
.calm #cs-warp::after { animation-duration: 26s; }
.calm .cs-card-ring { animation-duration: 20s; }
.calm .cs-emblem-gear i { animation-duration: 34s; }
.calm .cs-emblem-gear i:last-child { animation-duration: 22s; }
.calm .cs-emblem-orbit i { animation-duration: 28s; }
.calm .cs-emblem-orbit i:nth-child(2) { animation-duration: 17s; }
.calm .cs-gate-title { animation-duration: 20s; }

/* a touch screen has no pointer to follow, so the spotlight and the tilt are
   simply not part of the design there */
@media (hover: none) {
  .cs-card-glow { display: none; }
  .cs-gate-card { transform: none; }
}

/* ===========================================================================
   THE FRONT DOOR, THIRD PASS — the galaxy, and the things you can grab

   Two problems with the second pass, both visible on a wide screen:

   1. The title was invisible. `-webkit-background-clip: text` on the heading
      stopped clipping its letters the moment each letter became a transformed
      inline-block — a transform gives the child its own paint layer, outside
      the parent's clip, so every glyph rendered at `color: transparent`. The
      gradient is gone; the letters are solid now and the light travels across
      them as a staggered glow instead, which cannot break the same way.

   2. It did not feel like a gate at the crux of space and time. There is a
      painted galaxy behind it now (see #cs-sky in chronoshell-stage.js) and
      the cards can be picked up and thrown.
   ========================================================================= */

#cs-sky {
  position: absolute;
  inset: 0;
  /* A canvas is a replaced element: with `left:0; right:0; width:auto` the
     used width comes from its intrinsic size — the width attribute — not from
     the insets. Without these the resize observer fed the bitmap size back
     into the layout size and the canvas grew without bound (6825px on a
     624px window before this was caught). */
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
}

/* the aurora sits under the galaxy rather than over it */
#cs-warp::before { z-index: 0; }
#cs-warp::after { z-index: 2; opacity: .32; }
.cs-gate { z-index: 5; }

/* --- title --------------------------------------------------------------
   Solid ink, and a wave of light that crosses it letter by letter. No
   background-clip anywhere near a transformed child. --- */
.cs-gate-title {
  background: none;
  color: #fff6e4;
  -webkit-text-fill-color: currentColor;
  animation: none;
  text-shadow: 0 0 42px rgba(143, 211, 255, .28);
}

.cs-gate-title .ltr {
  display: inline-block;
  opacity: 0;
  transform: translateY(.55em) rotateX(-70deg);
  transition: transform .35s cubic-bezier(.2, .8, .25, 1), color .35s ease;
  animation:
    cs-drop .62s cubic-bezier(.2, .8, .25, 1) forwards,
    cs-lit 5.4s ease-in-out infinite;
  /* cs-drop owns the first 0.62s; the glow wave starts after the letter has
     landed, hence the second delay being set inline alongside the first */
}
.cs-gate-title .ltr.sp { width: .34em; }

@keyframes cs-lit {
  0%, 82%, 100% { color: #fff6e4; text-shadow: none; }
  90% {
    color: #ffffff;
    text-shadow: 0 0 22px rgba(255, 230, 176, .9), 0 0 54px rgba(143, 211, 255, .55);
  }
}

/* every letter is its own little target */
.cs-gate-title .ltr:hover {
  transform: translateY(-7px) scale(1.06);
  color: #ffe6b0;
}

.calm .cs-gate-title .ltr { animation-duration: .62s, 16s; }

/* --- cards you can pick up ----------------------------------------------
   `--dx/--dy` are written by the drag handler; the tilt keeps its own vars,
   so a card being thrown and a card being pointed at compose instead of
   fighting. --- */
.cs-gate-card {
  transform:
    perspective(1000px)
    translate3d(var(--dx, 0px), var(--dy, 0px), 0)
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    rotate(var(--spin, 0deg))
    translateY(var(--lift, 0px))
    scale(var(--sc, 1));
  touch-action: pan-y;          /* vertical scroll still belongs to the page */
  cursor: grab;
}
.cs-gate-card.dragging {
  cursor: grabbing;
  transition: none;             /* follow the finger exactly while held */
  z-index: 3;
  --sc: 1.03;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .4),
    inset 0 0 0 1px rgba(255, 255, 255, .14),
    0 40px 90px rgba(0, 0, 0, .6);
}
/* the throw home: slower and springier than the pointer tilt */
.cs-gate-card.settling {
  transition: transform .85s cubic-bezier(.18, 1.4, .36, 1), box-shadow .5s ease;
}

/* --- the ticking line ---------------------------------------------------- */
.cs-gate-small { min-height: 2.6em; }

/* ===========================================================================
   THE PHONE

   Everything above is written for a pointer. This is the same door for a
   thumb: one column, bigger targets, the emblem moved out of the corner so it
   stops colliding with the year, and type that does not shrink below 15px.
   ========================================================================= */
@media (max-width: 820px) {
  .cs-gate {
    width: min(30rem, 100%);
    padding-inline: 1.1rem;
  }

  .cs-gate-eyebrow { font-size: 10px; letter-spacing: .26em; }
  .cs-gate-title { font-size: clamp(28px, 8.4vw, 42px); margin-bottom: 1.3rem; }

  .cs-gate-cards { gap: .85rem; }

  .cs-gate-card {
    padding: 1.15rem 1.15rem 1.25rem;
    border-radius: 18px;
    /* the emblem used to sit top-right, on top of the year at this width */
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "year emblem"
      "name name"
      "note note"
      "cta  cta";
    align-items: center;
    gap: .3rem .6rem;
  }
  .cs-gate-card .cs-gate-year { grid-area: year; font-size: clamp(30px, 9vw, 40px); }
  .cs-gate-card b { grid-area: name; font-size: 1.02rem; }
  .cs-gate-card .cs-gate-note { grid-area: note; font-size: .9rem; line-height: 1.45; }
  .cs-gate-card .cs-gate-cta {
    grid-area: cta;
    justify-self: start;
    margin-top: .7rem;
    padding: .62rem 1.05rem;    /* a 44px-tall target, which is the point */
    font-size: .74rem;
  }

  .cs-emblem {
    grid-area: emblem;
    /* `relative`, not `static`: the gear and the orbit rings inside are
       `position: absolute; inset: 0`, so a static parent hands them the card
       as their containing block and the emblem fills the whole panel */
    position: relative;
    inset: auto;
    width: 2.5rem;
    opacity: .8;
  }

  .cs-gate-small { font-size: 10px; letter-spacing: .08em; line-height: 1.7; }
}

@media (max-width: 380px) {
  .cs-gate-title { font-size: 26px; }
  .cs-gate-note { font-size: .86rem; }
}

/* a short landscape phone: the door has to fit between two thumbs */
@media (max-height: 520px) and (orientation: landscape) {
  .cs-gate-title { font-size: 24px; margin-bottom: .7rem; }
  .cs-gate-note { display: none; }
  .cs-gate-cards { grid-template-columns: 1fr 1fr; }
}

/* ===========================================================================
   THE 1888 CARD — candlelight

   The 2026 card is glass and starlight and needs nothing. This one should feel
   like the door to somewhere with a library in it: warm parchment behind the
   glass, gilt corner brackets, embers drifting up off the panel, and a candle
   that never quite holds still.

   Still all CSS — seven spans and two pseudo-elements. It costs one composited
   layer for the embers and nothing else.
   ========================================================================= */
.cs-gate-card[data-go="past"] {
  background:
    radial-gradient(120% 90% at 18% 8%, rgba(255, 214, 148, .13), transparent 62%),
    radial-gradient(90% 70% at 92% 96%, rgba(150, 84, 30, .16), transparent 66%),
    linear-gradient(160deg, rgba(255, 238, 208, .11), rgba(120, 82, 44, .05) 46%, rgba(40, 26, 14, .06));
  animation: cs-candle 6.5s ease-in-out infinite;
}
.cs-gate-card[data-go="past"].dragging { animation: none; }

/* the flame is never steady, and never on a metronome either — the keyframes
   are deliberately uneven so it does not read as a pulse */
@keyframes cs-candle {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(255, 236, 196, .3), inset 0 0 0 1px rgba(226, 189, 118, .10), 0 22px 50px rgba(0, 0, 0, .55), 0 0 34px -12px rgba(240, 173, 87, .45); }
  17%      { box-shadow: inset 0 1px 0 rgba(255, 236, 196, .42), inset 0 0 0 1px rgba(226, 189, 118, .16), 0 22px 50px rgba(0, 0, 0, .55), 0 0 52px -10px rgba(240, 173, 87, .7); }
  29%      { box-shadow: inset 0 1px 0 rgba(255, 236, 196, .28), inset 0 0 0 1px rgba(226, 189, 118, .09), 0 22px 50px rgba(0, 0, 0, .55), 0 0 30px -14px rgba(240, 173, 87, .4); }
  53%      { box-shadow: inset 0 1px 0 rgba(255, 236, 196, .48), inset 0 0 0 1px rgba(226, 189, 118, .2), 0 22px 50px rgba(0, 0, 0, .55), 0 0 62px -8px rgba(240, 173, 87, .8); }
  71%      { box-shadow: inset 0 1px 0 rgba(255, 236, 196, .3), inset 0 0 0 1px rgba(226, 189, 118, .11), 0 22px 50px rgba(0, 0, 0, .55), 0 0 36px -13px rgba(240, 173, 87, .5); }
}
.calm .cs-gate-card[data-go="past"] { animation-duration: 20s; }

/* --- gilt corner brackets ------------------------------------------------
   Two elements, four corners: each draws an L at opposite ends of the card. --- */
.cs-filigree {
  position: absolute;
  inset: 11px;
  z-index: 1;
  pointer-events: none;
}
.cs-filigree::before,
.cs-filigree::after {
  content: "";
  position: absolute;
  width: 30px;
  height: 30px;
  border: 1.4px solid rgba(226, 189, 118, .5);
  transition: border-color .5s ease, width .5s ease, height .5s ease;
}
.cs-filigree::before {
  top: 0; left: 0;
  border-right: 0; border-bottom: 0;
  border-top-left-radius: 11px;
}
.cs-filigree::after {
  bottom: 0; right: 0;
  border-left: 0; border-top: 0;
  border-bottom-right-radius: 11px;
}
.cs-filigree.alt::before {
  top: 0; left: auto; right: 0;
  border: 1.4px solid rgba(226, 189, 118, .5);
  border-left: 0; border-bottom: 0;
  border-radius: 0 11px 0 0;
}
.cs-filigree.alt::after {
  bottom: 0; right: auto; left: 0;
  border: 1.4px solid rgba(226, 189, 118, .5);
  border-right: 0; border-top: 0;
  border-radius: 0 0 0 11px;
}
.cs-gate-card[data-go="past"]:hover .cs-filigree::before,
.cs-gate-card[data-go="past"]:hover .cs-filigree::after {
  border-color: rgba(255, 224, 160, .95);
  width: 42px;
  height: 42px;
}

/* --- embers --------------------------------------------------------------
   Seven of them, each with its own column, delay, drift and duration, so the
   pattern never repeats visibly. --- */
.cs-embers {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}
.cs-embers i {
  position: absolute;
  bottom: -8px;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: radial-gradient(circle, #ffe0a8, #f0a94a 55%, rgba(240, 169, 74, 0) 72%);
  box-shadow: 0 0 9px 2px rgba(240, 169, 74, .45);
  opacity: 0;
  animation: cs-ember 8s linear infinite;
}
.cs-embers i:nth-child(1) { left:  8%; --wx:  16px; animation-duration: 7.4s;  animation-delay: 0s; }
.cs-embers i:nth-child(2) { left: 21%; --wx: -14px; animation-duration: 9.1s;  animation-delay: 1.3s; }
.cs-embers i:nth-child(3) { left: 37%; --wx:  22px; animation-duration: 6.8s;  animation-delay: 2.9s; }
.cs-embers i:nth-child(4) { left: 52%; --wx: -19px; animation-duration: 10.2s; animation-delay: .7s; }
.cs-embers i:nth-child(5) { left: 68%; --wx:  12px; animation-duration: 8.3s;  animation-delay: 4.1s; }
.cs-embers i:nth-child(6) { left: 81%; --wx: -24px; animation-duration: 7.7s;  animation-delay: 3.2s; }
.cs-embers i:nth-child(7) { left: 93%; --wx:  10px; animation-duration: 9.6s;  animation-delay: 5.4s; }

@keyframes cs-ember {
  0%   { transform: translate3d(0, 0, 0) scale(.5); opacity: 0; }
  12%  { opacity: .95; }
  65%  { opacity: .6; }
  100% { transform: translate3d(var(--wx, 14px), -230px, 0) scale(1.1); opacity: 0; }
}
.calm .cs-embers i { animation-duration: 22s; }

/* the year, lit like gilt lettering on a spine */
.cs-gate-card[data-go="past"] .cs-gate-year {
  color: #f7e3bb;
  text-shadow: 0 1px 0 rgba(120, 74, 26, .5), 0 0 30px rgba(240, 173, 87, .35);
}

/* on a phone the embers have less height to travel, and seven of them in a
   short card reads as smoke rather than sparks */
@media (max-width: 820px) {
  .cs-embers i:nth-child(n + 5) { display: none; }
  @keyframes cs-ember {
    0%   { transform: translate3d(0, 0, 0) scale(.5); opacity: 0; }
    14%  { opacity: .9; }
    100% { transform: translate3d(var(--wx, 12px), -150px, 0) scale(1.05); opacity: 0; }
  }
}

/* ===========================================================================
   §14 THE SPACE-TIME LOOP

   The tunnel artifact is 500 KB of WebGL in front of an 8-12 MB video. On a
   cold cache or a slow line it can spend a long time unpacking, and until it
   has finished it paints its own opaque "Unpacking…" screen — which sat on
   z-index 2, over the copy, so the whole wait read as a broken page.

   So the run has three phases, carried on `data-phase` on `.cs-warp-run`:

     wait    the loop is painted over the artifact. However long this takes,
             something is visibly moving and the copy is readable.
     travel  the artifact has reported a year, so it is really running: the
             loop fades away and the real journey is revealed.
     ready   arrived.

   Everything here is transform/opacity on a single canvas plus one blurred
   glow, so it stays on the compositor and can run for minutes on a phone.
   ========================================================================= */

.cs-vortex {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity .9s ease;
  pointer-events: none;
}

/* while the artifact unpacks, the loop and the copy sit ABOVE it (the frame
   is z-index 2) — the skip button stays above both at z-index 3 so the way
   out is reachable for the whole of the wait, however long the wait is. */
.cs-warp-run[data-phase="wait"] .cs-warp-tunnel { z-index: 2; }
.cs-warp-run[data-phase="wait"] .cs-warp-copy   { z-index: 2; }
.cs-warp-run[data-phase="wait"] .cs-vortex      { opacity: 1; }

/* the loop's own backdrop, so the artifact's loading screen never shows
   through it. Painted on the tunnel box, which is already inset: 0. */
.cs-warp-run[data-phase="wait"] .cs-warp-tunnel::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(60% 60% at 50% 50%, rgba(24, 16, 8, .55), rgba(6, 5, 9, .96) 72%),
    #06050a;
}

/* once it is genuinely travelling the artifact is the thing worth looking at */
.cs-warp-run[data-phase="travel"] .cs-vortex,
.cs-warp-run[data-phase="ready"]  .cs-vortex { opacity: 0; }

/* a slow halo behind the canvas so the middle of the loop glows rather than
   sitting flat. One element, one transform loop. */
.cs-warp-run[data-phase="wait"] .cs-warp-tunnel::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 46vmin;
  height: 46vmin;
  margin: -23vmin 0 0 -23vmin;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(226, 189, 118, .30), rgba(120, 190, 255, .12) 45%, transparent 70%);
  filter: blur(14px);
  animation: cs-loop-breathe 6.5s ease-in-out infinite;
  will-change: transform, opacity;
}

@keyframes cs-loop-breathe {
  0%, 100% { transform: scale(.92); opacity: .55; }
  50%      { transform: scale(1.12); opacity: .95; }
}

.calm .cs-warp-run[data-phase="wait"] .cs-warp-tunnel::before { animation-duration: 16s; }

/* the wait line under the bar — only present while unpacking */
.cs-warp-wait {
  display: block;
  margin-top: .7em;
  font-family: ui-monospace, monospace;
  font-size: clamp(9px, .8vw, 12px);
  letter-spacing: .12em;
  color: rgba(244, 226, 190, .38);
}

.cs-warp-run:not([data-phase="wait"]) .cs-warp-wait { visibility: hidden; }

/* the skip button must never be the thing that is hard to find during a long
   wait, so it is given a little more presence than it had. */
.cs-warp-skip { z-index: 5; }

/* ===========================================================================
   THE REFUSAL — a phone that travelled and got turned around

   Deliberately inside `.cs-warp-copy` rather than as a new overlay: the run
   stays visible, so Dobby's context stays `tunnel` and he keeps talking over
   it without a line of code knowing this screen exists.
   ========================================================================= */
.cs-warp-run[data-phase="refused"] .cs-warp-tunnel,
.cs-warp-run[data-phase="refused"] .cs-warp-frame,
.cs-warp-run[data-phase="refused"] .cs-warp-skip { opacity: 0; pointer-events: none; }

.cs-warp-run[data-phase="refused"] .cs-warp-copy {
  z-index: 4;
  width: min(34rem, 92vw);
  text-align: center;
  animation: cs-refuse-in .6s cubic-bezier(.2, .8, .25, 1) both;
}

@keyframes cs-refuse-in {
  from { opacity: 0; transform: translateY(14px) scale(.97); }
  to   { opacity: 1; transform: none; }
}

.cs-refuse-eyebrow {
  margin: 0 0 .7rem;
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: 10px;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: rgba(143, 211, 255, .8);
}

.cs-refuse-title {
  margin: 0 0 1rem;
  font-family: "IM Fell English", Georgia, serif;
  font-weight: 400;
  font-size: clamp(21px, 6vw, 32px);
  line-height: 1.16;
  color: #fff6e4;
  text-shadow: 0 0 40px rgba(143, 211, 255, .3);
  text-wrap: balance;
}

.cs-refuse-note {
  margin: 0 auto .8rem;
  max-width: 36ch;
  font-family: "IM Fell English", Georgia, serif;
  font-size: .95rem;
  line-height: 1.5;
  color: rgba(246, 236, 216, .78);
}

.cs-refuse-count {
  margin: 1.2rem 0 .2rem;
  font-family: ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(246, 236, 216, .5);
}
.cs-refuse-count b {
  color: #8fd3ff;
  font-variant-numeric: tabular-nums;
}

.cs-refuse-go {
  display: inline-block;
  margin-top: .9rem;
  padding: .72rem 1.5rem;
  font: inherit;
  font-family: ui-monospace, monospace;
  font-size: .76rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #08111d;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(120deg, #8fd3ff, #b9a6ff);
  box-shadow: 0 10px 28px rgba(143, 211, 255, .3);
  cursor: pointer;
}

/* ===========================================================================
   THE WAY OUT OF 1888

   present.html carries a "↺ 1888" link in its top-right corner; this is the
   same control pointing the other way, so the two eras are reachable from each
   other rather than only from the front door.

   Fixed to the window, not the stage: the stage is deliberately wider than the
   viewport and crops at the edges, and a control that crops is a control that
   is sometimes not there. z-index sits above the hotspot layer (4) and below
   the certificate plate and the book (20), so it never covers what a visitor
   has deliberately opened.
   ========================================================================= */
.cs-era-out {
  position: fixed;
  top: max(.9rem, env(safe-area-inset-top));
  right: max(.9rem, env(safe-area-inset-right));
  z-index: 12;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .46rem .9rem;
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: .72rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  text-decoration: none;
  color: #f0dcae;
  border: 1px solid rgba(226, 189, 118, .38);
  border-radius: 999px;
  background: rgba(24, 17, 9, .5);
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .45);
  opacity: .62;
  transition: opacity .3s ease, background .3s ease, border-color .3s ease, transform .3s cubic-bezier(.22,.9,.3,1);
}
.cs-era-out:hover,
.cs-era-out:focus-visible {
  opacity: 1;
  transform: translateY(-2px);
  background: rgba(52, 34, 14, .72);
  border-color: rgba(255, 224, 160, .8);
}

/* Hidden until the room is there, and hidden again while the shell is
   expanded — the expanded shell owns the whole window and a floating link over
   it reads as part of the shell's own chrome, which it is not.

   Both states are plain classes toggled from JS. They were `body:has(...)`
   selectors for one commit and that was a real mistake: a `:has()` anchored on
   <body> is re-evaluated against the whole subtree on every DOM mutation, and
   the tunnel artifact rewrites its year text every frame. The traversal went
   from eight seconds to seventy. */
.cs-era-out { display: none; }
body.cs-arrived .cs-era-out { display: inline-flex; }
body.cs-arrived.cs-shell-open .cs-era-out { display: none; }

@media (max-width: 820px) {
  .cs-era-out { font-size: .68rem; padding: .42rem .78rem; }
}

/* ===========================================================================
   THE PREFLIGHT READOUT

   Percentage, measured throughput, time remaining. It says what it is doing
   and how long it will take, because the honest answer to "why is this slow"
   is usually "your connection", and a bar that admits that is less annoying
   than one that pretends.
   ========================================================================= */
.cs-load {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .7rem;
  margin-top: .9rem;
}

.cs-load-meta {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: rgba(246, 236, 216, .5);
}
.cs-load-pct { color: #8fd3ff; font-size: 13px; letter-spacing: .1em; }
.cs-load-speed { color: rgba(226, 189, 118, .8); }

.cs-load-sound {
  /* One line. "SOUND ON" was breaking after "SOUND" and drawing "ON" below
     its own pill, the same way the deck's chips were. */
  white-space: nowrap;
  padding: .34rem .8rem;
  font: inherit;
  font-family: ui-monospace, monospace;
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(246, 236, 216, .6);
  background: rgba(255, 255, 255, .04);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 999px;
  cursor: pointer;
  transition: color .25s ease, border-color .25s ease, background .25s ease;
}
.cs-load-sound[aria-pressed="true"] {
  color: #8fd3ff;
  border-color: rgba(143, 211, 255, .5);
  background: rgba(143, 211, 255, .12);
}

/* While preflighting there is no corridor behind the copy yet — only the
   cosmos loop — so the artifact is kept out of the way entirely rather than
   sitting at zero behind it. */
.cs-warp-run[data-phase="loading"] .cs-warp-frame { opacity: 0; pointer-events: none; }
.cs-warp-run[data-phase="loading"] .cs-warp-tunnel { z-index: 2; }
.cs-warp-run[data-phase="loading"] .cs-warp-copy { z-index: 3; }
.cs-warp-run[data-phase="loading"] .cs-vortex { opacity: 1; }

/* ===========================================================================
   THE LOADING SCREEN, PROPERLY

   The bar was a hairline because it used to be decoration over a fake
   estimate. It is now the only thing on screen that is telling the truth, so
   it is the size of that job: wide, centre of the frame, the line above it,
   the percentage under it, and the throughput and time remaining under that.

   The corridor is kept hidden for the whole of `loading` AND `wait`. It boots
   behind this, and until it is actually moving it is a still clock face —
   which is precisely what a visitor should never be shown.
   ========================================================================= */
.cs-warp-run[data-phase="loading"] .cs-warp-frame,
.cs-warp-run[data-phase="wait"] .cs-warp-frame {
  opacity: 0;
  pointer-events: none;
}

.cs-warp-run[data-phase="loading"] .cs-warp-line {
  font-size: clamp(14px, 2.1vw, 20px);
  line-height: 1.35;
  max-width: 32ch;
  margin: 0 auto .95rem;
  min-height: 2.7em;
  color: #fff6e4;
}

/* the sub-line sits between the bar and the number and crowds both */
.cs-warp-run[data-phase="loading"] .cs-warp-sub,
.cs-warp-run[data-phase="loading"] .cs-warp-wait { display: none; }

.cs-warp-run[data-phase="loading"] .cs-warp-bar {
  width: min(34rem, 84vw);
  /* border-box is set for this whole subtree, so the padding and border come
     out of this: 26 - 8 - 2 leaves a 16px fill, which is the size the bar
     needs to read as a bar rather than as a rule */
  height: 26px;
  margin: 0 auto;
  padding: 4px;
  border-radius: 999px;
  border: 1px solid rgba(143, 211, 255, .34);
  background: rgba(255, 255, 255, .045);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .5), 0 0 26px -8px rgba(143, 211, 255, .5);
  overflow: hidden;
}

.cs-warp-run[data-phase="loading"] .cs-warp-bar span {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, #e2bd76, #8fd3ff 55%, #b9a6ff);
  box-shadow: 0 0 16px rgba(143, 211, 255, .75);
  transition: width .28s ease-out;
  /* the notched edge of the reference: a repeating gradient over the fill,
     so it reads as segments filling rather than a plain sweep */
  background-image:
    repeating-linear-gradient(115deg, rgba(0, 0, 0, .18) 0 6px, transparent 6px 13px),
    linear-gradient(90deg, #e2bd76, #8fd3ff 55%, #b9a6ff);
}

.cs-warp-run[data-phase="loading"] .cs-load { margin-top: 1rem; gap: .5rem; }

.cs-warp-run[data-phase="loading"] .cs-load-pct {
  font-size: clamp(22px, 4vw, 32px);
  letter-spacing: .06em;
  color: #fff6e4;
  font-weight: 500;
}

.cs-warp-run[data-phase="loading"] .cs-load-meta {
  flex-direction: column;
  gap: .35rem;
}

.cs-warp-run[data-phase="loading"] .cs-load-rate {
  display: flex;
  gap: 1.1rem;
  font-size: 11px;
  letter-spacing: .14em;
}

/* The way out stays live during loading. It used to be disabled here on the
   grounds that there was nothing to skip to yet — which made the longest phase
   also the only inescapable one, and that is exactly the wrong pairing for the
   phase most likely to stall. */
.cs-warp-run[data-phase="loading"] .cs-warp-skip { opacity: .55; }
.cs-warp-run[data-phase="loading"] .cs-warp-skip:hover { opacity: 1; }

@media (max-width: 820px) {
  .cs-warp-run[data-phase="loading"] .cs-warp-bar { height: 13px; }
}

/* ===========================================================================
   MORE WEATHER, DURING THE WAIT ONLY

   The front door's galaxy is still painting behind all of this. These are four
   slow, heavily blurred blobs over the top of it, present only while the
   loading screen is, so the wait is the same sky with more going on in it.
   Composited transforms and opacity only — nothing here costs layout, which
   matters because the corridor is compiling behind it.
   ========================================================================= */
.cs-load-neb {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  overflow: hidden;
  contain: paint;
  opacity: 0;
  transition: opacity 1.2s ease;
}
.cs-warp-run[data-phase="loading"] .cs-load-neb { opacity: 1; }

.cs-load-neb i {
  position: absolute;
  border-radius: 50%;
  /* 44, not 60: the same look for meaningfully less fill rate. No
     `will-change` — these already animate a transform, so the compositor
     promotes them when it needs to, and pinning four large blurred layers by
     hand was adding pressure for nothing. */
  filter: blur(44px);
}
.cs-load-neb i:nth-child(1) {
  width: 46vmax; height: 46vmax; left: -12vmax; top: -10vmax;
  background: radial-gradient(circle, rgba(143, 211, 255, .30), transparent 68%);
  animation: cs-neb-a 34s ease-in-out infinite alternate;
}
.cs-load-neb i:nth-child(2) {
  width: 40vmax; height: 40vmax; right: -10vmax; top: 4vmax;
  background: radial-gradient(circle, rgba(226, 189, 118, .26), transparent 68%);
  animation: cs-neb-b 41s ease-in-out infinite alternate;
}
.cs-load-neb i:nth-child(3) {
  width: 52vmax; height: 52vmax; left: 18vmax; bottom: -22vmax;
  background: radial-gradient(circle, rgba(167, 139, 250, .28), transparent 68%);
  animation: cs-neb-a 47s ease-in-out infinite alternate-reverse;
}
.cs-load-neb i:nth-child(4) {
  width: 34vmax; height: 34vmax; right: 12vmax; bottom: -12vmax;
  background: radial-gradient(circle, rgba(80, 200, 190, .22), transparent 68%);
  animation: cs-neb-b 29s ease-in-out infinite alternate;
}

@keyframes cs-neb-a {
  0%   { transform: translate3d(0, 0, 0) scale(1); }
  100% { transform: translate3d(9vmax, 6vmax, 0) scale(1.22); }
}
@keyframes cs-neb-b {
  0%   { transform: translate3d(0, 0, 0) scale(1.1); }
  100% { transform: translate3d(-8vmax, -5vmax, 0) scale(1); }
}

.calm .cs-load-neb i { animation-duration: 90s; }

/* A phone has a fraction of the fill rate and none of the memory headroom.
   Four animating half-screen blurs was what pushed the loading card's own
   tile out of the compositor. Two, smaller, softer - the sky still moves. */
@media (max-width: 820px) {
  .cs-load-neb i { filter: blur(30px); }
  .cs-load-neb i:nth-child(3),
  .cs-load-neb i:nth-child(4) { display: none; }
  .cs-load-neb i:nth-child(1) { width: 34vmax; height: 34vmax; }
  .cs-load-neb i:nth-child(2) { width: 30vmax; height: 30vmax; }
}

/* The vortex rings were competing with the copy they sit behind — the year
   pair and the first line of the witty rotation were both hard to read
   through them. During loading the loop is background, not subject: it is
   dimmed, and the copy gets a soft plate of its own so it stays legible over
   whichever part of the rings happens to pass under it. */
.cs-warp-run[data-phase="loading"] .cs-vortex { opacity: .42; }
.cs-warp-run[data-phase="loading"] .cs-warp-tunnel span { opacity: .3; }

.cs-warp-run[data-phase="loading"] .cs-warp-copy {
  padding: 1.6rem clamp(1rem, 4vw, 2.6rem) 1.4rem;
  border-radius: 22px;
  /* Flat, opaque, and on its own compositor layer. It was a radial-gradient
     plate, and on Android Chrome the whole card would vanish mid-download and
     leave a forty-pixel strip of itself behind: a partial raster, not a style
     change. A soft gradient over four animating 50vmax blur(44px) layers is a
     lot of fill for a phone, and when the compositor runs out of room it is
     the transparent, gradient-filled tile that loses. A solid colour on a
     promoted layer rasters once and stays rastered.

     No backdrop-filter either - that was tried and its edge showed as a
     full-width seam on some devices. */
  /* Fully opaque, not .9. A translucent layer still has to be blended
     against everything moving behind it every frame; an opaque one is
     rastered once and composited. That is the difference between a card that
     survives on a mid-range Android and one that drops out mid-download. */
  background: #080b14;
  border: 1px solid rgba(143, 211, 255, .16);
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .8);
  transform: translateZ(0);
  will-change: transform;
}

.cs-warp-run[data-phase="loading"] .cs-warp-years {
  opacity: 1;
  text-shadow: 0 2px 14px rgba(0, 0, 0, .9);
}
.cs-warp-run[data-phase="loading"] .cs-warp-line {
  text-shadow: 0 2px 12px rgba(0, 0, 0, .85);
}

/* ===========================================================================
   THE ROOM DOES NOT EXIST UNTIL YOU ARRIVE

   On a cold phone the 1888 shell was bleeding through the loading screen: the
   boot banner, the session UUID, the ASCII name, the gold fullscreen frame —
   all of it appearing over the corridor and then disappearing again. Which
   element and which z-index was doing it on that device is not worth chasing,
   because the correct answer is the same either way: while the door, the
   loading screen or the corridor is up, the room is not a thing that can
   paint at all.

   `visibility: hidden` and not just `opacity`, because it hides
   position:fixed descendants too — and the shell's own fullscreen rule in the
   legacy stylesheet is exactly that. The video still loads and decodes while
   hidden, so nothing about the preflight changes.

   `body.cs-arrived` is set in enter(), the moment the door is removed.
   ========================================================================= */
.hero-frame {
  visibility: hidden;
  opacity: 0;
}

body.cs-arrived .hero-frame {
  visibility: visible;
  opacity: 1;
  transition: opacity .7s ease;
}

/* No script, no journey — the room is simply the page. */
html:not(.js-warp) .hero-frame {
  visibility: visible;
  opacity: 1;
}
