/* Keys to My Lockheart — Ren'Py-flavoured visual novel shell */

:root {
  --stage-w: 1920;
  --stage-h: 1080;
  --ink: #f3ecff;
  --accent: #d9a5ff;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;

  /* Timings. These are defaults — vn.js overwrites them at boot from the
     consts at the top of that file, so tune them there, not here. */
  --dissolve: 420ms;   /* crossfade between two images in a slot */
  --slide: 620ms;      /* spriteX movement */
  --glow: 200ms;       /* hotspot hover highlight */
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  background: #05030a;
  color: var(--ink);
  font-family: var(--serif);
  overflow: hidden;   /* the stage is fixed; only doc pages scroll */
}

html.doc-page, html.doc-page body {
  height: auto;
  min-height: 100%;
  overflow: visible;
}

/* --- Stage: a 16:9 box letterboxed into the window ---------------------- */

#viewport {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
}

/* Letterbox: fill the window, never crop, never overflow. Whichever of the two
   caps binds first wins, so the stage stays exactly 16:9 at any window shape. */
#stage {
  position: relative;
  aspect-ratio: var(--stage-w) / var(--stage-h);
  width: min(100vw, calc(100dvh * (var(--stage-w) / var(--stage-h))));
  overflow: hidden;
  background: #05030a;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

#stage.is-hotspot { cursor: pointer; }

/* --- Layers ------------------------------------------------------------- */

#layers { position: absolute; inset: 0; }

/* One crossfading pair — the background, or the sprite. See Slot in content.js.
   `isolation` is load-bearing: it confines the plus-lighter addition below to
   the two images in this box, so they blend with each other and not with the
   background sitting behind the slot. */
.slot {
  position: absolute;
  inset: 0;
  isolation: isolate;
}

/* What makes a dissolve opaque. Ordinary alpha compositing of two stacked
   half-visible copies leaves (1-a)(1-b) of the backdrop showing — 25% at the
   midpoint, which on a character sprite reads as the room being visible through
   her. plus-lighter ADDS premultiplied colour and alpha instead, so the two
   halves sum back to a solid picture: alpha 1 over the shared silhouette, and a
   genuine 50/50 of the pixels where the poses differ.

   Where only one of the two covers a pixel, the other contributes nothing and
   that pixel is correctly half-transparent — an arm that is genuinely leaving
   should be genuinely fading.

   A browser without plus-lighter drops the declaration and gets the old
   crossfade, which is dimmer in the middle but not broken. */
.slot .layer { mix-blend-mode: plus-lighter; }

.layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  /* Longhands, not the `transition` shorthand: WebKit has a history of
     rejecting var() inside shorthands, and a rejected declaration drops the
     WHOLE transition — which reads as "no animation at all", not "wrong
     duration". Longhands resolve custom properties reliably. */
  transition-property: opacity, transform, filter;
  transition-duration: var(--dissolve), var(--slide), var(--glow);
  transition-timing-function: ease, cubic-bezier(.22, .61, .36, 1), ease;
  will-change: opacity, transform;
  pointer-events: none;
}

.layer.is-visible { opacity: 1; }

/* Interactive layers glow when the cursor is over their opaque pixels. */
.layer.is-hot {
  filter: brightness(1.25) drop-shadow(0 0 14px rgba(217, 165, 255, .95))
          drop-shadow(0 0 38px rgba(217, 165, 255, .55));
}

@keyframes drift {
  0%, 100% { transform: translate3d(0, calc(var(--drift) * -1), 0); }
  50%      { transform: translate3d(0, var(--drift), 0); }
}

.layer.is-drifting {
  animation: drift var(--drift-dur, 7s) ease-in-out infinite;
}

/* --- Textbox ------------------------------------------------------------ */

#textbox {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 32%;
  min-height: 170px;
  pointer-events: none;
}

