.stf__parent {
  position: relative;
  display: block;
  box-sizing: border-box;
  transform: translateZ(0);
  -ms-touch-action: pan-y;
  touch-action: pan-y;
}
.sft__wrapper {
  position: relative;
  width: 100%;
  box-sizing: border-box;
}
.stf__parent canvas {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
}
.stf__block {
  position: absolute;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  perspective: 2000px;
}
.stf__item {
  display: none;
  position: absolute;
  transform-style: preserve-3d;
}
.stf__outerShadow { position: absolute; left: 0; top: 0; }
.stf__innerShadow { position: absolute; left: 0; top: 0; }
.stf__hardShadow { position: absolute; left: 0; top: 0; }
.stf__hardInnerShadow { position: absolute; left: 0; top: 0; }

/*
  Deliberately single-theme: a screening-room reader. The dark stage stays
  constant regardless of OS theme, the way a photo lightbox or PDF
  full-screen mode does, so the page images (not the chrome) carry the light.
*/
:root {
  --ground: #14151a;
  --panel: #1b1d24;
  --panel-line: #2a2d36;
  --ink: #efe9de;
  --ink-dim: #9a9488;
  --accent: #e2a33e;
  --accent-ink: #1a1206;
  --stage-vignette: radial-gradient(ellipse at center, rgba(255,255,255,0.04) 0%, rgba(0,0,0,0) 60%);
}
* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  height: 100vh;
  height: 100dvh;
  background: var(--ground);
  color: var(--ink);
  font-family: 'Manrope', -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  overflow: hidden;
  overscroll-behavior: none;
}
#app { display: flex; flex-direction: column; height: 100%; }
header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding: 10px max(16px, env(safe-area-inset-right)) 10px max(16px, env(safe-area-inset-left));
  padding-top: max(10px, env(safe-area-inset-top));
  background: var(--panel);
  border-bottom: 1px solid var(--panel-line);
  z-index: 10;
}
header h1 {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 600;
  font-size: clamp(13px, 3.6vw, 16px);
  letter-spacing: 0.01em;
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
#stage {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: clamp(8px, 3vw, 28px);
  position: relative;
  background: var(--stage-vignette);
}
#bookViewport {
  position: relative;
  overflow: hidden;
  flex: 0 0 auto;
  filter: drop-shadow(0 18px 40px rgba(0,0,0,0.55));
  /* filter is included here so the .flip-in-progress toggle below fades
     in/out instead of snapping — an instant on/off read as a flicker on
     every page turn. */
  transition: width .7s ease, transform .25s ease, filter .15s ease;
  transform-origin: center center;
}
/*
  Clickable regions for links extracted from the source PDF (a Spotify
  playlist icon, an advertiser's URL, etc.) — positioned in plain, unscaled
  CSS pixels matching currentLayout's own page dimensions, as a sibling of
  #bookShift rather than a child of it, specifically so it's unaffected by
  #bookShift's dpr-supersampling transform and only inherits #bookViewport's
  own zoom/pan transform, the same way the visible page image does.
*/
#linkOverlay {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  pointer-events: none;
}
.page-link {
  position: absolute;
  pointer-events: auto;
  cursor: pointer;
  border-radius: 3px;
  -webkit-tap-highlight-color: rgba(255,255,255,0.18);
}
.page-link:hover, .page-link:focus-visible {
  background: rgba(255,255,255,0.10);
  outline: 1px solid rgba(255,255,255,0.3);
  outline-offset: -1px;
}
/* See reader.js's 'changeState' handler: the drop-shadow filter above is
   expensive to keep live while StPageFlip's canvas is repainting every
   animation frame (a filter re-blurs its whole box on every repaint,
   unlike a plain box-shadow), so it's dropped for the ~700ms a committed
   flip is actually in motion, fading out/in via the transition above
   rather than snapping, and restored once it settles. */
#bookViewport.flip-in-progress { filter: none; }
#bookViewport.no-anim { transition: none; }
#bookViewport.zoomed { cursor: grab; }
#bookViewport.panning { cursor: grabbing; transition: none !important; }
/*
  #book is always constructed at the full two-page spread size (so
  StPageFlip's own landscape detection and animations work normally even
  on the lone cover page) — #bookShift never changes size, it only slides
  sideways so the narrower #bookViewport crop window reveals just the
  cover's half. The width/margin transitions run together so the crop
  widens in step with the page-curl animation instead of snapping.
*/
#bookShift {
  height: 100%;
  transition: margin-left .7s ease;
}
#bookViewport.no-anim #bookShift { transition: none; }
#book img { width: 100%; height: 100%; display: block; user-select: none; -webkit-user-drag: none; }

