:root {
  /* Measured off the book's own blank sheet (page 4), not chosen. The paper is
     cool, not cream: mean RGB (250, 249, 252). The previous warm #fdfbf6 differed
     by about 4 levels of blue, which is precisely enough to see a step where the
     plate ends -- the thing that made it read as a photograph of a page. */
  --paper: #faf9fc;
  --paper-deep: #efedf3;
  /* The surface the book rests on. Deliberately NOT the paper colour: when the
     surround matched the page, the letterbox read as the image failing to fill
     rather than as a book sitting on something. */
  --surface: #d9d5d0;
  --surface-deep: #bdb8b2;
  --ink: #231c20;
  --edge: #e2dbce;
}

* { box-sizing: border-box; }

/* The `hidden` attribute gets its display:none from the UA stylesheet, and ANY
   author rule beats a UA rule regardless of specificity. `#landing` sets
   display:grid, so `landing.hidden = true` did nothing at all: the landing page
   stayed fully laid out behind the reader for the whole session. On a desktop the
   fixed reader covers it and nobody sees. On an iPhone, where Safari keeps a
   toolbar strip the reader cannot paint over, the landing page's credit line
   showed underneath the book. Reported from a real phone. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--paper);
  color: var(--ink);
  font-family: "Frank Ruhl Libre", "Times New Roman", serif;
  -webkit-text-size-adjust: 100%;
}

body.reading {
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
}

/* ------------------------------------------------------- standalone pages */

/* Three documents that are not the story: the back matter (pages 30-32), the
   dedication (page 1) and the credits (page 2). None of them has an illustration
   to be registered against, so unlike the story they are set as ordinary prose
   and reflow to the reader's screen instead of being shown as fixed spreads. */

.doc {
  max-width: 38rem;
  margin-inline: auto;
  padding: max(3rem, env(safe-area-inset-top)) clamp(1.25rem, 6vw, 3rem)
           max(4rem, env(safe-area-inset-bottom));
  font-size: 1.02rem;
  line-height: 1.85;
}

.doc h1 { font-size: clamp(1.6rem, 5vw, 2.2rem); margin: .2rem 0 .4rem; font-weight: 600; }
.doc h2 {
  font-size: 1.22rem;
  font-weight: 600;
  margin: 2.6rem 0 .9rem;
  color: #6d3348;
}
.doc section:first-of-type h2 { margin-top: 2rem; }
.doc p { margin: 0 0 1.15rem; }
.doc ul { margin: 0 0 1.15rem; padding-inline-start: 1.2rem; }
.doc li { margin: 0 0 1.15rem; }

/* The run-in term that opens most paragraphs is bold in the book. It is not
   marked up as such: the extraction knows which spans were bold, but a run-in
   heading is a heading, and a <strong> here would be styling standing in for
   structure. Left as prose until someone decides it needs to be navigable. */

.doc-head { margin-bottom: 2.5rem; }

/* The author's line breaks are the whole of the dedication's typography, so it
   keeps them instead of reflowing. */
.doc .dedication { line-height: 2.1; font-size: 1.15rem; }

/* Credits are one-line entries. At the body's leading they read as if there
   were a blank line between each. */
.doc .credits p { margin: 0 0 .5rem; line-height: 1.55; }
.doc .credits { margin-bottom: 1.6rem; }

.doc-toc {
  margin-top: 1.8rem;
  padding: 1.1rem 1.4rem;
  border: 1px solid rgba(60, 48, 34, .15);
  border-radius: 6px;
  background: rgba(255, 255, 255, .45);
  font-size: .96rem;
}
/* Scoped under .doc to outrank the body list spacing, which is set on
   `.doc li` and would otherwise open these out to paragraph gaps. */
.doc .doc-toc ol { margin: 0; padding-inline-start: 1.4rem; }
.doc .doc-toc li { margin: 0 0 .45rem; line-height: 1.5; }
.doc .doc-toc li:last-child { margin-bottom: 0; }

