/* ============================================================================
   landing.css — the marketing landing at braided.ink/ ("Crossfade")
   ----------------------------------------------------------------------------
   Design tokens come from the live /static/tokens.css (linked alongside this
   file) — this stylesheet adds ZERO new global tokens. It carries only:
     1. self-hosted @font-face (the SPA loads Google-CDN fonts; the landing
        self-hosts to kill the swap-flash — Preston's #1 "things moved" goal),
     2. metric-matched fallback faces for the two CLS-critical families
        (headline + body), so the swap is near-zero vertical shift,
     3. the landing-specific layout: phone scroll-scrubbed crossfade (default,
        mobile-first) and the desktop/tablet two-up spread (>=1020px).

   Two layouts, not three: phone (full-bleed crossfade) below 1020px, desktop
   two-up above; tablet-portrait falls through to phone. The behavior split
   lives in landing.js; this file is the look.
   ============================================================================ */

/* ── Self-hosted fonts (latin subset, same-origin, ~22KB each) ───────────────
   font-display: swap — the metric fallbacks below make the swap shift-free, so
   we get the real face the moment it loads with no reflow penalty. */
@font-face {
  font-family: "Cormorant Garamond";
  font-style: italic; font-weight: 400; font-display: swap;
  src: url("fonts/cormorant-garamond-italic-400.woff2") format("woff2");
}
@font-face {
  font-family: "Cormorant Garamond";
  font-style: italic; font-weight: 500; font-display: swap;
  src: url("fonts/cormorant-garamond-italic-500.woff2") format("woff2");
}
@font-face {
  font-family: "Cormorant SC";
  font-style: normal; font-weight: 500; font-display: swap;
  src: url("fonts/cormorant-sc-500.woff2") format("woff2");
}
@font-face {
  font-family: "Lora";
  font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/lora-400.woff2") format("woff2");
}
@font-face {
  font-family: "Lora";
  font-style: normal; font-weight: 500; font-display: swap;
  src: url("fonts/lora-500.woff2") format("woff2");
}
@font-face {
  font-family: "Lora";
  font-style: italic; font-weight: 400; font-display: swap;
  src: url("fonts/lora-italic-400.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/jetbrains-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal; font-weight: 500; font-display: swap;
  src: url("fonts/jetbrains-mono-500.woff2") format("woff2");
}

/* ── Metric-matched fallbacks (the no-reflow-on-swap fix) ─────────────────────
   Override values computed from the real font metrics vs. Georgia's average
   advance width (capsize method, 2026-06-05). They make the Georgia fallback
   box identically to the web font, so when the web font swaps in there's no
   layout shift — only the two CLS-critical families (huge italic headline,
   body) earn this; the tiny tracked mono/SC marks don't move enough to matter. */
@font-face {
  font-family: "Cormorant Garamond Fallback";
  src: local("Georgia"), local("Times New Roman"), local("Times");
  font-style: italic;
  size-adjust: 82.96%;
  ascent-override: 111.38%;
  descent-override: 34.60%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Lora Fallback";
  src: local("Georgia"), local("Times New Roman"), local("Times");
  size-adjust: 100.73%;
  ascent-override: 99.87%;
  descent-override: 27.20%;
  line-gap-override: 0%;
}

/* ── Landing-scoped token overrides ──────────────────────────────────────────
   Inject the metric fallbacks into the display/body stacks for the landing
   only (never touches the global tokens or the SPA). One landing-local value:
   the cinematic stage letterbox backing. */
.bi-landing {
  --bi-font-display: "Cormorant Garamond", "Cormorant Garamond Fallback", Georgia, serif;
  --bi-font-body:    "Lora", "Lora Fallback", Georgia, serif;
  --bi-landing-stage: #0f0a05;            /* dark letterbox behind every beat */
  --bi-bar-height: 23px;                  /* phone: minimal identity strip below the safe-area inset (desktop restores 50px) */
  --bi-bottom-bar-height: 104px;          /* phone action bar; the safe-area inset is added on top of this */
}

/* ── Base ────────────────────────────────────────────────────────────────── */
* { box-sizing: border-box; }
html, body.bi-landing {
  margin: 0; padding: 0;
  background: var(--bi-landing-stage);
  overflow-x: hidden;
}
body.bi-landing {
  color: var(--bi-ink);
  font-family: var(--bi-font-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
.bi-landing img { display: block; max-width: 100%; }
/* Anchors are colored by their component class (.bi-btn, .beat__cta-*,
   .cf-paper__link, …). Deliberately NO blanket `color: inherit` here — at
   specificity (0,1,1) it would override every (0,1,0) component rule and wash
   the button/CTA/link text out. */
.bi-landing a { text-decoration: none; }
.bi-landing ::selection { background: var(--bi-accent); color: var(--bi-bg); }
.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;
}

/* ── Shared primitives (the live tokens.css has the variables but not these
   component classes — they live here, referencing the live tokens) ────────── */
.bi-wordmark {
  font-family: var(--bi-font-sc); text-transform: uppercase;
  letter-spacing: 0.32em; font-weight: 500; color: var(--bi-ink);
}
.bi-btn {
  font-family: var(--bi-font-body); font-size: 13px; font-weight: 500;
  color: var(--bi-bg); background: var(--bi-ink);
  border: 1px solid var(--bi-ink); border-radius: var(--bi-radius);
  padding: 9px 14px; min-height: 38px; cursor: pointer; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  white-space: nowrap;
}
.bi-btn--ghost {
  color: var(--bi-ink); background: transparent;
  border: 1px solid var(--bi-ink);
}

/* ── Fixed top bar ───────────────────────────────────────────────────────── */
.cf-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 200;
  background: var(--bi-bg); border-bottom: 1px solid var(--bi-rule);
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 2px 16px 3px;
  padding-top: calc(2px + var(--bi-safe-top));   /* the camera/status bar is the safe-area inset; sit just below it */
}
.cf-bar__wordmark { font-size: 12px; }
.cf-bar__cta { display: flex; align-items: center; gap: 7px; }

/* Phone: the primary "Start reading free" CTA lives in the bottom action bar,
   so the top bar is a thin identity strip — wordmark + a quiet "Sign up or log
   in" text link. The desktop block restores BOTH as boxed buttons up here. */
.cf-bar__start { display: none; }
.cf-bar__login {
  border: 0; background: transparent; min-height: 0; padding: 2px 2px;
  font-size: 12.5px; color: var(--bi-ink);
  text-decoration: underline; text-underline-offset: 2px;
  text-decoration-color: var(--bi-rule);
}

/* ── Fixed bottom action bar (phone only) ────────────────────────────────────
   The primary reader CTA, moved into the thumb zone; a quieter "For authors &
   publishers" door rides above it. Authors/publishers are an intentional
   audience, so that door is top-level and persistent — not buried in the
   footer. The desktop block hides this bar entirely (the top bar carries the
   CTAs there). It sits OUTSIDE #cf-stage, so its links are real anchors the
   crossfade tap-handler never intercepts. */
.cf-bottom-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 200;
  background: var(--bi-bg); border-top: 1px solid var(--bi-rule);
  min-height: var(--bi-bottom-bar-height);
  display: flex; flex-direction: column; align-items: stretch; justify-content: center;
  gap: 8px; padding: 11px 16px;
  padding-bottom: calc(11px + var(--bi-safe-bottom));
}
.cf-bottom-bar__start {
  width: 100%; min-height: 50px; font-size: 15px; padding: 0 18px;
}
/* The row beneath the primary CTA: the friction-killer reassurance reads as
   the button's sub-label on the left ("Start reading free — no account
   required"); the authors/publishers door sits on the right. */