/*
  The phone reading engine — plain <img> elements stacked on top of each
  other, turned with a CSS 3D rotateY() (perspective + backface-visibility),
  used instead of #bookShift/#book (StPageFlip's canvas) below
  MOBILE_ARROW_HIDE_WIDTH. No canvas, no devicePixelRatio-supersampling
  trick, nothing for a device's GPU/compositor to render incorrectly: the
  browser's own native <img> decoding is what actually fixed the crop bug
  that motivated this, since it already handles every pixel density
  correctly without a custom hack — this is still true here, since rotateY
  is a standard, heavily-used compositor transform, not a custom pixel
  buffer. See reader.js's initPhoneEngine().
*/
#pagesTrack {
  position: relative;
  height: 100%;
  width: 100%;
  perspective: 1800px;
}
#pagesTrack[hidden] { display: none; }
.page-slide {
  position: absolute;
  inset: 0;
  display: none;
  transform-style: preserve-3d;
  backface-visibility: hidden;
  transform: rotateY(0deg);
  box-shadow: 0 0 14px rgba(0,0,0,0.35);
  /* Hints the browser to promote this to its own GPU layer as soon as it's
     shown (display:none elements cost nothing here either way), instead of
     waiting to discover it needs one only once the first rotateY frame is
     already underway — that late promotion was a real, if brief, hitch
     right at the start of every drag/flip. */
  will-change: transform;
}
.page-slide.flipping { transition: transform .5s cubic-bezier(.4,0,.2,1); }
.page-slide img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
  backface-visibility: hidden;
}

/*
  Large side-arrow overlays, floating over the stage rather than living in a
  toolbar — the way FlipHTML5's own reader puts navigation directly beside
  the page, not in a separate control bar.
*/
.side-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  background: rgba(27, 29, 36, 0.55);
  backdrop-filter: blur(2px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: var(--ink);
  width: 52px; height: 52px;
  border-radius: 50%;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: transform .15s ease, opacity .15s ease, background-color .15s ease;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.side-nav--prev { left: clamp(6px, 2vw, 24px); }
.side-nav--next { right: clamp(6px, 2vw, 24px); }
.side-nav svg { width: 24px; height: 24px; }
.side-nav:disabled { opacity: 0; pointer-events: none; }
.side-nav:hover:not(:disabled) { background: rgba(255,255,255,0.16); transform: translateY(-50%) scale(1.06); }
.side-nav:active:not(:disabled) { transform: translateY(-50%) scale(0.94); }
.side-nav:focus-visible, #fullscreenBtn:focus-visible {
  outline: 2px solid rgba(255,255,255,0.6);
  outline-offset: 2px;
}
@media (max-width: 640px) {
  /* Real phones flip pages by swipe (StPageFlip's own touch handling) —
     the floating arrow circles are a desktop/mouse affordance, and hiding
     them here is what lets the page run edge-to-edge instead of reserving
     lateral clearance for them (see MOBILE_ARROW_HIDE_WIDTH in reader.js). */
  .side-nav { display: none; }
}