/* The gradient plate: black at the bottom, fading up AND out to the sides. */
.textbox-bg {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(3, 1, 8, .94) 0%,
    rgba(3, 1, 8, .82) 38%,
    rgba(3, 1, 8, .48) 68%,
    rgba(3, 1, 8, 0) 100%
  );
  -webkit-mask-image: linear-gradient(to right,
    transparent 0%, #000 16%, #000 84%, transparent 100%);
          mask-image: linear-gradient(to right,
    transparent 0%, #000 16%, #000 84%, transparent 100%);
}

.textbox-inner {
  position: absolute;
  inset: auto 0 0 0;
  padding: 0 12% 4.5%;
  max-width: 1500px;
  margin: 0 auto;
}

#speaker {
  font-size: clamp(15px, 1.55vw, 26px);
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent);
  text-shadow: 0 2px 10px rgba(0, 0, 0, .9);
  margin-bottom: .5em;
}

#dialogue {
  font-size: clamp(16px, 1.75vw, 30px);
  line-height: 1.62;
  text-shadow: 0 2px 12px rgba(0, 0, 0, .95), 0 0 32px rgba(0, 0, 0, .7);
  min-height: 3.2em;
}

.caret {
  display: inline-block;
  width: .06em;
  height: 1.05em;
  margin-left: .04em;
  background: var(--ink);
  vertical-align: text-bottom;
  animation: blink 1s steps(2, start) infinite;
}

@keyframes blink { to { visibility: hidden; } }

#advance {
  position: absolute;
  right: 12%;
  bottom: 1.4em;
  font-size: clamp(13px, 1.1vw, 20px);
  color: var(--accent);
  animation: bob 1.1s ease-in-out infinite;
}

@keyframes bob {
  0%, 100% { transform: translateY(0); opacity: .55; }
  50%      { transform: translateY(4px); opacity: 1; }
}

/* --- Tooltip for hotspots ---------------------------------------------- */

#tooltip {
  position: absolute;
  z-index: 5;
  padding: .42em .85em;
  font-size: clamp(12px, 1.05vw, 19px);
  letter-spacing: .05em;
  color: var(--ink);
  background: rgba(6, 2, 14, .82);
  border: 1px solid rgba(217, 165, 255, .45);
  border-radius: 3px;
  white-space: nowrap;
  pointer-events: none;
  transform: translate(-50%, -160%);
  backdrop-filter: blur(3px);
}

#tooltip[hidden] { display: none; }

/* --- Choices ------------------------------------------------------------ */

/* Centred in the frame ABOVE the textbox, not in the whole frame. The textbox
   is the bottom 32%, and a menu centred over all of it sits low enough to be
   half-behind the line it is answering. Centring in what is left also walks the
   stack up off the speaker's face, which is what a full-frame sprite keeps
   dead-centre — and it stays centred as choices are added rather than growing
   off one end. */
#choices {
  position: absolute;
  inset: 0 0 32% 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2% 0;
  gap: 1em;
  z-index: 4;
}

#choices[hidden] { display: none; }

.choice {
  font-family: var(--serif);
  font-size: clamp(15px, 1.5vw, 26px);
  color: var(--ink);
  background: rgba(6, 2, 14, .74);
  border: 1px solid rgba(217, 165, 255, .38);
  border-radius: 3px;
  padding: .7em 2.4em;
  min-width: 44%;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}

.choice:hover:not(:disabled) {
  background: rgba(50, 18, 82, .88);
  border-color: var(--accent);
  transform: scale(1.02);
}

/* A locked choice is one the author wants seen and not taken — so it has to
   read as a real option that is out of reach, not as chrome. Dimmed and struck
   rather than hidden; hiding it is what `if` is for. */