.cf-bottom-bar__row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.cf-bottom-bar__note {
  font-family: var(--bi-font-body); font-size: 12.5px;
  color: var(--bi-ink-soft); white-space: nowrap;
}
.cf-bottom-bar__authors {
  display: inline-flex; align-items: center; gap: .35em;
  font-family: var(--bi-font-body); font-size: 12.5px; font-weight: 500;
  line-height: 1; color: var(--bi-accent); white-space: nowrap;
  border-bottom: 1px solid var(--bi-accent); padding-bottom: 1px;
}
/* FAQ — the prominent, can't-miss spot for the FAQ (Preston, 2026-06-15).
   Styled as the authors link's sibling (accent + hairline underline) so the two
   secondary doors read consistently; sits between the reassurance note and the
   authors door via the row's space-between. */
.cf-bottom-bar__faq {
  font-family: var(--bi-font-body); font-size: 12.5px; font-weight: 500;
  line-height: 1; color: var(--bi-accent); white-space: nowrap;
  border-bottom: 1px solid var(--bi-accent); padding-bottom: 1px;
}
.cf-bottom-bar__arrow { font-size: 1.05em; }

/* ── Pinned stage (phone crossfade) ──────────────────────────────────────────
   position:fixed; the document scroll drives the crossfade. The cf-spacer
   (below) defines the scrollable range. Desktop unpins this entirely. */
.cf-stage {
  position: fixed; left: 0; right: 0;
  top: calc(var(--bi-bar-height) + var(--bi-safe-top));
  bottom: calc(var(--bi-bottom-bar-height) + var(--bi-safe-bottom));
  overflow: hidden; z-index: 1;
  background: var(--bi-landing-stage);
}
.cf-spacer { position: relative; z-index: 0; width: 100%; }

/* a beat = one full-stage layer. Every beat stays PAINTED; the crossfade
   engine never sets opacity:0/visibility:hidden as a resting state (an
   un-rasterized full-viewport layer flashes one blank frame the first time
   it's shown). The current beat sits on top via z-index and its opaque image
   occludes the rest; dissolves fade the top layer out to reveal the painted
   layer below. */
.beat {
  position: absolute; inset: 0;
  background: var(--bi-landing-stage);
  opacity: 0;
}
.beat:first-of-type { opacity: 1; }

.beat__media { position: absolute; inset: 0; }
.beat__img { position: absolute; inset: 0; }
.beat__img-el {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover;
  /* Curated focal box (render.py::_focal_style) pans the cover-crop onto its
     region; default is the historical center-crop for un-curated beats. Pan
     only — no zoom (the box size is ignored). */
  object-position: var(--focal-x, 50%) var(--focal-y, 50%);
}
/* Hero's tasteful off-center default, expressed as the focal var so an actual
   curated focal box on the hero img still overrides it. */
.beat--hero { --focal-y: 32%; }