/* Without this an anchor jump puts the heading flush against the top edge and
   it reads as though the section has no beginning. */
.doc h2 { scroll-margin-top: 1.5rem; }
.doc-sub { color: #6b6053; }
.doc-back, .doc-foot { font-size: .95rem; }
.doc-foot {
  margin-top: 3.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid rgba(60, 48, 34, .15);
}
/* Attached to the viewport, not the document: sized to the page, the gradient
   ran out partway down and left a visible band where it met the body colour. */
body.doc-page { background: linear-gradient(#fffdf8, #f6f1e6) fixed; }

.doc a { color: #8c3f5c; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.doc a:hover { color: #6d3348; }

/* An arrow written into the text is a neutral character, and bidi resolves it to
   the far end of the line -- the wrong end for "back". As a generated inline
   block it is an object with a position in the logical order instead, so it sits
   at the start of the link where a back arrow belongs. */
.doc-back a::before {
  content: "\2192";
  display: inline-block;
  margin-inline-end: .4em;
}

/* ------------------------------------------------------------------ landing */

#landing {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: max(3rem, env(safe-area-inset-top)) clamp(1.5rem, 6vw, 4rem)
           max(3rem, env(safe-area-inset-bottom));
  background:
    radial-gradient(120% 90% at 50% 0%, #fffdf8 0%, #f6f1e6 60%, #efe8da 100%);
}

.landing-inner {
  display: grid;
  grid-template-columns: minmax(240px, 380px) minmax(280px, 460px);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
  max-width: 900px;
}

/* Portrait only. A landscape phone is narrow in the other direction and stacking
   there would guarantee a scroll -- see the short-landscape block below. */
@media (max-width: 720px) and (orientation: portrait) {
  .landing-inner { grid-template-columns: 1fr; text-align: center; }
}


.cover {
  /* The cover is square, like every page of this book. */
  margin: 0;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 4px;
  box-shadow: 0 18px 44px -18px rgba(60, 48, 34, .45),
              0 2px 6px rgba(60, 48, 34, .12);
}
.cover picture { display: contents; }
.cover img { width: 100%; height: 100%; display: block; }

h1 {
  font-size: clamp(1.7rem, 4.2vw, 2.6rem);
  line-height: 1.25;
  margin: 0 0 .4rem;
  font-weight: 600;
}

.byline { margin: 0 0 1.1rem; color: #6b6053; font-size: 1.02rem; }
.byline strong { font-weight: 600; color: var(--ink); }
/* Sits directly under the credit for the drawings, because it belongs to that
   credit and not to the blurb below it. Quieter than the names: the illustrations
   are the illustrator's, and only the motion is generated. */
.byline-ai { margin-top: -.8rem; font-size: .92rem; color: #857a6c; }

.blurb { margin: 0 0 1.9rem; line-height: 1.85; font-size: 1.06rem; color: #3f382f; }

.launch {
  font: inherit;
  font-size: 1.12rem;
  font-weight: 600;
  color: #fff;
  background: #b8607a;
  border: 0;
  border-radius: 999px;
  padding: .85rem 2.4rem;
  cursor: pointer;
  box-shadow: 0 8px 20px -8px rgba(184, 96, 122, .8);
  transition: transform .12s ease, box-shadow .12s ease;
}
.launch:hover { transform: translateY(-1px); box-shadow: 0 12px 24px -8px rgba(184, 96, 122, .85); }
.launch:active { transform: translateY(0); }
.launch:focus-visible { outline: 3px solid #7fa8c9; outline-offset: 3px; }

.hint { margin: .8rem 0 1.4rem; font-size: .88rem; color: #8a7f70; }

#landing a { color: #8c3f5c; text-decoration-thickness: 1px; text-underline-offset: 2px; }
#landing a:hover { color: #6d3348; }

/* The three standalone documents. Links rather than an expanding panel, so
   nothing below them moves when one is chosen. */
.extras { margin: 0 0 1.6rem; font-size: .95rem; }
.extras ul { list-style: none; margin: 0; padding: 0; }
.extras li { margin: 0 0 .45rem; }
.extras li:last-child { margin-bottom: 0; }

.credit { margin: 0; font-size: .82rem; color: #8a7f70; line-height: 1.6; }
.credit a { color: #8a7f70; }


/* ------------------------------------------------------------------- reader */

#reader {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: var(--paper);
  cursor: grab;
}

/* There is no portrait experience. When the viewport is portrait the whole reader is
   turned 90 deg so the book is still read in landscape, and the reader turns the
   phone. screen.orientation.lock() would be the tidy way, but Safari does not support
   it on any platform, so this is what actually guarantees the outcome.

   The frame is laid out at the SWAPPED dimensions, which is the point: everything
   inside genuinely is in a landscape box, so fitting, the bleed maths and
   ResizeObserver need no changes at all. Only pointer coordinates arrive in screen
   space -- see Reader.localPoint(). */
.reader-frame {
  position: absolute;
  inset: 0;
  transform-origin: top left;
}

#reader.rotate .reader-frame {
  width: 100dvh;
  height: 100dvw;
  /* The document is dir="rtl". With inset:0 inherited from .reader-frame, giving an
     explicit width over-constrains left/right, and in RTL the RIGHT edge wins -- so
     the frame anchored to the right and started at (viewportWidth - frameWidth),
     dragging every rotated layout off the left of the screen by exactly that much.
     Anchor it explicitly instead. */
  right: auto;
  bottom: auto;
  left: 0;
  top: 0;
  /* Reading right-to-left in the rotated frame, the content's top edge lands on the
     screen's right, so the phone is turned anticlockwise. Flip to
     rotate(-90deg) translateX(-100%) to send it the other way. */
  transform: rotate(90deg) translateY(-100%);
}
#reader:active { cursor: grabbing; }

.reader-loading {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(circle at 50% 44%, rgba(255, 255, 255, .82), transparent 32%),
    var(--paper) url('assets/paper.webp') repeat;
  background-size: auto, min(100vmin, 900px);
  color: #6b6053;
  font-size: 1.1rem;
}
.reader-loading::before {
  content: '';
  width: 2.2rem;
  height: 2.2rem;
  margin-inline-end: .8rem;
  display: inline-block;
  vertical-align: middle;
  border: 3px solid rgba(184, 96, 122, .2);
  border-top-color: #b8607a;
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }

/* Stage 3: whatever the bleed does not reach. Not a flat colour -- the book's own
   paper, tiled across the whole viewport at the same grain scale as the page, so
   the surround has the same texture as the sheet and the eye cannot find the
   boundary. Flat colour is what makes a page look pasted onto a background. */
.backdrop {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Anchored to the BOOK, not the viewport: the halo is centred on the sheet and
     falls off over a distance proportional to it, then flattens to the base surface.
     So the transition beside the book looks the same on every screen, and only the
     amount of flat surface beyond it changes. */
  background:
    radial-gradient(ellipse calc(var(--sheet-w, 100vw) * 0.85)
                            calc(var(--sheet-h, 100vh) * 1.35)
                    at var(--sheet-cx, 50%) var(--sheet-cy, 50%),
      var(--surface) 0%, var(--surface) 55%, var(--surface-deep) 100%);
  background-color: var(--surface-deep);
}

.backdrop-paper {
  position: absolute;
  inset: 0;
  background-image: url('assets/paper.webp');
  background-repeat: repeat;
  /* --grain is set per render to the on-screen size of one page, so the texture
     runs at exactly the scale it does inside the plate. */
  /* Set per render to one page's on-screen size, so the surround's grain runs at
     the same scale as the grain inside the page. */
  background-size: var(--page, 900px) var(--page, 900px);
  opacity: .28;
}

.backdrop-grain {
  position: absolute;
  inset: 0;
  background: var(--paper);
  mix-blend-mode: multiply;
  opacity: .35;
}

.backdrop-vignette {
  position: absolute;
  inset: 0;
  /* Also book-anchored, so the corners darken relative to the sheet rather than
     relative to whatever window it happens to be in. */
  background: radial-gradient(ellipse calc(var(--sheet-w, 100vw) * 1.05)
                                      calc(var(--sheet-h, 100vh) * 1.7)
                              at var(--sheet-cx, 50%) var(--sheet-cy, 50%),
    rgba(0, 0, 0, 0) 55%, rgba(60, 52, 44, .22) 100%);
}

.stage {
  position: absolute;
  inset: 0;
  transition: transform .26s cubic-bezier(.22, .61, .36, 1);
}
/* No transition while the finger is down, or the page lags behind the pointer. */
.stage.dragging { transition: none; }

/* The shadow the departing sheet casts on the one it is uncovering. It rides on
   the moving layer, and it is placed from the sheet's own published geometry
   (--sheet-*, set by Reader.#publishSheetMetrics) so it hugs the paper rather
   than the viewport. Width is a fraction of --page, like every other measurement
   around the book, so it looks the same at any size. */
.stage.is-leaving { transition: none; }
.stage.is-leaving::before {
  content: '';
  position: absolute;
  top: calc(var(--sheet-cy) - var(--sheet-h) / 2);
  height: var(--sheet-h);
  width: calc(var(--page) * .11);
  pointer-events: none;
}
.stage.is-leaving[data-turn='next']::before {
  left: calc(var(--sheet-cx) - var(--sheet-w) / 2 - var(--page) * .11);
  background: linear-gradient(to left, rgba(60, 48, 34, .28), rgba(60, 48, 34, 0));
}
.stage.is-leaving[data-turn='prev']::before {
  left: calc(var(--sheet-cx) + var(--sheet-w) / 2);
  background: linear-gradient(to right, rgba(60, 48, 34, .28), rgba(60, 48, 34, 0));
}

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

#reader img { -webkit-user-drag: none; user-select: none; }

/* Keep the trim rectangle exact. A former prototype rendered a second enlarged
   copy as fake bleed and feathered the source page; on illustrated edges that
   looked like duplicated characters and changed the source composition. */
/* Three layers over the sheet's rect, and the order is load-bearing:
   the sheet (art), then the motion clip, then the verse. The words are the one
   thing nothing may paint over -- see .page-motion and .verse-layer. */
.page {
  position: absolute;
  overflow: hidden;
  z-index: 1;
  background: var(--paper);
}

/* One shadow under the whole sheet, cast on the surface it rests on, plus a hairline
   so the sheet has an edge. Feathering the page was tried and made the boundary mushy
   without hiding it; against a distinct surface a clean edge reads as a sheet. */
/* Every dimension here is a fraction of --page, one page of the printed sheet, so
   the framing is identical at any size. Nothing is tuned to a particular screen. */
.stage {
  --sheet-shadow:
    0 calc(var(--page, 600px) * 0.024) calc(var(--page, 600px) * 0.064)
      calc(var(--page, 600px) * -0.024) rgba(60, 50, 40, .55),
    0 calc(var(--page, 600px) * 0.004) calc(var(--page, 600px) * 0.013)
      rgba(60, 50, 40, .14);
}
/* Keyed on which half the page is, not on :first-of-type / :last-of-type. The
   verse now rides on its own layer beside the sheets rather than inside them, and
   a div sibling is the same "type" -- the last-of-type rule silently stopped
   matching any page at all the moment those layers appeared. */
.page { box-shadow: var(--sheet-shadow); }
.stage[data-spread="1"] .page[data-half="left"] {
  /* on a spread the shadow belongs to the pair, so clip it off the inner edge */
  clip-path: inset(calc(var(--page, 600px) * -0.09) 0
                   calc(var(--page, 600px) * -0.09) calc(var(--page, 600px) * -0.09));
}
.stage[data-spread="1"] .page[data-half="right"] {
  clip-path: inset(calc(var(--page, 600px) * -0.09) calc(var(--page, 600px) * -0.09)
                   calc(var(--page, 600px) * -0.09) 0);
}

.page-art {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
  user-select: none;
}

/* The verse layer: over the sheet AND over any motion clip laid on it.
   It is a sibling of .page, not a child, because .page is a stacking context --
   a verse inside it could never rise above a clip that sits beside it, and the
   clip would paint across the words. The reader gives it the sheet's exact rect.

   No `inset` here. The shorthand writes all four sides, so a later left/top pair
   left right/bottom at 0; the box was then over-constrained, and in this RTL
   document that drops `left`, not `right`. Sides are set explicitly instead. */
.verse-layer { position: absolute; z-index: 3; pointer-events: none; }
.page-verse { position: absolute; pointer-events: none; }
.page-verse .verse-svg { width: 100%; height: 100%; display: block; }


/* --------------------------------------------------------------------- hud */

/* Two chips in the top corners, and nothing over the book.
   
   This was a control pill across the bottom -- close, prev, next and a thirteen
   leaf scrubber. On spreads whose verse runs low it sat on top of the words, and
   chrome over the text is worse than no chrome. Turning pages already works by
   tap, swipe, arrow keys and trackpad, so the arrows were duplicating gestures
   that exist, and a scrubber is for finding your way around something longer than
   thirteen spreads.

   The way out stays. Escape is not a thing on a phone. */

.hud {
  position: absolute;
  inset-block-start: max(.5rem, env(safe-area-inset-top));
  inset-inline-end: max(.5rem, env(safe-area-inset-left));
  z-index: 4;
}

.hud-btn {
  font-family: system-ui, sans-serif;
  font-size: 1.1rem;
  line-height: 1;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  padding: 0;
  color: #6b6053;
  background: rgba(255, 253, 248, .72);
  backdrop-filter: blur(6px);
  box-shadow: 0 2px 10px -5px rgba(60, 48, 34, .4);
  cursor: pointer;
  transition: color .15s ease, background .15s ease;
}
.hud-btn:hover { color: var(--ink); background: rgba(255, 253, 248, .95); }
.hud-btn:focus-visible { outline: 2px solid #7fa8c9; outline-offset: 2px; }

/* Prototype instrumentation — shows the live layout decision. Remove for release. */
.hud-mode {
  position: absolute;
  inset-block-start: 2.5rem;
  inset-inline-end: 0;
  white-space: nowrap;
  font-size: .72rem;
  font-family: ui-monospace, monospace;
  color: #a2988a;
}

/* Where you are. Its own chip in the top corner rather than a slot in the control
   pill: it is the one thing you want to glance at without the controls being in
   the way, and inset-inline puts it at the reading edge in either direction.
   The spread you are on carries the weight; the total is only context for it. */
.page-count {
  position: absolute;
  inset-block-start: max(.6rem, env(safe-area-inset-top));
  inset-inline-start: max(.7rem, env(safe-area-inset-right));
  padding: .2rem .6rem;
  border-radius: 999px;
  background: rgba(255, 253, 248, .72);
  backdrop-filter: blur(6px);
  box-shadow: 0 2px 10px -5px rgba(60, 48, 34, .4);
  font-size: .78rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  color: #6b6053;
  white-space: nowrap;
  user-select: none;
  pointer-events: none;
  z-index: 4;
}
.page-count b { font-size: .95rem; font-weight: 600; color: var(--ink); }
.page-count .count-all { color: #a2988a; }



@media (prefers-reduced-motion: reduce) {
  .stage, .launch { transition: none; }
  .reader-loading::before { animation: none; }
}

/* Narrow AND tall: a phone held upright. Was width-only, which meant a phone held
   sideways matched it too -- 667 px wide is under 720 -- and it quietly beat the
   short-landscape block below on source order, undoing most of it. */
@media (max-width: 720px) and (orientation: portrait) {
  #landing { padding-inline: 1.5rem; }
  .landing-inner { gap: 1rem; }
  .cover { width: min(54vw, 220px); margin-inline: auto; }
  h1 { font-size: clamp(1.55rem, 7vw, 2rem); }
  .blurb { font-size: .98rem; line-height: 1.6; margin-bottom: 1.1rem; }
}

/* The reader's own chrome, which cares about width whichever way up you are. */
@media (max-width: 720px) {
  .page-count { font-size: .74rem; }
  .hud-btn { width: 1.8rem; height: 1.8rem; font-size: 1rem; }
}

/* Short and wide: a phone held sideways. Everything here fits the landing page
   into one screen with nothing to scroll, which is the whole point of it -- it is
   a door, and a door you have to scroll is a bad door.
   
   The saving is not in the type, which stays close to full size, but in the
   chrome: the padding, the gaps between blocks, and the three document links,
   which become one row instead of three. The cover is capped against the
   viewport height as well as its width so it can never be what forces the
   overflow. Numbers here were measured, not guessed -- tools/measure_landing.py. */
@media (orientation: landscape) and (max-height: 600px) {
  #landing {
    padding: max(.7rem, env(safe-area-inset-top)) clamp(.9rem, 2.6vw, 2rem)
             max(.7rem, env(safe-area-inset-bottom));
  }
  .landing-inner {
    grid-template-columns: min(380px, 34vw, 88svh) minmax(0, 1fr);
    gap: clamp(.9rem, 2.6vw, 2rem);
    max-width: 1100px;
  }
  h1 { font-size: clamp(1.3rem, 3.4vw, 2.2rem); margin-bottom: .2rem; }
  .byline { margin-bottom: .55rem; font-size: .95rem; }
  .blurb {
    margin-bottom: .85rem;
    line-height: 1.65;
    font-size: clamp(.9rem, 2.1vw, 1.02rem);
  }
  .launch { padding: .6rem 1.9rem; font-size: 1.02rem; }
  .hint { margin: .5rem 0 .55rem; font-size: .82rem; }
  .extras { margin: 0 0 .45rem; font-size: .88rem; }
  .extras ul { display: flex; flex-wrap: wrap; gap: .2rem 1.1rem; }
  .extras li { margin: 0; }
}

/* Failure is visible, never silent. A dead button on a page that looks fine is the
   worst possible outcome, and it is what happens on a file:// origin. */
.error {
  margin: 1rem 0 1.4rem;
  padding: .85rem 1rem;
  border: 1px solid #e0bfa8;
  border-inline-start: 4px solid #c2705a;
  border-radius: 6px;
  background: #fdf3ec;
  color: #6b3a2a;
  font-size: .92rem;
  line-height: 1.7;
  text-align: start;
}
.error strong { display: block; margin-bottom: .2rem; }
.error code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .86em;
  background: rgba(0, 0, 0, .05);
  padding: .1em .4em;
  border-radius: 3px;
  direction: ltr;
  unicode-bidi: embed;
  display: inline-block;
}
.error code.cmd { display: block; margin: .45rem 0; padding: .4rem .6rem; }
.error a { color: #a2503a; }
.error .detail {
  display: block;
  margin-top: .3rem;
  font-family: ui-monospace, monospace;
  font-size: .82em;
  opacity: .8;
  direction: ltr;
  text-align: start;
}
.launch:disabled { opacity: .6; cursor: progress; }

/* The motion clip sits over its own still and fades in only once it is playing, so
   every failure path -- missing file, refused autoplay, decode error -- lands back
   on the still with nothing to notice.

   Between the picture (z 1) and the verse (z 3). It replaces the illustration, never
   the words -- the text must not change, and a clip painted over it would change it. */
.page-motion {
  position: absolute;
  z-index: 2;
  object-fit: cover;
  pointer-events: none;          /* or it swallows the drag-to-turn gesture */
  opacity: 0;
  transition: opacity .6s ease;
  -webkit-user-drag: none;
}
.page-motion.is-playing { opacity: 1; }


/* A view supplied as one re-composed plate rather than two page images. */
.plate-art { width: 100%; height: 100%; object-fit: cover; display: block; }


/* The art is offered as AVIF first with the WebP behind it, which needs a
   <picture> wrapper. `display: contents` keeps it out of the layout entirely, so
   .page-art still sizes against .page exactly as it did before the wrapper. */
.page picture { display: contents; }

/* ------------------------------------------------------------------- nux */

/* Shown once, the first time the book is opened. It exists because an older
   reader tried to swipe, did not travel far enough to cross the threshold, saw
   the page bounce back, and had no way to learn that tapping a side works too.
   Deliberately two shapes and six words: the aim is to be understood at a glance
   and then never seen again. It sits inside the reader's frame, so on a portrait
   phone -- where the whole reader is rotated 90deg -- left and right still mean
   what the reader sees rather than what the device thinks. */
.nux {
  position: absolute;
  inset: 0;
  z-index: 6;                    /* over the verse (3) and the chrome (4) */
  background: rgba(24, 20, 22, .58);
  backdrop-filter: blur(1.5px);
  -webkit-backdrop-filter: blur(1.5px);
  color: #fff;
  cursor: pointer;
  /* No blue flash on touch. A tap used to light the whole overlay up like a
     pressed button, and dragging across it selected the text, which on a phone
     reads as the app having gone wrong rather than as a page turning. */
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
  /* `both` would have been wrong: it keeps the animation's last keyframe applied
     forever, and a running or filled animation outranks a normal declaration -- so
     `.is-going { opacity: 0 }` did nothing and the overlay vanished in one frame
     instead of dissolving. Measured: opacity stayed at 1 the whole way out. */
  animation: nux-in .35s ease;
}
@keyframes nux-in { from { opacity: 0; } to { opacity: 1; } }
.nux.is-going { opacity: 0; transition: opacity .35s ease; }

/* Placed by PHYSICAL side, not by grid order. This is an RTL document, so grid's
   first column is the RIGHT one -- the first child landed on the right and the
   overlay confidently told readers that tapping right goes forward, which is the
   opposite of what tapping right does. Left and right here are the literal sides
   of the screen, because that is what the tap handler compares x against, so they
   are written as `left` and `right` and cannot be flipped by direction again. */
.nux-half {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  font-size: clamp(.95rem, 2.4vh, 1.25rem);
  font-weight: 600;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .5);
}
/* RTL: forward is leftward, so the left half is "next". Matches #bind's
   `x < clientWidth / 2 -> go(+1)` exactly. */
.nux-next { left: 0; }
/* A hairline between the two halves, so they read as two places rather than one
   dark sheet with words on it. */
.nux-prev { right: 0; box-shadow: inset 1px 0 0 rgba(255, 255, 255, .22); }

.nux-half svg {
  width: clamp(2.4rem, 7vh, 4rem);
  height: clamp(2.4rem, 7vh, 4rem);
  opacity: .95;
}

.nux-keys {
  position: absolute;
  left: 0;
  right: 0;
  bottom: clamp(1rem, 4vh, 2.5rem);
  margin: 0;
  text-align: center;
  font-size: clamp(.8rem, 1.9vh, .95rem);
  opacity: .8;
}
/* Only worth saying where there is a keyboard. */
@media (hover: none) and (pointer: coarse) { .nux-keys { display: none; } }

@media (prefers-reduced-motion: reduce) {
  .nux { animation: none; }
  .nux.is-going { transition: none; }
}