.choice.is-locked {
  opacity: .4;
  cursor: not-allowed;
  border-style: dashed;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

/* A MARKED choice is the opposite case, and must not look like the locked one.
   It is fully live — take it again, read it again — and only says that it has
   already given up what it had. So: no dimming past the point of legibility, no
   strike-through, and the mark is a glyph in the margin rather than a change to
   the label. The ✦ matches the one the achievements screen uses for an earned
   row, because it is reporting the same fact. */
.choice.is-taken {
  position: relative;
  border-color: rgba(217, 165, 255, .22);
  color: rgba(243, 236, 255, .72);
}

.choice.is-taken::before {
  content: "✦";
  position: absolute;
  left: .9em;
  color: var(--accent);
  opacity: .75;
}

.choice.is-taken:hover:not(:disabled) { color: var(--ink); }

/* --- Quick menu (Ren'Py's row along the bottom) ------------------------- */

#quick-menu {
  position: absolute;
  left: 50%;
  bottom: .5%;
  transform: translateX(-50%);
  z-index: 5;
  display: flex;
  gap: .2em;
  align-items: center;
  white-space: nowrap;
}

#quick-menu button,
#quick-menu a {
  font-family: var(--serif);
  font-size: clamp(10px, .85vw, 15px);
  letter-spacing: .06em;
  color: var(--ink);
  text-decoration: none;
  background: none;
  border: 0;
  padding: .45em .85em;
  border-radius: 2px;
  cursor: pointer;
  opacity: .45;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .95);
  transition: opacity 140ms ease, color 140ms ease, background 140ms ease;
}

#quick-menu button:hover:not(:disabled),
#quick-menu a:hover {
  opacity: 1;
  color: var(--accent);
  background: rgba(6, 2, 14, .6);
}

#quick-menu button:disabled {
  opacity: .16;
  cursor: default;
}

/* A toggle that is ON has to read as ON with the pointer nowhere near it, so it
   borrows the hover treatment and keeps it. The underline is the part that
   survives a reader who cannot separate accent from ink. */
#quick-menu button[aria-pressed="true"] {
  opacity: 1;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: .3em;
}

/* --- Chapter nav -------------------------------------------------------- */

/* The ☰ glyph is three horizontal lines and so is a bookshelf — at any real
   transparency the library reads straight through the button and the icon
   vanishes into the shelves. So the plate is opaque rather than tinted, and a
   dark outer ring separates it from bright art the way a drop shadow can't. */
#nav-toggle {
  position: absolute;
  top: 2.2%;
  right: 2.2%;
  z-index: 5;
  width: 2.6em;
  height: 2.6em;
  font-size: clamp(15px, 1.2vw, 22px);
  line-height: 1;
  color: var(--accent);
  background: rgba(8, 3, 18, .93);
  border: 1px solid rgba(217, 165, 255, .62);
  border-radius: 4px;
  cursor: pointer;
  opacity: 1;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55),
              0 4px 18px rgba(0, 0, 0, .8);
  text-shadow: 0 0 12px rgba(217, 165, 255, .55);
  transition: background 160ms ease, border-color 160ms ease,
              box-shadow 160ms ease, transform 160ms ease;
}

#nav-toggle:hover,
#nav-toggle:focus-visible,
#nav-toggle[aria-expanded="true"] {
  background: rgba(62, 23, 100, .97);
  border-color: var(--accent);
  color: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55),
              0 4px 22px rgba(0, 0, 0, .85),
              0 0 22px rgba(217, 165, 255, .5);
  transform: translateY(-1px);
}

/* The ☰ menu is an authored screen now (content/overlays/navigation.html); it
   borrows the overlay panel chrome and only needs its lists and buttons styled.
   The panel positioning, backdrop and close button all come from #overlay. */
#overlay .nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Separates chapters from screens from pages from outbound links. */
#overlay .nav-group {
  margin-top: .35em;
  padding-top: .35em;
  border-top: 1px solid rgba(217, 165, 255, .18);
}

#overlay .nav-list a,
#overlay .nav-action,
#overlay .nav-restart {
  display: block;
  width: 100%;
  text-align: left;
  font-family: var(--serif);
  font-size: clamp(12px, 1.05vw, 18px);
  color: var(--ink);
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: 2px;
  padding: .5em .7em;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}