/* Graceful image handling (progressive enhancement). The server emits image
   URLs without pre-checking that the pixels exist (no per-request storage I/O —
   see render.py / images.py), so the FRONTEND owns the "thoughtful, stable"
   degradation: a beat image fades in once it actually decodes, and a failed
   load (e.g. a missing render) stays fully transparent so the dark cinematic
   stage shows through — never the browser's broken-image glyph. landing.js adds
   `.cf-img--managed` (so a no-JS crawler still sees the images in markup),
   then `.cf-img--loaded` on success or `.cf-img--error` on failure. */
.cf-img--managed { opacity: 0; transition: opacity .6s ease; }
.cf-img--managed.cf-img--loaded { opacity: 1; }
.cf-img--managed.cf-img--error { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .cf-img--managed { transition: none; }
}

/* Bottom poster scrim (decision 8) — part of the photograph, never themed.
   Transparent at top, ~0.92 near-black at the bottom band where the text and
   read affordance sit. Replaces the prototype's breathing haze entirely. */
.beat__scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: var(--bi-scrim-bottom);
}

/* the text + read affordance, anchored in the bottom dark band */
.beat__panel {
  position: absolute; left: 0; right: 0;
  bottom: calc(8% + var(--bi-safe-bottom));
  z-index: 2; padding: 0 22px;
  display: flex; flex-direction: column; align-items: center;
  text-align: center; gap: 18px;
  will-change: opacity, transform;
}
.beat__text { width: min(90%, 28rem); }
.beat__label {
  font-family: var(--bi-font-mono); font-size: 10px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--bi-poster-ink-soft); margin: 0 0 11px;
}
.beat__h {
  font-family: var(--bi-font-display); font-style: italic; font-weight: 500;
  color: var(--bi-poster-ink); margin: 0; text-wrap: balance;
  font-size: clamp(29px, 7.6vw, 41px); line-height: 1.04;
}
.beat--hero .beat__h { font-size: clamp(40px, 11.4vw, 64px); line-height: 1.0; }
.beat--detail .beat__h { font-size: clamp(23px, 5.9vw, 32px); line-height: 1.14; }
.beat__sub {
  font-family: var(--bi-font-body); font-size: 14.5px; line-height: 1.56;
  color: var(--bi-poster-ink-soft); margin: 13px auto 0; max-width: 25rem;
}

/* the read affordance — the whole block is one anchor into this book's reader */
.beat__read-block {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
}
.beat__read {
  display: inline-flex; align-items: center; gap: .42em;
  font-family: var(--bi-font-body); font-size: 12px; font-weight: 500;
  letter-spacing: .02em; color: var(--bi-poster-ink);
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-thickness: 1px; text-decoration-color: rgba(235, 228, 214, 0.42);
}
.beat__read .bi-glyph { width: 11px; height: 11px; opacity: .72; flex: 0 0 auto; }
.beat__cite {
  font-family: var(--bi-font-body); font-size: 11.5px; line-height: 1.4;
  color: var(--bi-poster-ink-soft); white-space: nowrap;
}
.beat__cite cite { font-style: italic; font-weight: 500; color: var(--bi-poster-ink); }

/* ── Final CTA beat — full-bleed climax, real buttons replace the read block ─ */
.beat--final .beat__panel { bottom: calc(20% + var(--bi-safe-bottom)); gap: 0; }
.beat__cta { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 22px; }
.beat__cta-solid, .beat__cta-ghost {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--bi-font-body); font-size: 14.5px; font-weight: 500;
  min-height: 44px; padding: 0 20px; border-radius: var(--bi-radius); cursor: pointer;
}
.beat__cta-solid { color: var(--bi-poster-ink-on); background: var(--bi-poster-ink); border: 1px solid var(--bi-poster-ink); }
.beat__cta-ghost { color: var(--bi-poster-ink); background: transparent; border: 1px solid var(--bi-poster-ink); }

/* ── Triptych beat — three genres, one frame, two diagonals ──────────────────
   clip-path polygons carve the frame into top / mid / bottom bands. The two
   diagonals run in DIFFERENT directions (the modern, non-generic feel). Angles
   are driven by --tri-skew so the bands stay balanced phone -> desktop.
   Nothing animates. */
/* The triptych's combined-headline panel is sr-only on phone — the three slice
   titles carry the phrase visually over the full-bleed frame. The >=1020px block
   un-hides it into a normal two-up text panel. */