#zoomControls {
  position: absolute;
  right: clamp(6px, 2vw, 24px);
  bottom: clamp(6px, 2vw, 24px);
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.zoom-btn {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(27, 29, 36, 0.55);
  backdrop-filter: blur(2px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color .15s ease, opacity .15s ease;
  -webkit-tap-highlight-color: transparent;
}
.zoom-btn:hover:not(:disabled) { background: rgba(255,255,255,0.16); }
.zoom-btn:disabled { opacity: 0.3; cursor: default; }
.zoom-btn svg { width: 15px; height: 15px; }
@media (max-width: 640px) {
  .zoom-btn { width: 30px; height: 30px; }
  .zoom-btn svg { width: 13px; height: 13px; }
  /* #stage now runs full-bleed behind the floating footer bar (see the
     full-bleed mobile rules below), so the zoom buttons need enough
     bottom clearance to sit above it instead of being covered by it. */
  #zoomControls { bottom: calc(70px + env(safe-area-inset-bottom)); }
}
/*
  Single bottom bar, matching FlipHTML5's own reader bar exactly (measured
  from their live player, on both desktop and mobile): an icon cluster on
  the left (sound, thumbnail grid), a counter + full-width scrub slider in
  the middle, and a single icon on the right (fullscreen) — one row, same
  structure at every width, rather than a split header/footer.
*/
#footerBar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px max(14px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
  background: var(--panel);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
#footerBar[hidden] { display: none; }

#muteBtn, #gridBtn, #fullscreenBtn {
  background: transparent;
  border: 1px solid var(--panel-line);
  color: var(--ink);
  border-radius: 7px;
  width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: border-color .15s ease, color .15s ease;
  flex: 0 0 auto;
  -webkit-tap-highlight-color: transparent;
}
#muteBtn:hover, #gridBtn:hover, #gridBtn.active, #fullscreenBtn:hover { border-color: rgba(255,255,255,0.4); color: var(--ink); }
#muteBtn svg, #gridBtn svg, #fullscreenBtn svg { width: 16px; height: 16px; }
#muteBtn .icon-muted { display: none; }
#muteBtn[data-muted="true"] .icon-on { display: none; }
#muteBtn[data-muted="true"] .icon-muted { display: block; }
#fullscreenBtn .icon-collapse { display: none; }
#fullscreenBtn[data-fullscreen="true"] .icon-expand { display: none; }
#fullscreenBtn[data-fullscreen="true"] .icon-collapse { display: block; }
#fullscreenBtn[hidden] { display: none; }

#footerCenter {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
#counter {
  flex: 0 0 auto;
  font-size: 12.5px;
  color: var(--ink);
  min-width: 40px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
#pageSlider {
  flex: 1 1 auto;
  min-width: 0;
  height: 30px;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
#pageSlider::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: rgba(255,255,255,0.14);
}
#pageSlider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--ink);
  border: none;
  margin-top: -6px;
  box-shadow: 0 2px 6px rgba(0,0,0,0.45);
}
#pageSlider::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: rgba(255,255,255,0.14);
}
#pageSlider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--ink);
  border: none;
  box-shadow: 0 2px 6px rgba(0,0,0,0.45);
}
#pageSlider:active::-webkit-slider-thumb { background: var(--accent); }
#pageSlider:active::-moz-range-thumb { background: var(--accent); }

/*
  Full-bleed mobile reading, matching FlipHTML5's own player: the header
  and footer become floating translucent overlays instead of dedicated
  flex rows, so #stage (and the book inside it) gets the entire viewport
  instead of having its height eaten by two toolbars. A small always-there
  handle (mirroring the drag-handle in Flip's own bottom bar) toggles both
  bars away for distraction-free reading.
*/
#chromeToggle { display: none; }
@media (max-width: 640px) {
  header {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 20;
    background: rgba(20, 21, 26, 0.72);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-bottom: none;
    transition: transform .28s ease, opacity .28s ease;
  }
  #footerBar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 20;
    background: rgba(20, 21, 26, 0.72);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    box-shadow: none;
    transition: transform .28s ease, opacity .28s ease;
  }
  #app.controls-hidden header { transform: translateY(-100%); opacity: 0; pointer-events: none; }
  #app.controls-hidden #footerBar { transform: translateY(100%); opacity: 0; pointer-events: none; }
  #chromeToggle {
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    left: 50%;
    bottom: max(4px, env(safe-area-inset-bottom));
    transform: translateX(-50%);
    z-index: 21;
    width: 56px;
    height: 18px;
    padding: 0;
    border: none;
    background: transparent;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  #chromeToggle span {
    width: 36px;
    height: 4px;
    border-radius: 999px;
    background: rgba(255,255,255,0.35);
    transition: background-color .15s ease;
  }
  #chromeToggle:hover span, #chromeToggle:active span { background: rgba(255,255,255,0.6); }
}
@media (max-width: 640px) {
  #footerBar { padding-top: 8px; padding-bottom: 8px; }
}
@media (max-height: 460px) {
  #footerBar { padding-top: 5px; padding-bottom: 5px; }
}

#debugPanel {
  position: fixed;
  top: 4px;
  left: 4px;
  z-index: 999;
  max-width: calc(100vw - 8px);
  background: rgba(0,0,0,0.85);
  color: #6f6;
  font: 10px/1.4 monospace;
  padding: 6px 8px;
  border-radius: 4px;
  white-space: pre;
  pointer-events: none;
  overflow: auto;
}
#debugPanel[hidden] { display: none; }