#overlay .nav-list a:hover,
#overlay .nav-action:hover:not(:disabled),
#overlay .nav-restart:hover { background: rgba(50, 18, 82, .9); color: #fff; }

#overlay .nav-action:disabled { opacity: .3; cursor: default; }

/* --- Savegames screen -------------------------------------------------- */

#overlay .saves-top { margin-bottom: .6em; }

#overlay .saves-top .nav-action {
  text-align: center;
  border: 1px solid rgba(217, 165, 255, .28);
  border-radius: 3px;
}

/* The list is the one part that grows without bound, so it — and only it —
   scrolls, inside a fixed height, while the Save and Download/Upload rows stay
   put above and below. */
#overlay .saves-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 15em;
  overflow-y: auto;
  border: 1px solid rgba(217, 165, 255, .18);
  border-radius: 3px;
}

#overlay .save-row {
  display: flex;
  flex-wrap: wrap;
  gap: .4em 1em;
  align-items: center;
  justify-content: space-between;
  padding: .55em .7em;
  border-top: 1px solid rgba(217, 165, 255, .14);
}

#overlay .save-row:first-child { border-top: 0; }

#overlay .save-meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

#overlay .save-name {
  color: var(--ink);
  font-weight: 600;
  font-size: clamp(12px, 1.05vw, 18px);
}

#overlay .save-name.is-unnamed { font-weight: 400; opacity: .6; }

#overlay .save-when {
  font-size: clamp(10px, .85vw, 14px);
  opacity: .55;
}

#overlay .save-actions { display: flex; flex-wrap: wrap; gap: .3em; }

#overlay .save-btn {
  font-family: var(--serif);
  font-size: clamp(10px, .9vw, 15px);
  color: var(--ink);
  background: rgba(50, 18, 82, .55);
  border: 1px solid rgba(217, 165, 255, .28);
  border-radius: 3px;
  padding: .3em .6em;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}

#overlay .save-btn:hover { background: rgba(62, 23, 100, .95); color: #fff; }

#overlay .save-btn.is-danger { color: #ffb3b3; border-color: rgba(255, 120, 120, .35); }
#overlay .save-btn.is-danger:hover { background: rgba(120, 24, 24, .8); color: #fff; }

/* Download / Upload the whole history, set apart from the per-save buttons. */
#overlay .saves-io {
  display: flex;
  gap: .5em;
  margin-top: .8em;
  padding-top: .8em;
  border-top: 1px solid rgba(217, 165, 255, .18);
}

#overlay .saves-io .nav-action {
  width: auto;
  flex: 1;
  text-align: center;
  border: 1px solid rgba(217, 165, 255, .28);
  border-radius: 3px;
}

/* --- HUD: the always-on panel ------------------------------------------ */

/* Top left: the one corner nothing else claims. ☰ is top right, the quick menu
   is bottom centre, the unlock notice is bottom right. Non-interactive, because
   it is a readout and catching a click here would cost the reader a line. */
#hud {
  position: absolute;
  top: 2.2%;
  left: 2.2%;
  z-index: 5;
  pointer-events: none;
}

#hud[hidden] { display: none; }

.hud-panel {
  padding: .5em .8em;
  background: rgba(8, 3, 18, .82);
  border: 1px solid rgba(217, 165, 255, .38);
  border-radius: 3px;
  font-size: clamp(10px, .85vw, 15px);
  letter-spacing: .05em;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .95);
}

.hud-row {
  display: flex;
  gap: 1.2em;
  justify-content: space-between;
  line-height: 1.7;
}

.hud-label { color: var(--ink); opacity: .6; }
.hud-value { color: var(--accent); }

/* --- Overlay: authored screens ----------------------------------------- */

/* The element is both backdrop and centring context — a click landing on it
   rather than on .panel is a click outside, which is what closes it. */
#overlay {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4%;
  background: rgba(4, 1, 10, .78);
  backdrop-filter: blur(3px);
}