.beat__panel--triptych {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.tri-frame { --tri-skew: 7%; }
.tri-slice {
  position: absolute; inset: 0; overflow: hidden;
  display: block; cursor: pointer;
}
.tri-slice__img-wrap, .tri-slice__img { position: absolute; inset: 0; }
.tri-slice__img {
  width: 100%; height: 100%; object-fit: cover;
  /* same curated-focal contract as .beat__img-el (pan only, no zoom) */
  object-position: var(--focal-x, 50%) var(--focal-y, 50%);
}
.tri-slice__scrim { position: absolute; inset: 0; background: var(--bi-scrim-bottom); pointer-events: none; }
/* top band: top edge straight, bottom edge dips top-left -> middle-right */
.tri-slice--top { clip-path: polygon(0 0, 100% 0, 100% calc(33% + var(--tri-skew)), 0 calc(33% - var(--tri-skew))); }
/* middle band: between the two diagonals (they cross in opposite directions) */
.tri-slice--mid {
  clip-path: polygon(
    0 calc(33% - var(--tri-skew)), 100% calc(33% + var(--tri-skew)),
    100% calc(66% - var(--tri-skew)), 0 calc(66% + var(--tri-skew)));
}
/* bottom band: top edge runs middle-right -> bottom-left; bottom edge straight */
.tri-slice--bot { clip-path: polygon(0 calc(66% + var(--tri-skew)), 100% calc(66% - var(--tri-skew)), 100% 100%, 0 100%); }
/* Each slice's image fills only its own band — a wide, short slot — not the full
   (tall, on phone) frame. This is the load-bearing fix for focal panning: a
   horizontal focal box only maps to a horizontal display when the slot is itself
   horizontal. With the slot tall, object-fit:cover fills its height and leaves NO
   vertical room for object-position to pan into (the curated y was inert on
   phone). Bounds = each clip band's vertical bounding box, skew-aware so they
   track the clip-path on both viewports. */
.tri-slice--top .tri-slice__img-wrap { top: 0; bottom: calc(67% - var(--tri-skew)); }
.tri-slice--mid .tri-slice__img-wrap { top: calc(33% - var(--tri-skew)); bottom: calc(34% - var(--tri-skew)); }
.tri-slice--bot .tri-slice__img-wrap { top: calc(66% - var(--tri-skew)); bottom: 0; }
.tri-slice--placeholder .tri-slice__img-wrap::after {
  /* the sci-fi gap (no sci-fi book in the v1 pool) — a quiet marked placeholder,
     never a silent two-genre frame passing as three */
  content: ""; position: absolute; inset: 0;
  background: repeating-linear-gradient(135deg,
    rgba(15,10,5,0.0) 0 18px, rgba(15,10,5,0.22) 18px 36px);
}
.tri-slice__title {
  position: absolute; left: 22px; right: 22px; z-index: 2;
  display: flex; gap: .4em; align-items: baseline; flex-wrap: wrap;
  font-family: var(--bi-font-display); font-style: italic; font-weight: 500;
  font-size: clamp(20px, 6vw, 30px); color: var(--bi-poster-ink);
  text-wrap: balance;
}
.tri-slice--top .tri-slice__title { top: calc(7% + var(--bi-safe-top)); }
.tri-slice--mid .tri-slice__title { top: 50%; transform: translateY(-50%); }
.tri-slice--bot .tri-slice__title { bottom: calc(8% + var(--bi-safe-bottom)); }
.tri-slice__cite {
  font-family: var(--bi-font-body); font-style: normal; font-weight: 400;
  font-size: 12px; color: var(--bi-poster-ink-soft);
}

/* ── Micro-reader beat — a real reader page, framed on the dark stage ─────────
   Demonstrates the reading experience with the reader's own grammar. Themed
   (Cashmere/Lamplight) so it looks exactly like the actual reader. */
/* Phone: a STACKED reader demo (Preston, 2026-06-06) — no scrim, no overlay. The
   beat layer becomes a flex column: the reader sits at true size in the upper
   region (the media, which grows to fill), and a dedicated caption band
   ("Reading, not scrolling." + "Begin reading this book now") is its own section
   below it (the panel). Desktop overrides this back to the centered two-up card. */
.beat--reader { display: flex; flex-direction: column; }
.beat--reader .beat__media {
  position: relative; inset: auto; flex: 1 1 auto; min-height: 0;
  display: block; padding: 0;
}
.beat--reader .beat__scrim { display: none; }
.micro-reader {
  display: block; position: absolute; inset: 0; width: 100%; height: 100%;
  background: var(--bi-bg); color: var(--bi-ink); overflow: hidden;
}
.micro-reader__page { padding: 40px 26px 30px; }
.micro-reader__chapter {
  font-family: var(--bi-font-mono); font-size: 10px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--bi-meta); margin: 0 0 20px;
}
.micro-reader__body p {
  font-family: var(--bi-font-body); font-size: 17px; line-height: 1.72;
  color: var(--bi-ink-soft); margin: 0 0 16px;
}
.micro-reader__body p:first-of-type::first-letter {
  font-family: var(--bi-font-display); font-size: 3.1em; line-height: .82;
  float: left; padding: 6px 9px 0 0; color: var(--bi-ink); font-weight: 500;
}
.micro-reader__cite {
  font-family: var(--bi-font-mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--bi-meta); margin: 18px 0 0;
}
.micro-reader__cite cite { font-style: normal; }
/* on the dark stage, the micro-reader's headline + read line are cream */
.beat--reader .beat__read-block--reader { opacity: .9; }
/* the caption band — its own section below the reader (not a scrim over the
   page). Sits on the dark stage; a hairline keeps the seam visible even when the
   reader card is Lamplight-dark and close to the stage in value. */
.beat--reader .beat__panel {
  position: static; flex: 0 0 auto; gap: 12px;
  padding: 22px 22px calc(20px + var(--bi-safe-bottom));
  border-top: 1px solid rgba(235, 228, 214, 0.18);
}

/* ── Device-showcase beat — the real reader across devices ───────────────────
   Floating screen-shaped panels (rounded corners + soft shadow, NO device bezel:
   feel device-like without rendering a device — no notch, no Apple/Android
   hardware). Phone: two phones (a text page + the image that follows) as the top
   ~two-thirds, a landscape tablet spread as the bottom ~third; the combined
   headline is sr-only (the screenshots carry the beat). The >=1020px block
   un-hides the headline into a two-up text half with the cross-device copy. */