/*
  Thumbnail-grid page picker — a bottom sheet, matching FlipHTML5's own
  mobile "Thumbnails" drawer (rounded top corners, drag handle, numbered
  grid) closely, since that's specifically what was asked for. Works
  identically on desktop too, just wider/more columns, rather than being a
  phone-only feature — the "go to page" input at its header doubles as the
  numeric jump that the mobile header doesn't have room for.
*/
#thumbGridBackdrop {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgba(0,0,0,0.55);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}
#thumbGridBackdrop.visible { opacity: 1; pointer-events: auto; }
#thumbGridBackdrop[hidden] { display: block; } /* stays in the DOM for the fade-out transition; see JS */
#thumbGridSheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 41;
  max-height: min(78vh, 720px);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-bottom: none;
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -12px 40px rgba(0,0,0,0.5);
  transform: translateY(100%);
  transition: transform .3s cubic-bezier(.32,.72,0,1);
  padding-bottom: env(safe-area-inset-bottom);
}
#thumbGridSheet.visible { transform: translateY(0); }
#thumbGridSheet[hidden] { display: flex; } /* stays in the DOM for the slide transition; see JS */
#thumbGridHandle {
  width: 36px;
  height: 4px;
  border-radius: 999px;
  background: var(--panel-line);
  margin: 10px auto 4px;
  flex: 0 0 auto;
}
#thumbGridHeader {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 16px 12px;
  border-bottom: 1px solid var(--panel-line);
}
#thumbGridJump { display: flex; align-items: center; gap: 6px; }
#thumbGridJumpInput {
  width: 56px;
  background: var(--ground);
  border: 1px solid var(--panel-line);
  color: var(--ink);
  border-radius: 7px;
  padding: 7px 6px;
  font-size: 13.5px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  appearance: textfield;
  -moz-appearance: textfield;
}
#thumbGridJumpInput::-webkit-outer-spin-button, #thumbGridJumpInput::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#thumbGridJumpInput:focus { outline: none; border-color: var(--ink-dim); }
#thumbGridJumpBtn {
  background: transparent;
  border: 1px solid var(--panel-line);
  color: var(--ink-dim);
  border-radius: 7px;
  padding: 7px 14px;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
}
#thumbGridJumpBtn:hover { border-color: rgba(255,255,255,0.4); color: var(--ink); }
#thumbGridCloseBtn {
  background: transparent;
  border: none;
  color: var(--ink-dim);
  width: 32px; height: 32px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
#thumbGridCloseBtn:hover { background: rgba(255,255,255,0.08); color: var(--ink); }
#thumbGridCloseBtn svg { width: 18px; height: 18px; }
#thumbGridList {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 14px 16px calc(20px + env(safe-area-inset-bottom));
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}
@media (min-width: 640px) {
  #thumbGridList { grid-template-columns: repeat(6, 1fr); }
}
@media (min-width: 980px) {
  #thumbGridList { grid-template-columns: repeat(8, 1fr); }
}
.thumb-grid-item {
  position: relative;
  display: block;
  border: 2px solid transparent;
  border-radius: 6px;
  overflow: hidden;
  background: #fff;
  cursor: pointer;
  padding: 0;
  /* The zero-height + percentage-padding trick, not the `aspect-ratio`
     property — aspect-ratio on a CSS Grid item can leave the grid's own
     row-track sizing not accounting for the ratio-derived height (a real,
     confirmed bug here: rows measured as overlapping by 100+px), since a
     row's height needs to be resolved from the item's height and the
     item's height needs the (already-resolved) column width — a circular
     dependency some engines don't reliably self-correct out of. Percentage
     padding has no such ambiguity: it's always resolved against the
     item's own width alone, which grid always knows by the time row
     heights are computed. */
  height: 0;
  padding-bottom: calc(100% / var(--page-ratio, 0.7));
  -webkit-tap-highlight-color: transparent;
}
.thumb-grid-item img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; object-fit: cover; user-select: none; -webkit-user-drag: none; }
.thumb-grid-item:hover { border-color: rgba(255,255,255,0.25); }
.thumb-grid-item.current { border-color: var(--accent); }
.thumb-grid-item .num {
  position: absolute;
  left: 4px;
  bottom: 4px;
  background: rgba(0,0,0,0.72);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 1;
  padding: 3px 5px;
  border-radius: 4px;
  font-variant-numeric: tabular-nums;
}

#loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-dim);
  font-size: 13px;
}
@media (max-width: 420px) {
  header h1 { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
  .side-nav { transition: none; }
}