#overlay[hidden] { display: none; }
#overlay:focus { outline: none; }

#overlay .panel {
  position: relative;
  max-width: 46em;
  max-height: 100%;
  overflow-y: auto;
  padding: 1.8em 2em;
  background: rgba(10, 4, 22, .97);
  border: 1px solid rgba(217, 165, 255, .5);
  border-radius: 5px;
  box-shadow: 0 10px 44px rgba(0, 0, 0, .7);
}

#overlay h2 {
  margin: 0 0 .8em;
  font-size: clamp(17px, 1.7vw, 30px);
  color: var(--accent);
  letter-spacing: .04em;
}

#overlay .panel-close {
  position: absolute;
  top: .5em;
  right: .6em;
  font-size: clamp(16px, 1.4vw, 24px);
  line-height: 1;
  color: var(--ink);
  background: none;
  border: 0;
  padding: .2em .35em;
  border-radius: 3px;
  cursor: pointer;
  opacity: .5;
  transition: opacity 140ms ease, color 140ms ease;
}

#overlay .panel-close:hover { opacity: 1; color: var(--accent); }

#overlay .panel-note {
  margin: 1.4em 0 0;
  font-size: clamp(10px, .85vw, 14px);
  opacity: .45;
  cursor: pointer;
}

#overlay .unlocks {
  list-style: none;
  margin: 0;
  padding: 0;
}

#overlay .unlock {
  display: flex;
  gap: .8em;
  align-items: baseline;
  padding: .5em 0;
  border-top: 1px solid rgba(217, 165, 255, .14);
  font-size: clamp(12px, 1.05vw, 18px);
  line-height: 1.5;
}

#overlay .unlock:first-child { border-top: 0; }
#overlay .unlock-mark { color: var(--accent); }
#overlay .unlock b { color: var(--ink); font-weight: 600; }

#overlay .unlock em {
  display: block;
  font-style: normal;
  font-size: .84em;
  opacity: .62;
}

/* A row the reader has not earned states that it exists and no more. */
#overlay .is-locked { opacity: .34; }

/* --- The 18+ gate ------------------------------------------------------- */

/* Narrower than a reading panel and centred, because it is one question with
   two answers — it should read as a door, not as a page. */
#overlay .gate-panel {
  max-width: 30em;
  text-align: center;
}

#overlay .gate-body {
  margin: 0 0 1.6em;
  font-size: clamp(12px, 1.05vw, 17px);
  line-height: 1.6;
  opacity: .8;
}

#overlay .gate-actions {
  display: flex;
  flex-direction: column;
  gap: .7em;
}

/* The two answers are the same size and the same weight. A gate that styles
   "yes" as the button and "no" as a link has picked the answer for the reader,
   which is the one thing a gate is not allowed to do. */
#overlay .gate-actions .choice {
  min-width: 0;
  width: 100%;
  padding: .8em 1.2em;
}

#overlay .gate-note {
  margin-top: 1.4em;
  cursor: default;
}

/* --- Unlock notice ----------------------------------------------------- */

/* Bottom right: out of the textbox's way, out of the quick menu's way, and on
   the side the reader is not reading toward. */
#toast {
  position: absolute;
  right: 2.2%;
  bottom: 8%;
  z-index: 6;
  max-width: 34%;
  padding: .7em .95em;
  background: rgba(8, 3, 18, .93);
  border: 1px solid rgba(217, 165, 255, .62);
  border-left: 3px solid var(--accent);
  border-radius: 4px;
  box-shadow: 0 6px 26px rgba(0, 0, 0, .6);
  opacity: 0;
  transform: translateY(.5em);
  transition: opacity 320ms ease, transform 320ms ease;
  pointer-events: none;
}

#toast[hidden] { display: none; }
#toast.is-up { opacity: 1; transform: none; }

.toast-title {
  font-size: clamp(12px, 1.05vw, 18px);
  color: var(--accent);
  letter-spacing: .04em;
}