.beat__panel--showcase {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.device-showcase {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(10px, 2.5svh, 24px);
  padding: clamp(12px, 3svh, 32px) clamp(16px, 5vw, 34px);
}
.device-showcase__phones {
  display: flex; gap: clamp(10px, 3vw, 18px);
  align-items: center; justify-content: center; width: 100%;
}
.device-screen {
  /* an empty screen (a missing capture) reads as a dark panel on the stage */
  background: var(--bi-landing-stage);
  border-radius: clamp(13px, 3.4vw, 20px); overflow: hidden;
  box-shadow: 0 18px 46px -14px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.32);
}
.device-screen__img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The phone composite sizes by HEIGHT (svh), not width — Safari's URL bar makes
   the phone stage shorter than the viewport, and width-sized panels overflowed
   it and clipped the tablet top/bottom. svh tracks the small (toolbar-shown)
   viewport, so the whole composite shrinks to fit whatever stage it's on. Both
   panels keep their capture aspect-ratio (height set, width derived) so
   object-fit: cover never crops. max-width keeps two phones within the width. */
.device-screen--phone {
  height: min(42svh, 360px); width: auto; max-width: 43vw;
  aspect-ratio: 390 / 844;        /* the iphone capture ratio */
}
.device-screen--tablet {
  /* The tablet spans exactly the two phones' combined OUTER width — 2× phone
     width + the gap between them — so its edges line up under the pair. Phone
     width is height-derived (height × 390/844), so mirror that here and let
     aspect-ratio drive the tablet's height. max-width: 92vw is the narrow-
     viewport overflow guard (binds only if the phones hit their own max-width). */
  width: calc(min(42svh, 360px) * 390 / 844 * 2 + clamp(10px, 3vw, 18px));
  height: auto; max-width: 92vw;
  aspect-ratio: 1194 / 834;       /* the ipad landscape capture ratio */
}
/* The orientation caption — what the three screenshots are. First DOM child of
   .device-showcase (column flex), so on phone it lands at the TOP, above the
   phones, light against the dark stage. The >=1020px block re-orders it below
   the tablet and recolors it for the paper background (see there). */
.device-showcase__caption {
  margin: 0; text-align: center; text-wrap: balance;
  font-family: var(--bi-font-body); font-style: italic;
  font-size: clamp(12.5px, 3.4vw, 15px); line-height: 1.4;
  color: var(--bi-poster-ink-soft); max-width: 24rem;
}

/* ── Paper beat (author / footer) ────────────────────────────────────────── */
.beat--paper { background: var(--bi-bg); }
.cf-paper {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  padding: 30px 26px calc(26px + var(--bi-safe-bottom));
}
.cf-paper__eyebrow { margin: 0 0 16px; letter-spacing: .18em; }
.cf-paper__h {
  font-family: var(--bi-font-display); font-style: italic; font-weight: 500;
  font-size: clamp(24px, 6.4vw, 32px); line-height: 1.1; margin: 0 0 14px;
  color: var(--bi-ink); max-width: 22rem;
}
.cf-paper__lede {
  font-family: var(--bi-font-body); font-size: 14.5px; line-height: 1.55;
  color: var(--bi-ink-soft); max-width: 26rem; margin: 0 0 14px;
}
.cf-paper__link {
  font-family: var(--bi-font-body); font-size: 14.5px; color: var(--bi-accent);
  border-bottom: 1px solid var(--bi-accent); font-weight: 500;
}
.cf-paper__rule { width: 100%; max-width: 30rem; height: 1px; background: var(--bi-rule); margin: 30px 0 26px; }
.cf-paper__cols { display: grid; grid-template-columns: repeat(3, auto); gap: 0 28px; text-align: left; margin: 0 auto; }
.cf-paper__col h3 {
  font-family: var(--bi-font-mono); font-size: 9.5px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--bi-meta); margin: 0 0 10px; font-weight: 500;
}
.cf-paper__col a {
  display: block; font-family: var(--bi-font-body); font-size: 13px;
  color: var(--bi-ink-soft); margin-bottom: 7px;
}
.cf-paper__base { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; margin-top: 28px; }
.cf-paper__base .bi-wordmark { font-size: 11px; }
.cf-paper__built {
  font-family: var(--bi-font-mono); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--bi-meta);
}

/* ── Theme toggle + scroll indicator (ported primitives) ─────────────────── */
.bi-theme-toggle {
  display: inline-flex; align-items: center; border: 1px solid var(--bi-rule);
  background: transparent; padding: 0; cursor: pointer; height: 30px;
  font-family: var(--bi-font-mono); font-size: 9.5px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--bi-meta);
}
.bi-theme-toggle span { padding: 0 9px; line-height: 28px; }
.bi-theme-toggle span[data-active="true"] { color: var(--bi-bg); background: var(--bi-ink); }
.bi-scrollbar {
  position: fixed; top: 0; right: 3px; width: 3px; height: 100%;
  z-index: 250; pointer-events: none; opacity: 0; transition: opacity .3s ease;
}
.bi-scrollbar.is-active { opacity: 1; }
.bi-scrollbar__thumb {
  position: absolute; top: 0; right: 0; width: 3px; min-height: 28px;
  background: var(--bi-poster-ink-soft); opacity: .5; will-change: transform;
}

/* ============================================================================
   DESKTOP / TABLET — two-up alternating spreads (>= 1020px)
   Unpin the stage; native vertical scroll + snap. Each beat is a full-viewport
   two-up: text on one half (on paper), image on the other; the side alternates
   each section; the headline stays vertically centered every section so only
   the horizontal side flips. Triptych + final stay full-bleed (the two
   deliberate breaks in the rhythm); the footer lifts out into a normal band.
   ============================================================================ */