.toast-desc {
  margin-top: .25em;
  font-size: clamp(10px, .9vw, 15px);
  color: var(--ink);
  opacity: .82;
  line-height: 1.4;
}

/* The notice slides; readers who asked for no motion get the fade alone. */
@media (prefers-reduced-motion: reduce) {
  #toast { transform: none; transition: opacity 320ms ease; }
}

#overlay .nav-list a[aria-current="page"] {
  color: var(--accent);
  font-weight: 700;
}

/* Anything leaving the site says so before it is clicked. */
#overlay .is-external::after,
.choice.is-external::after {
  content: " ↗";
  opacity: .65;
}

#overlay .nav-restart {
  margin-top: .4em;
  border-top: 1px solid rgba(217, 165, 255, .18);
  padding-top: .7em;
  border-radius: 0;
  opacity: .75;
  font-size: clamp(11px, .95vw, 16px);
}

/* --- Fade-to-black transition ------------------------------------------ */

/* Sits above everything (including the textbox), so a scene change dips the
   whole frame the way Ren'Py's `with fade` does. */
#fade {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0;
  pointer-events: none;
  z-index: 6;
  transition: opacity 260ms ease;
}

#fade.is-black { opacity: 1; }

/* Swapping art while the frame is black must not crossfade — otherwise the
   old scene is still dissolving out as we fade back in. */
.layer.is-instant { transition: none !important; }

/* --- Loader ------------------------------------------------------------- */

#loader {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: #05030a;
  color: rgba(243, 236, 255, .55);
  letter-spacing: .3em;
  text-transform: uppercase;
  font-size: 13px;
  transition: opacity 500ms ease;
  z-index: 10;
}

#loader.is-done { opacity: 0; pointer-events: none; }

/* Suppress things that actually MOVE. Opacity is deliberately left alone: a
   crossfade is the recommended substitute for motion, not a casualty of it.
   The old rule here clobbered `transition` wholesale, which silently retimed
   the dissolve and killed the sprite slide. */
/* --- Document pages (About, Help) --------------------------------------- */
/* Deliberately not visual-novel styled: these are things to read, not scenes.
   Same palette so it still feels like the same site. */

.doc-page body {
  background:
    radial-gradient(120% 80% at 50% 0%, #150a26 0%, #05030a 62%) no-repeat fixed;
}

.doc-bar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 1.2em;
  padding: .9em clamp(1em, 5vw, 3em);
  background: rgba(5, 3, 10, .88);
  border-bottom: 1px solid rgba(217, 165, 255, .16);
  backdrop-filter: blur(8px);
}

.doc-bar .doc-back { margin-right: auto; }

.doc-bar a {
  font-size: clamp(12px, 1.05vw, 17px);
  color: var(--ink);
  text-decoration: none;
  opacity: .7;
  padding: .35em .2em;
  transition: opacity 140ms ease, color 140ms ease;
}

.doc-bar a:hover { opacity: 1; color: var(--accent); }
.doc-bar a[aria-current="page"] { opacity: 1; color: var(--accent); }
.doc-links { display: flex; gap: 1.1em; }

/* The ☰ menu is shared with the story pages, but here the button hangs off the
   sticky bar rather than the stage. The menu itself is the overlay screen; on a
   doc page there is no positioned stage to fill, so it is pinned to the viewport
   (fixed) rather than the document, which would scroll away under it. */
.doc-page #nav-toggle { position: static; }
.doc-page #overlay { position: fixed; }

.doc {
  max-width: 46em;
  margin: 0 auto;
  padding: clamp(2em, 6vw, 4.5em) clamp(1.2em, 5vw, 3em) 6em;
  line-height: 1.7;
  font-size: clamp(15px, 1.15vw, 18px);
  color: rgba(243, 236, 255, .86);
}

.doc h1 {
  font-size: clamp(28px, 3.4vw, 46px);
  font-weight: 400;
  letter-spacing: .01em;
  margin-bottom: .5em;
}

.doc h2 {
  font-size: clamp(19px, 1.7vw, 26px);
  font-weight: 400;
  color: var(--accent);
  margin: 2.4em 0 .7em;
  padding-bottom: .35em;
  border-bottom: 1px solid rgba(217, 165, 255, .16);
}

.doc h3 {
  font-size: clamp(15px, 1.15vw, 18px);
  font-weight: 700;
  margin: 1.6em 0 .3em;
  color: var(--ink);
}

.doc p { margin-bottom: 1em; }
.doc address { font-style: normal; margin-bottom: 1em; }

.doc-lede {
  font-size: clamp(17px, 1.4vw, 22px);
  color: var(--ink);
  margin-bottom: 2em;
}

.doc dl { margin: 0 0 1em; }

.doc dt {
  font-weight: 700;
  color: var(--accent);
  margin-top: 1.4em;
}

.doc dd { margin: .25em 0 0; }

.doc a { color: var(--accent); }

.doc-note {
  font-size: .9em;
  opacity: .62;
  border-left: 2px solid rgba(217, 165, 255, .3);
  padding-left: 1em;
}

.doc-foot {
  margin-top: 3.5em;
  padding-top: 1.5em;
  border-top: 1px solid rgba(217, 165, 255, .16);
}

.doc-foot a { text-decoration: none; }
.doc-foot a:hover { text-decoration: underline; }

/* --- The 18+ gate, on a plain page -------------------------------------- */

/* downloads.html asks the same question the story does, but there is no stage
   under it and no overlay over it — it IS the page until it is answered. So it
   borrows the panel chrome and centres itself in the column rather than in a
   backdrop. Same two answers, same weight, same refusal to pick one for the
   reader; the "no" is an <a> because leaving is a link, not a verb. */
.gate-page {
  max-width: 30em;
  margin: 2em auto 0;
  text-align: center;
  background: rgba(10, 4, 22, .8);
  border: 1px solid rgba(217, 165, 255, .5);
  border-radius: 5px;
  padding: 1.8em 2em;
}

.gate-page h2 {
  margin: 0 0 .8em;
  font-size: clamp(17px, 1.7vw, 26px);
  color: var(--accent);
  letter-spacing: .04em;
}

.gate-page .gate-body {
  margin: 0 0 1.6em;
  font-size: clamp(13px, 1.05vw, 16px);
  line-height: 1.6;
  opacity: .8;
}

.gate-page .gate-actions { display: flex; flex-direction: column; gap: .7em; }

.gate-page .choice {
  min-width: 0;
  width: 100%;
  padding: .8em 1.2em;
  font-size: clamp(14px, 1.15vw, 17px);
  text-align: center;
  text-decoration: none;
  display: block;
  /* The "no" is an <a> and the "yes" is a <button>, because leaving is a link
     and confirming is an act. That must not show: `.doc a` would paint the no
     in accent and leave the yes in ink, which is the gate quietly recommending
     one of its two answers. Same colour, same weight, same size. */
  color: var(--ink);
}

.gate-page .panel-note { margin-top: 1.4em; cursor: default; opacity: .45; }

.downloads-soon { font-size: 1.05em; line-height: 1.6; }
.downloads-soon b { color: var(--accent); font-weight: 600; }

/* --- The gallery -------------------------------------------------------- */

/* Wider than a reading column: this one is looking at pictures, and 46em of
   text measure would put two thumbnails a screen apart. */
.doc-wide { max-width: 74em; }

.shelf { margin: 0 0 3.5em; }

.shelf-note {
  max-width: 42em;
  margin: -.4em 0 1.6em;
  font-size: .92em;
  opacity: .62;
}

.shelf-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 19em), 1fr));
  gap: clamp(.9em, 2vw, 1.6em);
}