@media (min-width: 1020px) {
  body.bi-landing { background: var(--bi-bg); --bi-bar-height: 50px; }

  .cf-stage {
    position: static; overflow: visible; z-index: auto;
    background: var(--bi-bg);
    /* offset the FIRST spread below the fixed bar; snapped spreads are handled
       by scroll-padding-top on the snap container (both = the bar height). */
    padding-top: var(--bi-bar-height);
  }
  .cf-spacer { display: none; }

  /* Each spread is exactly the screenful BELOW the fixed bar: height is
     100dvh − bar (no top padding). The stage's padding-top + the snap
     container's scroll-padding-top (both = bar) keep every spread just beneath
     the bar. Sizing the beat to the visible area — rather than a full 100dvh
     with a top padding band — lets the gutter border run the FULL beat height,
     so adjacent spreads' borders touch into one continuous spine instead of
     breaking at each section's padding band. */
  .beat {
    position: relative; inset: auto;
    /* opacity 1 is the resting/no-JS/reduced-motion state (overrides the phone
       base .beat{opacity:0}); NOT !important, because the cf-scrub engine sets
       inline opacity per spread to drive the dissolve and must be able to win. */
    opacity: 1;
    height: calc(100vh - var(--bi-bar-height));
    height: calc(100dvh - var(--bi-bar-height));
    background: var(--bi-bg);
    display: grid; grid-template-columns: 1fr 1fr;
    /* single row pinned to the content height (minmax(0,1fr), NOT auto) so the
       full-height image's height:100% resolves against a definite track —
       otherwise an auto row sizes to the image's intrinsic height, the image
       falls back to width-driven sizing, and it overflows the viewport. */
    grid-template-rows: minmax(0, 1fr);
    scroll-snap-align: start;
  }

  /* image right / text left by default; alternate every other spread.
     The gutter is always a border-LEFT on whichever element sits in
     column 2 (media on odd spreads, panel on even). Drawing it on the
     same logical edge every section keeps the 1px line at an identical
     pixel — with box-sizing: border-box a border-left renders just right
     of the column boundary while a border-right renders just left of it,
     so flipping the border side per section offset the gutter by 1px. */
  .beat__media { position: relative; inset: auto; grid-column: 2; grid-row: 1;
    border-left: 1px solid var(--bi-rule); }
  /* Text hugs the gutter (spine), not the outer edge: the column pins to the
     gutter side of its half while the text itself stays left-aligned, so the
     OUTER edge breathes. The hug-side flips with the alternating layout —
     odd: panel in col 1 (left), gutter on its right → align-items: flex-end;
     even: panel in col 2 (right), gutter on its left → align-items: flex-start. */
  .beat__panel { position: static; grid-column: 1; grid-row: 1;
    align-items: flex-end; text-align: left;
    justify-content: center; padding: 64px clamp(40px, 6vw, 96px);
    bottom: auto; gap: 22px;
    /* the content-enter fade-in (compositor-only; resting state visible) */
    opacity: 1; }
  .beat:nth-of-type(even) .beat__media { grid-column: 1; border-left: 0; }
  .beat:nth-of-type(even) .beat__panel { grid-column: 2; border-left: 1px solid var(--bi-rule);
    align-items: flex-start; }

  /* text is on paper now, not over an image — ink, not poster-ink; no scrim */
  .beat__scrim { display: none; }
  /* ── Full-height spread illustration, glued to the gutter (2026-06-22) ──
     Replaces the 2026-06-15 limited-cover frame on the standard image beats.
     The image now fills the full content height (100dvh − bar) and shows the
     WHOLE illustration (object-fit: contain) so the baked-in watermark is
     never cropped. Width follows the image's own aspect, so a portrait scene
     leaves intentional whitespace on its OUTER edge while its inner edge hugs
     the gutter. The media half is a flexbox that pins the image to the gutter
     side (which flips with the alternating layout). Landscape images can't be
     both full-height AND inside the half — max-width clamps them so they
     letterbox rather than cross the gutter. Scoped via :has() to the standard
     image beats; triptych / device-showcase / micro-reader carry no
     .beat__img and keep their own rules. Phone keeps full-bleed.
     Background: docs/potential_work_plans/desktop_landing_scrollytelling.md. */
  .beat__media:has(> .beat__img) {
    display: flex; align-items: stretch;
    justify-content: flex-start;   /* odd: media col 2 (right), gutter on its left → hug left */
  }
  .beat:nth-of-type(even) .beat__media:has(> .beat__img) {
    justify-content: flex-end;     /* even: media col 1 (left), gutter on its right → hug right */
  }
  .beat__media > .beat__img {
    position: static; inset: auto; transform: none;
    height: 100%; width: auto; max-width: 100%;
    display: flex; overflow: visible;
  }
  .beat__media > .beat__img > .beat__img-el {
    position: static; inset: auto;
    height: 100%; width: auto; max-width: 100%;
    object-fit: contain; object-position: center;
  }
  /* The text block and the read affordance share ONE measure so, when both are
     pushed to the gutter side (align-items per parity), their LEFT edges line up
     — otherwise the narrower "Begin reading this book now" link floats off to the
     gutter, misaligned under the header. */
  .beat__text { width: min(100%, 30rem); max-width: none; }
  .beat__h { color: var(--bi-ink); }
  .beat--hero .beat__h { font-size: clamp(44px, 4.4vw, 72px); }
  .beat__h:not(.beat--hero .beat__h) { font-size: clamp(30px, 3.2vw, 46px); }
  .beat--detail .beat__h { font-size: clamp(26px, 2.6vw, 38px); }
  .beat__label { color: var(--bi-meta); }
  /* Desktop type is bigger across the board EXCEPT the H1/H2 headlines (Preston:
     "Reading, re-enchanted" is big enough; make everything else bigger). */
  .beat__sub { color: var(--bi-ink-soft); margin-left: 0; margin-right: 0; max-width: 34rem;
    font-size: clamp(18px, 1.9vw, 25px); line-height: 1.5; }
  /* if a title beat ever carries a supporting line it reads nearly as large as
     its headline — so keep title subs to one short line (a denser, multi-sentence
     intro should be a `detail` beat, whose sub stays body-sized). */
  .beat--title .beat__sub { font-size: clamp(22px, 2.5vw, 32px); }
  .beat__read-block { align-items: flex-start; width: min(100%, 30rem); }
  .beat__read { color: var(--bi-ink-soft); text-decoration-color: var(--bi-rule); font-size: 15px; }
  .beat__cite { color: var(--bi-meta); font-size: 13.5px; }
  .beat__cite cite { color: var(--bi-ink); }

  /* content fade-in on enter — OPACITY ONLY (compositor only), no translateY.
     A transformed (shifted) element extends the scrollable overflow region,
     and with scroll-snap mandatory that phantom +16px on every un-revealed beat
     fights the snap and makes the page feel unscrollable. Opacity-only is also
     the literal "fade each section in" the design asked for. The whole spread
     (text + full-height image) settles together as it snaps into view. */
  .beat__panel { transition: opacity .6s ease; }
  .beat.is-pending .beat__panel { opacity: 0; }
  .beat__media { transition: opacity .6s ease; }
  .beat.is-pending .beat__media { opacity: 0; }

  /* hero: keep two-up but let the image bleed taller (focal var so a curated
     hero focal box still wins) */
  .beat--hero .beat__media { --focal-y: 30%; }

  /* ── triptych + final: two-up like every other spread (Preston: NOT full-bleed —
        at full width the images cropped weirdly). Image on one half, text on the
        other. Both now ride the default .beat two-up grid; we just undo the
        phone-only treatments. ── */
  /* triptych: un-hide the sr-only headline panel into the text half; the 3-genre
     tri-frame rides the image half via the default .beat__media rules. */
  .beat--triptych .beat__panel--triptych {
    position: static; width: auto; height: auto; margin: 0;
    overflow: visible; clip: auto; white-space: normal;
  }
  /* Match the standard scene-image footprint: instead of filling the whole media
     half (which made "Strange worlds…" wider than every other image and broke the
     rhythm), the tri-frame becomes a portrait box the same 2:3 size as a
     .beat__img-el, pinned to the gutter. position: relative re-anchors the
     absolute slices to the frame so the clip-bands ride the narrower box. */
  .beat--triptych .beat__media { display: flex; align-items: stretch; justify-content: flex-start; }
  .beat--triptych:nth-of-type(even) .beat__media { justify-content: flex-end; }
  .beat--triptych .tri-frame {
    position: relative; height: 100%;
    width: calc((100dvh - var(--bi-bar-height)) * 0.667); max-width: 100%;
    --tri-skew: 5%;
  }
  .tri-slice__title { font-size: clamp(16px, 1.7vw, 24px); }

  /* final CTA: now on paper — ink (not poster-ink), left-aligned, real buttons. */
  .beat--final .beat__cta { justify-content: flex-start; }
  .beat--final .beat__cta-solid { color: var(--bi-bg); background: var(--bi-ink); border-color: var(--bi-ink); }
  .beat--final .beat__cta-ghost { color: var(--bi-ink); background: transparent; border-color: var(--bi-ink); }

  /* ── micro-reader: text-left / reader-card-right two-up (resets the phone
        full-bleed page back to a centered, framed card; scrim hidden via the
        general .beat__scrim display:none above) ── */
  .beat--reader .beat__media { display: flex; align-items: center; justify-content: center; padding: 80px clamp(40px, 6vw, 96px); }
  /* reset the phone stacked layout back to the desktop two-up grid */
  .beat--reader { display: grid; }
  .beat--reader .beat__panel {
    z-index: auto; position: static; border-top: 0;
    padding: 64px clamp(40px, 6vw, 96px); gap: 22px;
  }
  .micro-reader {
    position: static; inset: auto; width: min(100%, 30rem); height: auto; max-height: 100%;
    border: 1px solid var(--bi-rule); box-shadow: 0 24px 60px rgba(0,0,0,0.22);
  }
  .beat--reader .beat__read-block--reader { display: none; }

  /* ── device-showcase: un-hide the headline into the text half (the cross-device
        copy); the screen composite rides the image half via the default
        .beat__media two-up rules. ── */
  .beat--device-showcase .beat__panel--showcase {
    position: static; width: auto; height: auto; margin: 0;
    overflow: visible; clip: auto; white-space: normal;
  }
  .device-showcase { padding: 64px clamp(32px, 4vw, 72px); gap: clamp(16px, 2vw, 28px); }
  /* Desktop: the composite rides the (full-height) image half of the two-up, so
     size by WIDTH here — reset the phone's svh height back to auto. */
  .device-screen--phone { height: auto; width: clamp(120px, 11vw, 168px); }
  .device-screen--tablet { height: auto; width: min(86%, 460px); max-width: min(86%, 460px); }
  /* Caption moves to the BOTTOM (below the tablet) on the paper half — order
     past the phones+tablet (both order 0), and recolor for the light background. */
  .device-showcase__caption { order: 1; color: var(--bi-meta); font-size: 14px; max-width: min(86%, 460px); }

  /* ── footer: a normal full-width band, not a scroll beat ── */
  .beat--paper {
    display: block; height: auto; min-height: auto; padding-top: 0;
    background: var(--bi-bg);
    border-top: 1px solid var(--bi-rule);
    scroll-snap-align: none;   /* not a snap target — scroll freely into it past the last spread */
  }
  .beat--paper .cf-paper { position: static; inset: auto; padding: 64px 40px; }
  .beat--paper .cf-paper__cols { gap: 0 56px; }

  /* the fixed bar persists; its CTAs are comfortable size on desktop */
  .cf-bar { padding: 10px 28px; }
  .cf-bar__wordmark { font-size: 14px; }
  .cf-bar .bi-btn { font-size: 13px; padding: 10px 16px; }

  /* Desktop is unchanged from v1: the phone-only bottom action bar is hidden,
     and both top-bar CTAs return to boxed buttons (undo the phone treatments). */
  .cf-bottom-bar { display: none; }
  .cf-bar__start { display: inline-flex; }
  .cf-bar__login {
    border: 1px solid var(--bi-ink); background: transparent;
    min-height: 38px; padding: 10px 16px; font-size: 13px; text-decoration: none;
  }
}

/* native scroll-snap on the desktop spreads — MANDATORY so the viewport always
   lands on a full spread (no half-of-one / half-of-the-next on screen ever).
   scroll-padding-top reserves the fixed bar so keyboard/tap scrollIntoView lands
   each spread just beneath it. No scroll-behavior: smooth — tap/keyboard nav is
   an instant cut to the next spread (Preston: no "going up and down" travel
   animation). Manual scroll gets the mandatory snap. */
@media (min-width: 1020px) {
  /* SNAP TEMPORARILY DISABLED. CSS scroll-snap mandatory felt janky on macOS
     trackpads (sticks on a section, then jumps) — the classic native-snap vs.
     momentum-scroll fight. Reverted to plain free native scroll while we
     evaluate the rest of the redesign and decide the snap approach separately
     (options: `proximity` — gentler, now that overflow-x:clip lets snap engage
     at all; or porting the phone's JS-driven pinned crossfade, which is smooth
     because JS drives it).
     overflow-x: clip is kept (not reverted to the base `hidden`) on purpose:
     `hidden` makes <body> a scroll container, and we want <html> to stay the
     scroller so re-enabling snap later Just Works. scroll-padding-top keeps
     keyboard/tap nav landing each spread below the fixed bar. */
  html, body.bi-landing { overflow-x: clip; }
  html { scroll-snap-type: none; scroll-padding-top: 50px; }
}