.shot {
  margin: 0;
  border: 1px solid rgba(217, 165, 255, .18);
  border-radius: 4px;
  background: rgba(10, 4, 22, .6);
  overflow: hidden;
}

.shot-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  line-height: 0;
}

.shot-open img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  transition: transform 320ms ease, filter 320ms ease;
}

.shot-open:hover img,
.shot-open:focus-visible img { transform: scale(1.03); filter: brightness(1.08); }

/* A locked card is a real card that states what it is not. Half the point of a
   gallery is seeing the size of what you have not found — and there is no <img>
   inside it at all, so a render nobody has earned is never fetched. */
.shot.is-locked {
  border-style: dashed;
  border-color: rgba(217, 165, 255, .16);
}

.shot-seal {
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 9;
  font-size: 2.4em;
  color: var(--accent);
  opacity: .18;
  background:
    repeating-linear-gradient(-45deg,
      rgba(217, 165, 255, .05) 0 10px, transparent 10px 20px);
}

.shot-cap {
  padding: .7em .9em .8em;
  font-size: .92em;
  line-height: 1.45;
}

.shot-cap b { color: var(--ink); font-weight: 600; }

.shot-cap em {
  display: block;
  margin-top: .2em;
  font-style: normal;
  font-size: .88em;
  opacity: .55;
}

.shot.is-locked .shot-cap b { opacity: .45; font-weight: 400; }

.shelf-shut {
  padding: 1.4em 1.6em;
  border: 1px dashed rgba(217, 165, 255, .3);
  border-radius: 4px;
  background: rgba(10, 4, 22, .5);
}

/* --- The full-size viewer ----------------------------------------------- */

/* Fixed, not absolute: this page scrolls, and a viewer that scrolls with it
   would leave the render half off the top by the time you reached the shelf. */
#viewer {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(.8em, 3vw, 2.4em);
  background: rgba(3, 1, 8, .93);
  backdrop-filter: blur(4px);
  cursor: zoom-out;
}

#viewer[hidden] { display: none; }

html.is-viewing, html.is-viewing body { overflow: hidden; }

.viewer-frame {
  display: flex;
  flex-direction: column;
  gap: .7em;
  max-width: 100%;
  max-height: 100%;
  cursor: default;
}

.viewer-frame img {
  max-width: 100%;
  min-height: 0;
  object-fit: contain;
  border-radius: 3px;
  box-shadow: 0 16px 60px rgba(0, 0, 0, .8);
}

.viewer-bar {
  display: flex;
  align-items: center;
  gap: 1em;
  font-size: clamp(12px, 1vw, 15px);
}

.viewer-title { color: var(--ink); opacity: .8; }

.viewer-dl {
  margin-left: auto;
  padding: .5em 1.1em;
  color: var(--accent);
  text-decoration: none;
  border: 1px solid rgba(217, 165, 255, .42);
  border-radius: 3px;
  transition: background 150ms ease, color 150ms ease;
}

.viewer-dl:hover { background: rgba(62, 23, 100, .9); color: #fff; }

.viewer-close {
  font-size: 1.6em;
  line-height: 1;
  color: var(--ink);
  background: none;
  border: 0;
  padding: 0 .2em;
  cursor: pointer;
  opacity: .55;
  transition: opacity 140ms ease, color 140ms ease;
}

.viewer-close:hover { opacity: 1; color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .layer.is-drifting, #advance, .caret {
    animation: none !important;
  }

  /* A thumbnail growing under the cursor is movement, and the gallery is a
     wall of them. The brightness lift stays: it says the same thing without
     anything travelling. */
  .shot-open:hover img,
  .shot-open:focus-visible img { transform: none; }

  /* Myra jumps to her mark instead of sliding — a large sprite travelling
     across the frame is exactly the movement this setting asks us to drop.
     Note this drops `transform` from the list but keeps opacity untouched. */
  .layer {
    transition-property: opacity, filter;
    transition-duration: var(--dissolve), var(--glow);
    transition-timing-function: ease, ease;
  }
}