/* ── Desktop crossfade (JS-activated via html.cf-scrub) ─────────────────────
   landing.js adds `cf-scrub` to <html> on desktop (unless the user prefers
   reduced motion) and drives the same scroll-scrubbed cross-dissolve engine the
   phone uses, with separate desktop tuning (TUNE_DESKTOP in landing.js). The
   stage pins below the bar, every spread stacks absolutely full-screen, and
   scroll scrubs opacity — one full spread always on screen, never half-and-half,
   with no native-snap-vs-trackpad fight (JS owns it). Each spread's INNER layout
   (two-column grid, full-height gutter-glued image, spine) is the native-scroll
   block above, unchanged — only outer positioning differs. That native block is
   the no-JS / reduced-motion fallback: readable, crawlable, SEO-safe. Beat
   opacity stays 1 (containers painted); the dissolve is driven by per-spread
   media/panel opacity + z-index in renderPhone, so we set no opacity here. */
@media (min-width: 1020px) {
  html.cf-scrub .cf-stage {
    position: fixed; top: var(--bi-bar-height); left: 0; right: 0; bottom: 0;
    overflow: hidden; padding-top: 0; z-index: 1;
  }
  html.cf-scrub .cf-spacer { display: block; }   /* JS sets its height */
  html.cf-scrub .beat { position: absolute; inset: 0; height: 100%; }
  /* the dissolve animates the beat CONTAINER's opacity (so the paper bg fades
     with it); promote only the 2 live spreads so the compositor stays cheap */
  html.cf-scrub .beat.is-live { will-change: opacity; }
  /* footer is the final full-screen spread in the dissolve (mirrors phone) */
  html.cf-scrub .beat--paper {
    position: absolute; inset: 0; height: 100%;
    display: flex; align-items: center; justify-content: center;
  }
  /* the JS scrub owns opacity — kill the native-block transitions so the
     compositor doesn't double-animate the dissolve */
  html.cf-scrub .beat__media, html.cf-scrub .beat__panel { transition: none; }
}

/* ── Reduced motion — snap cuts, no fade/drift/smooth-scroll ──────────────── */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .beat__panel, .beat__media { transition: none !important; }
  .beat.is-pending .beat__panel { opacity: 1 !important; transform: none !important; }
  .beat.is-pending .beat__media { opacity: 1 !important; }
}
