:root {
  --ink: #0b0613;
  --paper: #f7f5f0;
  --muted: rgba(247, 245, 240, 0.65);
  --rule: rgba(247, 245, 240, 0.2);
  --accent: #6a2a96; /* royal purple — UI accent (nav, buttons, focus states) */
  --max-width: 720px;
  --font-serif: "Iowan Old Style", "Palatino Linotype", Georgia, "Times New Roman", serif;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --panel-footer-gap: 1rem; /* gap between main's panel and <footer> on About/Book a Reading/FAQ/Contact — change once here */
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  scrollbar-gutter: stable;
}

/* The page defaults to dark (open, unbounded) — content is read as a lit
   room within it (bounded, resolved), never the other way around. */
body {
  margin: 0;
  background-color: var(--ink);
  color: var(--paper);
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.7;
}

/* Paragraph copy reads slightly large on phones relative to desktop's
   17px — sized down to a flat 16px at the user's request, rather than
   the desktop value scaling straight down with the viewport. */
@media (max-width: 640px) {
  body {
    font-size: 16px;
  }
}

/* Whole-page fade-in — homepage-only (the user liked it there but not
   on the other four pages), so this is scoped to `body.page-fade`
   (set on `index.html`'s `<body>` only) rather than plain `body`.
   `js/page-fade.js` adds `.is-loaded` on the window's `load` event (a
   "fuller load", not `DOMContentLoaded`, so this waits for the header
   photo and other assets too, revealing an already-painted page rather
   than one that's still popping in piece by piece) — also only loaded
   on `index.html` now. `body`'s own background is `var(--ink)` and
   paints immediately regardless (see the comment above), so this only
   fades in the *content* on top of that dark field — consistent with
   the dark field being the starting state, not something also being
   faded in. If JS is unavailable, the `<noscript>` fallback in
   `index.html`'s `<head>` forces this back to visible immediately —
   without it, the homepage with JS disabled or blocked would stay
   invisible forever. */
body.page-fade {
  opacity: 0;
  transition: opacity 1.618s ease-out;
}

body.page-fade.is-loaded {
  opacity: 1;
}

h1, h2, h3 {
  font-family: var(--font-serif);
  font-weight: 400;
  margin: 0 0 0.5em;
}

p {
  margin: 0 0 1em;
}

a {
  color: inherit;
}

/* Divider — a quiet rule marking the threshold between a page's dark
   intro and the content that follows */

.divider {
  width: 5.5rem;
  height: 1px;
  background: var(--rule);
  margin: 1.75rem auto;
}

/* Header */

/* Default (About/Book a Reading/FAQ/Contact): height fits its nav
   content instead of a fixed vh. On desktop, `background-attachment:
   fixed` + `background-size: cover` are computed against the
   *viewport*, not this element's own (short) box — the same math the
   homepage's `header.header-full` uses. Since both headers sit at the
   very top of an unscrolled page, this guarantees the short header
   shows exactly the same slice of Banner-top.webp that appears behind
   the homepage's nav, regardless of window size/aspect ratio, rather
   than a fixed percentage that only lined up for one aspect ratio.
   At 640px and below (real phones), `background-attachment: fixed` is
   unreliable on iOS Safari, so this rule is overridden there — see
   the mobile blocks near `.header-full-bg` further down. */
header {
  position: relative;
  padding: 16px 1.5rem;
  height: auto;
  background-color: var(--ink);
  background-image: url("../Banner-top.webp");
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
  background-repeat: no-repeat;
}

/* Homepage-only override: a full-viewport hero. Width is already
   full-bleed via the block-level default, not literal `100vw` — that
   unit counts the scrollbar on desktop and causes a sitewide
   horizontal scroll. `min-height` (not a fixed `height`) plus the flex
   column below let the header grow taller than one screen on a short
   viewport rather than clipping or forcing its title/subtext into an
   overlap — see `.header-invoke-stack` for why. `100vh` is kept first
   as a fallback for browsers without `svh` support, then overridden by
   `100svh`. This went through three viewport units before landing
   here: plain `100vh` on iOS Safari is fixed to the browser chrome's
   *collapsed*-toolbar height, so on initial load (chrome still
   expanded) the header rendered shorter than the true visible
   viewport, letting `.hero` peek in beneath it. `100dvh` (tracks the
   actual current viewport continuously) fixed that, but continuously
   recalculating this min-height — and therefore reflowing the page
   underneath — as the chrome collapses/expands mid-scroll was reported
   as a stutter. `100lvh` (the *largest* possible viewport, assuming
   chrome fully collapsed) is static, fixing the stutter, but on a
   normal page load (chrome still visible, the default state) the box
   is then taller than what's actually visible, and `.header-invoke-
   stack`'s bottom-anchored content (`justify-content: flex-end`) sits
   below the visible area, cut off by the browser's own UI. `100svh` —
   the *smallest* possible viewport, assuming chrome is always shown —
   is the remaining static option: like `lvh` it never changes while
   scrolling, so no stutter, and being the smallest guarantees this
   box is never taller than what's actually visible in any chrome
   state, so nothing inside it can ever be cut off. The trade-off is
   the opposite of `lvh`'s: once the chrome *does* collapse (more
   screen becomes visible than this static-small box accounts for),
   `.hero` can peek in slightly beneath the header for that moment —
   judged the lesser problem, since it's transient (only during the
   scroll past the header, gone once further scrolling clears it
   entirely) rather than a permanently unreachable button. The
   pinned background photo itself is a separate concern: on desktop
   it's this rule's own `background-attachment: fixed`; at 640px and
   below that's replaced by the `.header-full-bg` layer, which is
   sized with `lvh` (not `svh`) — see the mobile blocks further down. */
header.header-full {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
  width: 100%;
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

/* No longer positioned independently by percentage (see the
   `.header-invoke-stack` comment below for why that broke down) — this
   and `.header-invoke-lower` are now plain flex items, stacked and
   spaced by `.header-invoke-stack`'s `gap`. */
.header-invoke {
  width: 100%;
  max-width: 58rem;
  margin: 0;
  padding: 0 1.5rem;
  text-align: center;
}

/* The title surfaces out of scattered order, not left-to-right, and
   differently every load — the small inline script at the bottom of
   index.html shuffles the letters, splits them into randomly-sized
   groups (2-4 letters), and assigns each group a delay so a given
   instant can light up letters from all over the phrase at once
   rather than reading in order. That same script also sets each
   letter's own `animation-duration` so every letter's delay + duration
   sums to the same 1.5s finish line — early-starting letters get a
   long, slow fade and late-starting letters get a short, quick one,
   so the whole sentence lands in focus together rather than trailing
   off one letter at a time. The `0.5s` below is just a pre-script
   fallback. `animation-play-state: paused` holds every letter invisible
   (`opacity: 0` is the animation's own `from` state) until the script
   has finished assigning delays/durations and sets them running —
   without the pause, letters would flash in with the default 0s delay
   for a frame before the script catches up. `.header-invoke-title`
   itself doesn't animate — it's just the shared type styling for its
   `.letter` children. Each letter needs `display: inline-block` for
   the animation's `transform`/`opacity` to apply per-glyph, but that
   also gives every letter its own line-break opportunity regardless
   of whitespace — unlike plain inline text, adjacent inline-blocks can
   wrap between *any* two of them, not just at real word spaces, which
   on narrow screens split words mid-letter (e.g. "connected" wrapping
   as "co" / "nnected"). `.word` wraps each word's letters and pins
   `white-space: nowrap` on that wrapper so the word can't break
   internally — line breaks can still happen at the real spaces
   between `.word` spans, just not inside one. */
.header-invoke-title {
  margin: 0 0 1rem;
  font-family: var(--font-serif);
  font-style: normal;
  font-weight: 400;
  letter-spacing: -0.01em;
  font-size: clamp(2.25rem, 6vw, 4.25rem);
  line-height: 1.2;
  color: var(--paper);
  text-shadow: 0 4px 32px rgba(0, 0, 0, 0.7);
}

.header-invoke-title .word {
  display: inline-block;
  white-space: nowrap;
}

.header-invoke-title .letter {
  display: inline-block;
  opacity: 0;
  filter: blur(5px);
  transform: translateY(88px);
  transition-property: opacity, filter, transform;
  transition-timing-function: ease-out;
  transition-duration: 0.5s;
}

.header-invoke-title .letter.is-revealed {
  opacity: 1;
  filter: none;
  transform: none;
}

/* Sibling of `.header-invoke` (the title) inside `.header-invoke-stack`
   — see the big comment below for how the two are spaced. */
.header-invoke-lower {
  width: 100%;
  max-width: 36rem;
  margin: 0;
  padding: 0 1.5rem;
  text-align: center;
}

/* `.header-invoke` (title) and `.header-invoke-lower` (subtext + CTA)
   used to be positioned independently, each anchored as a percentage
   of the *whole* header (title via `top`, subtext via `bottom`), so
   the subtext could sit low near the bottom of the photo without
   moving the title. That only worked when both blocks' heights stayed
   small and predictable relative to the header's own height — which
   broke down repeatedly in practice: the title can wrap onto 2–3 lines
   instead of one on a narrower window (so its block is much taller
   than a percentage anchor assumed), a short-but-wide desktop window
   (a laptop screen, DevTools' responsive mode) leaves much less
   headroom between the two anchors than a merely-narrow one does, and
   neither anchor has any awareness of the other's actual rendered
   height to react to. Repeated rounds of hand-tuning the percentages
   and font sizes together (see git history on this file if curious)
   kept fixing one width/height combination while breaking another.
   Replaced with what the mobile layout already did on its own for
   exactly this reason: `.header-invoke-stack` (wrapping both blocks)
   is a flex column filling whatever space `.nav-wrap` leaves below it,
   with `.header-invoke` and `.header-invoke-lower` as plain flow
   children spaced by a real `gap` — flexbox guarantees the two boxes
   never overlap regardless of how many lines the title wraps to or how
   short the viewport is, which no combination of independent
   percentages could. This is now the base behavior for every width,
   not just mobile; the media queries below only retune `justify-
   content`/`gap`/`padding`/font sizes per breakpoint, not the
   underlying approach. A first pass used `grid-template-rows: auto
   1fr` on a `height: 100vh` header — that fixes the overlap on
   *typical* phones, but on a genuinely short viewport (an iPhone SE,
   or any phone in landscape) the nav + title + subtext still don't fit
   in a strict 100vh, and `justify-content: center` on a too-small
   `1fr` row just overflows symmetrically — colliding with the nav
   above and pushing the "Find Out" button off-screen below, worse than
   the overlap this was meant to fix. Swapped `height: 100vh` for
   `min-height: 100vh` (header is a flex column, `.header-invoke-stack`
   gets `flex: 1`; that `100vh` has since become `100svh`, see
   `header.header-full` above) so the header can grow taller than one
   screen on a short viewport rather than clipping — flexbox's default
   `min-height: auto` on a flex item means it won't shrink a child
   below what its content actually needs. On every normal-height
   window this still renders as exactly one screen, since `flex: 1`
   fills precisely the leftover space down to `min-height`'s floor. */
.header-invoke-stack {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Desktop default: `flex-end` so the title/subtext pair sits low in
     the leftover space below the nav — matching where the old
     percentage-based layout (title low via `top: 74-79%`, subtext
     anchored to `bottom: 3%`) used to put them — rather than floating
     centered in the middle of the screen. `gap` (not a percentage
     guess) controls the space between title and subtext directly —
     this is the value to change if a future "gap too wide/narrow"
     request comes up, rather than reaching for a position/percentage
     workaround again. `padding-bottom` keeps the pair off the very
     bottom edge, echoing the old layout's `bottom: 3%` breathing room. */
  justify-content: flex-end;
  gap: 0.1rem;
  padding-bottom: 0.8rem;
}

@media (max-width: 640px) {
  .header-invoke-stack {
    /* `flex-end` (was `center`) at the user's request, so the title/
       subtext/CTA sit low in the header rather than floating
       mid-header. This pushes all the leftover vertical space in the
       flex column above the content instead of splitting it top/
       bottom, which is what creates the requested empty gap under the
       logo/nav — `padding-top` below is just a guaranteed minimum for
       that gap on shorter viewports where there's little leftover
       space to push into. The "Find Out" button previously had its own
       `position: fixed` override here, pinned to the viewport so it
       stayed on screen while scrolling — removed at the user's
       request, since it should scroll away with the page like it does
       at wider widths, not stay fixed. It's back in normal flow inside
       `.header-invoke-lower`. */
    justify-content: flex-end;
    align-items: center;
    /* Closed from 1.5rem at the user's request, to bring the title and
       subtext visually closer together on mobile. This is the gap
       between `.header-invoke` (title) and `.header-invoke-lower`
       (subtext + the "Find Out" button, back in normal flow together —
       see below). An initial pass to 0.75rem alone wasn't visibly
       different, because `.header-invoke-title`'s own `margin-bottom`
       (zeroed below) was adding on top of this and masking the
       reduction. */
    gap: 0.5rem;
    /* Bottom padding — at the user's request, tuned so the "Find Out"
       button (the last thing in this flex-end stack, after the
       subtext) lands roughly centered on `.scroll-top`/`.scroll-down`'s
       height band (both `bottom: 1.5rem`, 2.5rem tall, centered around
       2.75rem from the viewport bottom) at initial, unscrolled load —
       this padding is the distance from the viewport bottom to the
       button's own bottom edge. An initial 1.4rem estimate (reused from
       the button's own briefly-fixed positioning earlier — see git
       history) was hand-tuned down further to this value by eye.
       Deliberately NOT `position: fixed` this time — the button stays
       in normal flow and scrolls away with the page like every other
       width, so this alignment only holds at the initial scroll
       position, not after scrolling (which is the point: the user
       explicitly didn't want it following the scroll). */
    padding: 3rem 0 0.25rem;
  }

  .header-invoke-title {
    /* Raised the floor from 2rem so the title:subtext size contrast
       stays closer to desktop's ~4.25:1 (title's 4.25rem max over the
       subtext's un-overridden 1rem) — at the old 2rem/1rem the ratio
       had flattened to 2:1, reading much less dramatic than desktop.
       Not pushed all the way to 4.25:1, though: at mobile widths that
       would mean either an oversized, overflowing title or an
       illegibly tiny subtext. */
    font-size: clamp(2.5rem, 8vw, 4.25rem);
    /* This margin (1rem on the base rule) sits inside `.header-invoke`
       and adds to the `.header-invoke-stack` flex `gap` above it,
       rather than being replaced by it — the first pass at closing
       this gap only touched the flex `gap` and missed this, which is
       likely why it looked like nothing changed. Zeroed here so the
       flex `gap` is the only thing controlling the space to the
       subtext on mobile. */
    margin-bottom: 0;
  }

  .header-invoke-sub {
    /* 1rem (16px) at the user's request, matching the site's mobile
       paragraph copy size — was 0.85rem. */
    font-size: 1rem;
    /* Nudges just the subtext lower (title and button stay put).
       `top`, not `transform`, since the reveal transition owns
       `transform`. */
    position: relative;
    top: 0.4rem;
  }

  /* Tightens the subtext-to-button gap specifically on real phones —
     this was tuned against DevTools' responsive-mode viewport, which
     doesn't subtract a real phone's browser chrome (address bar, tab
     strip) from the reported height, so the same layout read as more
     spaced-out on an actual device than it did while tuning it. Only
     overrides the shared `.btn`'s `1.5rem` margin-top; the separate
     max-height:650px query below sets the same value for short
     viewports. */
  .header-invoke-lower .btn-light {
    margin-top: 0.75rem;
  }
}

/* Title floor bumped slightly, but only from 420px up — iPhone 16
   Pro Max (430px) and wider phones get the bigger 2.75rem title.
   Two earlier attempts (a flat 2.75rem, then a `vw`-scaled version
   easing in from 385px) both still pushed the title from 3 lines to 4
   on iPhone 16 (393px) and Samsung Galaxy A55 (~412px) — even a couple
   px of increase was enough to tip those two over, since their 3-line
   wrap at the original 2.5rem had no spare room to begin with. Per the
   user's call, those widths (which also covers the Pixel 9/9 Pro/10
   family, sharing A55's ~412px) are left at the iPhone SE's original
   2.5rem rather than risk the extra line — only 420px+ is confirmed
   to have enough slack for the bigger size. */
@media (min-width: 420px) and (max-width: 640px) {
  .header-invoke-title {
    font-size: 2.75rem;
  }
}

/* Samsung Galaxy A55 tested at 360px — narrower than the iPhone SE
   (375px), which is what the base mobile rule's 2.5rem floor was
   actually tuned against. That extra 15px is enough to push the title
   to 4 lines at 360px even though 2.5rem fits in 3 at 375px+. A small,
   dedicated step down just for widths under the SE's own, so nothing
   at 375px and up (including the SE itself) is affected. Hand-picked,
   not derived — check on the actual device/DevTools size if this still
   wraps to 4 lines. */
@media (max-width: 374px) {
  .header-invoke-title {
    font-size: 2.25rem;
  }
}

/* Targets viewport *height*, not width — a short window (an older/
   compact phone like iPhone SE, or any phone in landscape) leaves so
   little room below the stacked mobile nav that even the shrunk
   `.header-invoke-title` above plus the full subtext still don't fit
   in one screen (`min-height: 100svh`, see `header.header-full`
   above), forcing the header to grow enough that the subtext can end
   up rendered behind
   the fixed scroll buttons at the initial, unscrolled position.
   Shrinking the subtext and tightening the stack's spacing here keeps
   content compact enough to avoid that growth (and the collision that
   comes with it) on these viewports, rather than trying to out-guess
   exactly where the fixed buttons' footprint lands after the fact. */
@media (max-width: 640px) and (max-height: 650px) {
  .header-invoke-stack {
    gap: 0.75rem;
    /* Compact padding for a short viewport — this overrides the base
       mobile query's padding entirely on a phone that's both narrow
       and short (both media queries match), independent of whatever
       that query's own bottom padding is currently tuned to for the
       "Find Out" button's scroll-button alignment (see above) — that
       alignment isn't attempted here, since fitting inside a single
       screen without the header growing takes priority. */
    padding: 1rem 0 1.25rem;
  }

  .header-invoke-title {
    font-size: clamp(1.4rem, 6.5vw, 4.25rem);
    margin-bottom: 0.5rem;
  }

  .header-invoke-sub {
    font-size: 1rem;
  }

  .header-invoke-lower .btn-light {
    margin-top: 0.75rem;
  }
}

/* The title's letters all converge by 1.5s after they start (see the
   reveal script in index.html's totalTarget); this fades in afterward,
   when the same script adds `.is-revealed` (currently 2777ms after
   the window `load` event — see `revealInvokeLower`'s `setTimeout` in
   index.html) and then takes this transition's 1.414s to finish.
   Driven by a JS-toggled `.is-revealed`
   class + transition rather than a `water-reveal` @keyframes animation
   with `forwards` fill (see `.header-invoke-lower .btn-light` below,
   and the reveal script in index.html) — a real iPhone 11 was
   observed losing the `forwards`-held end state on this element
   (reverting to invisible after the animation should have finished),
   which didn't reproduce in desktop DevTools' device emulation since
   that runs on the desktop browser engine, not actual iOS WebKit. A
   class-driven transition doesn't depend on fill-mode retention. */
.header-invoke-sub {
  margin: 0 auto;
  max-width: 36rem;
  /* Brighter than the site-wide `--muted` (0.65 alpha) — that read too
     dim against the header photo on a real phone. */
  color: rgba(247, 245, 240, 0.88);
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.75);
  opacity: 0;
  filter: blur(10px);
  transform: translateY(36px);
  transition: opacity 1.414s ease-out, filter 1.414s ease-out, transform 1.414s ease-out;
}

.header-invoke-sub.is-revealed,
.header-invoke-lower .btn-light.is-revealed {
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
}

/* Subtext read too small specifically around 960-1032px — bumped up
   from 900px so it doesn't touch 768px (not flagged as too small).
   A later attempt replaced this with a continuously-scaling `clamp()`
   to avoid the small→large→small jump at the 900/1032 boundaries, but
   that changed the desktop size outside this band too, which wasn't
   wanted — reverted back to this bounded flat value. */
@media (min-width: 900px) and (max-width: 1032px) {
  .header-invoke-sub {
    font-size: 1.5rem;
  }
}

/* The "Find Out" CTA surfaces alongside the subtext (both get
   `.is-revealed` at the same moment, from `revealInvokeLower` in
   index.html) rather than only the copy fading in above a static
   button. */
.header-invoke-lower .btn-light {
  opacity: 0;
  filter: blur(10px);
  transform: translateY(36px);
  transition: opacity 1.414s ease-out, filter 1.414s ease-out, transform 1.414s ease-out;
}

/* Scaled-down version of the sub/button reveal above, for individual
   letters — a full 10px blur/14px drop reads fine on a whole word or
   line, but is too heavy at glyph size, so both are pulled in for a
   subtler surfacing. */
@keyframes water-reveal-letter {
  from {
    opacity: 0;
    filter: blur(5px);
    transform: translateY(88px);
  }
  to {
    opacity: 1;
    filter: blur(0);
    transform: translateY(0);
  }
}

.btn-light {
  background: var(--paper);
  color: var(--ink);
}

.btn-light:hover {
  background: #2c114f;
  color: var(--paper);
}

.nav-wrap {
  max-width: 64rem;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5.55rem;
  flex-wrap: wrap;
}

.brand {
  font-family: "Cormorant SC", var(--font-serif);
  font-weight: 700;
  /* `vmin` is 1% of whichever viewport dimension is smaller — width on
     a phone in portrait, height on one in landscape — so this scales
     down on both without a separate breakpoint for each orientation.
     The `2.236rem` ceiling keeps today's desktop size unchanged (that
     value is only reached once the smaller dimension is ~597px, past
     every real phone — checked against Playwright's device catalog,
     which tracks current Apple/Samsung/Google specs, not assumed
     numbers: phones top out around 480px wide, so this is always
     scaling on a phone, never pinned to the ceiling). `1.1rem` is the
     floor so it stays legible even on the smallest real screens (an
     iPhone SE's 320px width still resolves to ~1.2rem, comfortably
     above it). */
  font-size: clamp(1.1rem, 6vmin, 2.236rem);
  letter-spacing: 0.0555em;
  text-decoration: none;
}

/* Hamburger toggle for the fullscreen mobile nav (see the nav overlay
   media query below) — hidden by default, only shown once `nav`
   switches from an inline link row to a fullscreen overlay that needs
   a control to open/close it. Drawn as the Aquarius glyph (♒ — two
   rows of zigzag "waves") at the user's request, in place of a plain
   two/three-bar hamburger — thematically apt for an astrology site,
   and still reads as a menu icon (two stacked horizontal marks) at a
   glance. This is the one exception to CLAUDE.md's "no `<svg>`
   iconography" note — that note exists to keep old yin-yang/botanical
   decoration from creeping back in unasked, not to block a shape that
   genuinely can't be drawn with CSS boxes and was explicitly
   requested. No visible circle/border — just the icon itself — though
   the button's own box stays the same 2.75rem square as the site's
   circular icon buttons (`.scroll-top`/`.scroll-down`) for a
   consistent tap target, it just isn't drawn.

   `position: absolute` (scoped to `header`, which now has its own
   `position: relative` — see above — as the containing block) with
   `right: 1.5rem` — the exact same value `.scroll-top` uses. An
   in-flow version (`margin-left: auto` inside `.nav-wrap`) was tried
   twice and measured as pixel-identical to this in headless testing
   both times, but the user still saw it as misaligned on their own
   device/browser both times, so the horizontal match is structural
   (both buttons literally share `right: 1.5rem`) rather than something
   that has to be re-derived from `.nav-wrap`'s box model matching
   `header`'s padding on every browser. A `position: fixed` version
   (identical `top`/`right`, but relative to the viewport instead of
   `header`) was also tried in between — that got the alignment right
   but made the toggle stay on screen as the page scrolled, which the
   user didn't want: they want it to scroll away with the logo, like
   every other `.nav-wrap` element. `position: absolute` against
   `header` is what gets both at once — it scrolls with `header` (and
   so with `.brand`) since `header` is what it's positioned against,
   while `right: 1.5rem` still resolves against `header`'s own right
   edge, which is the same as the viewport's right edge since `header`
   is a full-bleed block spanning the whole page width — so it's
   exactly the same coordinate `.scroll-top`'s `right: 1.5rem` (measured
   from the viewport directly) resolves to. `z-index` above `nav`'s own
   `z-index: 200` keeps the button clickable *on top of* the fullscreen
   overlay once it's open — without this, the overlay (which covers the
   whole viewport, including where the button sits) would sit visually
   and pointer-wise above its own toggle, making the one button meant
   to close the menu unreachable while the menu is open. */
.nav-toggle {
  display: none;
  position: absolute;
  /* Bottom-aligns the toggle with where `.brand`'s text visually ends
     — as if both were written on the same line of a page, rather than
     centered on each other. `.brand` has no `line-height` of its own,
     so it inherits `body`'s `1.7`, but the *visual* bottom of the
     glyphs sits above the full `font-size * 1.7` line-box bottom —
     `line-height` adds real leading space below the descenders.
     Measured in a headless browser (via a DOM `Range` around `.brand`'s
     text, which gives the glyphs' own rendered box rather than the
     line box) that the gap is close to `0.247 * font-size` for this
     font (Cormorant SC) at this weight, so `1.7 − 0.247 = 1.453` here
     targets the glyph bottom rather than the box bottom. `.brand`'s
     `font-size` is the exact same `clamp(1.1rem, 6vmin, 2.236rem)`
     expression `.brand` itself uses, so the two stay in sync as the
     viewport changes: `header`'s `1rem` top padding + that glyph-bottom
     offset, minus this button's own `2.75rem` height, is this button's
     `top`. Combined with `.nav-toggle-icon`'s `bottom: 0` (this
     button's box is taller than the visible glyph — 2.75rem vs. the
     icon's `clamp(1.55rem, 7vmin, 2rem)`, deliberately oversized for a comfortable tap
     target — so centering the icon inside it would float the glyph
     above the line this math targets), this lands the icon itself, not
     just the button's invisible box, on `.brand`'s line. This is a
     font-metric approximation, not an exact one — it'll drift slightly
     if `.brand`'s font or weight ever changes — but it matched within
     1px across every phone size tested (iPhone SE/13/13 Pro Max, a
     480px-wide Android profile). */
  top: calc(1rem + clamp(1.1rem, 6vmin, 2.236rem) * 1.453 - 2.75rem);
  /* Not a plain `1.5rem` match with `.scroll-top`'s own `right` — that
     matches the two *button boxes'* right edges, but the boxes are
     different widths (this one's `2.75rem` vs. `.scroll-top`'s
     `2.5rem`, both with their icon centered inside), so the icons
     themselves (what's actually visible) end up a few px apart even
     with identical box edges. This solves for the toggle's `right`
     that puts its icon at the exact same horizontal center as
     `.scroll-top`'s icon instead: both buttons center their icon
     within their own box, so icon-center = `button.right −
     buttonWidth / 2`; setting that equal for both and solving for this
     button's `right` gives `1.5rem + (2.5rem − 2.75rem) / 2`. Checked
     `.scroll-top`'s actual width in the CSS rather than assumed — it
     reads `2.5rem`, not the `3rem` this math used on a first pass,
     which is why that first pass was still ~4px off. Even that formula
     still left the icon a hair off-center against `.scroll-top`'s
     visually, so the user hand-tuned it by eye (editing this value
     directly, saving, and reloading the page) and landed on the
     formula minus an extra `3px`. */
  right: calc(1.5rem + (2.5rem - 2.75rem) / 2 - 3px);
  z-index: 201;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  background: transparent;
  border: none;
  cursor: pointer;
  flex-shrink: 0;
}

/* Two crossfading icons stacked via `position: absolute` inside the
   button — the same crossfade technique `.scroll-top`/`.scroll-down`
   already use for their default/hover icon swap, reused here for a
   menu/close swap instead. `fill: none` + `stroke` (rather than
   filled shapes) keeps the glyph as thin open lines, matching the
   site's other line-based marks (`.divider`, the nav pills' outline
   style) rather than a solid icon. */
/* Bottom-aligned (`bottom: 0`), not centered, within the button's own
   box — `.nav-toggle`'s `top` (above) puts that box's *bottom* edge on
   `.brand`'s own bottom edge, but the box itself is taller than the
   glyph (2.75rem vs. this icon's `clamp(1.55rem, 7vmin, 2rem)`,
   deliberately oversized for a comfortable tap target). Centering the icon inside that box would
   float the glyph well above the line the box's bottom is aligned to;
   flush-bottom is what actually lands the visible glyph on the same
   line as the logo text, "as if both were written on the same line
   of a page" per the user's request. */
.nav-toggle-icon {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  /* Was a flat `1.65rem`, but `.brand`'s own `font-size` scales up with
     `vmin` as the viewport widens (see `.brand` above) while this
     stayed fixed — so past roughly 400-450px wide the logo kept
     growing and the static icon started reading small next to it.
     `vmin` here (not `vw`) for the same reason `.brand` uses it: scales
     on both narrow *and* short viewports. Floors around today's old
     1.65rem-ish size on the smallest phones and grows to 2rem by
     ~480-500px wide, comfortably inside the `2.75rem` button box. */
  width: clamp(1.55rem, 7vmin, 2rem);
  height: clamp(1.55rem, 7vmin, 2rem);
  fill: none;
  stroke: var(--paper);
  stroke-width: 1.618;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: opacity 0.25s ease;
}

.nav-toggle-icon-close {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle-icon-menu {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle-icon-close {
  opacity: 1;
}

nav ul {
  list-style: none;
  display: flex;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
}

/* Nav sits on the header photo, which varies too much in hue/value for
   any single accent color to read reliably against it — plain white
   text is what actually pops consistently regardless of what's behind
   it. Every link (not just `.active`) carries the same pill padding/
   border-radius/font-weight up front — only `background` differs on
   `.active` — so which page is current never changes any link's box
   size and the row never reflows between pages. Pill padding is
   tighter vertically (0.1rem, was 0.3rem) than most of the site's
   other pill/button shapes (`.btn` runs 0.7rem) at the user's request
   — nav sits on a photo rather than the flat dark field the rest of
   the site's pills/buttons live on, so a slimmer mark reads as a quiet
   label rather than competing with the image behind it. */
nav a {
  display: inline-block;
  text-decoration: none;
  /* "Nunito Sans" is loaded via the Google Fonts link in each page's
     <head>, alongside the other font families — weights 300/400/500/
     600/700/800 are all loaded so different weights can be tried here
     (currently 300) without editing the font link each time. */
  font-family: "Nunito Sans", var(--font-sans);
  font-style: normal;
  font-size: 1.236rem;
  font-weight: 300;
  letter-spacing: 0.14em;
  color: var(--paper);
  background: transparent;
  border-radius: 999px;
  padding: 0.1rem 0.7rem;
}

nav a.active {
  background: var(--ink);
}

/* Below ~640px there isn't room for the brand + all five nav links on
   one line. Without this, `nav ul` never wraps as a *row* — it just
   shrinks each `<li>` until the link text itself wraps mid-phrase
   (e.g. "Book a Reading" splitting into "Book A" / "Reading"). Stack
   brand above nav, centered, and let nav items wrap as whole units. */
@media (max-width: 960px) {
  .nav-wrap {
    flex-direction: column;
    align-items: center;
    gap: 1rem;
  }

  nav ul {
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.6rem 1.25rem;
  }

  nav a {
    white-space: nowrap;
  }
}

/* Below 640px (real phones only — checked against Playwright's device
   catalog: they top out around 480px wide, well clear of this, while
   tablets start around 640–744px and stay above it), the stacked/
   wrapped nav above still isn't great: five pill links wrapped onto 2–3
   short lines under the brand eats a lot of vertical space before any
   real content starts. Replaced with a hamburger toggle (`.nav-toggle`,
   styled above) that opens `nav` as a fullscreen overlay instead —
   `position: fixed; inset: 0` so it sits above everything regardless of
   which header (the homepage's full-viewport photo, or the shorter
   `height: auto` one on the other four pages) it's toggled from, filled with
   `var(--ink)` to match the dark field the rest of the site opens and
   closes into (see the black-dominant design note in CLAUDE.md) rather
   than introducing a new surface color. `opacity`/`visibility` (not
   `display: none`) so the open/close can transition instead of
   snapping; `.nav-wrap` reverts to a plain row here since it now only
   ever holds two items (brand, toggle) that always fit on one line —
   the wrapping/stacking above was only ever needed for the full link
   row. `js/nav-menu.js` wires the toggle: adds/removes `.is-open` on
   `nav`, keeps `aria-expanded` in sync (which also drives the
   hamburger→X animation above), closes on Escape or a link click, and
   closes automatically if the viewport is resized past this breakpoint
   while open. */
@media (max-width: 640px) {
  /* `.nav-toggle` is `position: absolute` (see above) and so not part
     of this row's flex flow, so `.nav-wrap` only ever holds `.brand`
     here — this just keeps it a plain left-aligned row rather than
     the wider/centered treatment `.nav-wrap`'s other rules use. */
  .nav-wrap {
    flex-direction: row;
    justify-content: flex-start;
    gap: 0;
  }

  .nav-toggle {
    display: flex;
  }

  nav {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--ink);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.25s ease, visibility 0.25s ease;
  }

  nav.is-open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
  }

  /* Added by js/nav-menu.js right before closing the menu on a nav
     link click, so the overlay disappears instantly instead of
     playing its usual 0.25s fade — closing on a link click means the
     page is navigating away anyway, and the fade would otherwise
     briefly reveal the old page underneath before the new one loads. */
  nav.no-transition {
    transition: none;
  }

  nav ul {
    flex-direction: column;
    align-items: center;
    gap: 2rem;
  }

  nav a {
    font-size: 1.35rem;
    padding: 0.5rem 1.5rem;
  }

  /* The desktop active-link style is a `var(--ink)` pill — invisible
     once `nav`'s own background is also `var(--ink)` (the fullscreen
     overlay). `--accent` is otherwise reserved for interactive states
     (form focus, the FAQ `summary::after` +/− icon — see CLAUDE.md);
     marking the current page is exactly that kind of state, so it fits
     rather than introducing a new color. */
  nav a.active {
    background: transparent;
    color: var(--accent);
  }
}

/* Locks background scroll while the fullscreen mobile nav is open —
   toggled by js/nav-menu.js alongside `nav.is-open`. Unscoped by width
   since `.nav-toggle` (the only way to add this class) is only ever
   visible below 640px anyway. */
body.menu-open {
  overflow: hidden;
}

/* Open bands — full-bleed, unbounded, sit directly on the dark page */

.band {
  padding: 7rem 1.5rem;
}

.band-inner {
  max-width: var(--max-width);
  margin: 0 auto;
}

.lede {
  color: var(--muted);
  max-width: 34rem;
}

/* Hero */

/* Only the top padding is trimmed here — the header banner is a
   full-viewport photo, so the shared `.band` top padding (7rem) on top
   of that already-large image read as too much space before the intro
   text starts. Bottom padding is left at the shared 7rem so the
   hero→cta-panel gap stays matched to the cta-panel→quote gap. */
.hero {
  text-align: center;
  padding-top: 3rem;
}

.hero h1 {
  font-size: 2rem;
  font-style: italic;
  max-width: 26rem;
  margin: 0 auto 1rem;
}

/* Beside the intro paragraph rather than the heading — sized to
   roughly match that paragraph's own rendered height so image and
   text read as one unit, without stretching the (already circular,
   baked-in-crop) portrait into an oval. Sits outside `.band-inner`
   (a sibling, not a child) with its own wider max-width, so this one
   row can be wider than the shared `--max-width` column without
   affecting `.band-inner`'s width anywhere else on the site. */
.hero-intro {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  max-width: 56rem;
  margin: 2rem auto 0;
  text-align: left;
}

.hero-intro-text {
  flex: 1;
}

.hero-intro p {
  margin: 0 0 1em;
}

.hero-intro p:last-child {
  margin-bottom: 0;
}

.hero-portrait {
  width: clamp(10rem, 60vw, 22rem);
  height: clamp(10rem, 60vw, 22rem);
  flex-shrink: 0;
  border-radius: 50%;
  border: 1.5px solid #2c114f;
  object-fit: cover;
}

/* A row this wide (portrait + paragraph) doesn't fit narrow phone
   widths without squeezing the text into a sliver — stack instead.
   `text-align: left` (was `center`, matching the hero's original
   centered layout, changed at the user's request) only affects the
   paragraph text and "Read More" button below — the portrait's own
   horizontal centering comes from `align-items: center` on this flex
   column, not from text-align, so it stays centered either way. */
@media (max-width: 640px) {
  .hero-intro {
    flex-direction: column;
    text-align: left;
  }

  .hero-intro-text {
    flex: none;
  }

  /* Centers just the "Read More" button on mobile — the paragraph
     text above stays left-aligned (see `.hero-intro`'s own comment).
     `.btn` is `display: inline-block`, so `text-align: center` on the
     parent would work too, but that would also re-center the
     paragraphs since they inherit the same property; `margin: auto`
     on the button itself is scoped to just this one element. */
  .hero-intro-text .btn-light {
    display: block;
    width: fit-content;
    margin-left: auto;
    margin-right: auto;
  }
}

/* `.hero p` and `.hero-intro p` both match the same two paragraphs
   (`.hero-intro` sits inside `.band.hero`) with equal specificity —
   this used to also declare `margin: 0 auto`, which silently won the
   cascade tie (later in the file) over `.hero-intro p`'s intended
   `margin: 0 0 1em`, collapsing the gap between the two paragraphs to
   nothing. Removed since it was dead weight anyway: the paragraph's
   own rendered width never approached this 34rem max-width, so the
   auto centering it implied had no visible effect either way — only
   `color` was actually doing anything. */
.hero p {
  max-width: 34rem;
  color: var(--muted);
}

/* Page intro — the dark opening beat on inner pages, mirrors the hero */

.page-intro {
  text-align: center;
  padding-top: 3rem;
  padding-bottom: 1.5rem;
}

.page-intro h1 {
  font-size: 1.8rem;
  font-style: italic;
}

/* `margin-top` matches the About page's current portrait-to-text
   gap, which comes from `.page-intro`'s own 1.5rem bottom padding
   there (portrait's own margin and main's own top padding are both
   zeroed on About) — Book a Reading needs it here instead since its
   lede text sits in the same band as the portrait, not a separate
   `main` below it. */
.page-intro .lede {
  margin: 1.5rem auto 0;
}

/* CTA panel — a slim panel whose only job is a line of copy + a button */

/* Homepage-only variant of the shared `main` panel: shorter padding
   and no bottom margin so it reads as a slim CTA strip rather than a
   full panel. The base `main` rule is itself now transparent with
   white text/border (the dark page shows through), so the
   `background`/`color`/`border`/`--muted`/`--rule` declarations here
   just restate that for override-safety rather than changing
   anything. */
main.cta-panel {
  background: transparent;
  color: var(--paper);
  border: 1px solid var(--paper);
  padding: 1.75rem 2.25rem;
  margin-bottom: 0;
  --muted: rgba(247, 245, 240, 0.65);
  --rule: rgba(247, 245, 240, 0.2);
}

.cta-panel {
  text-align: center;
}

.cta-panel .lede {
  margin: 0 auto 0.5rem;
}

.booking-cta {
  margin-top: 2.5rem;
  padding-top: 2.5rem;
  font-weight: 700;
  color: var(--paper);
  border-top: 1px solid var(--rule);
}

.booking-cta .lede {
  color: var(--paper);
}

/* Buttons */

.btn {
  display: inline-block;
  margin-top: 1.5rem;
  padding: 0.7rem 1.75rem;
  border: 1px solid currentColor;
  border-radius: 999px;
  /* Every other `.btn` so far has been an `<a>`, which inherits the
     page font automatically — `<button>` doesn't always (browsers'
     default UA stylesheets often give it a system font instead), so
     this is explicit now that "Send Message" (a real `<button>`) uses
     `.btn` too. */
  font-family: inherit;
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.btn:hover {
  background: #22133a;
  border-color: #22133a;
  color: #fff;
}

/* Panel — the bounded, resolved content: a lit room within the dark field */

main {
  width: calc(100% - 2.5rem);
  max-width: var(--max-width);
  margin: 0 auto 5rem;
  padding: 3rem 2.25rem;
  background: transparent;
  color: var(--paper);
  border: 1px solid var(--paper);
  border-radius: 18px;
  --muted: rgba(247, 245, 240, 0.65);
  --rule: rgba(247, 245, 240, 0.2);
}

/* On About/Book a Reading/FAQ/Contact, main is followed directly by
   <footer> — no .band section in between, unlike the homepage where
   main sits between .hero and .quote. main's shared 5rem bottom
   margin was sized for that band-to-band gap, so stacked directly
   against footer's own top padding it read as an oversized gap.
   main.cta-panel (homepage) already zeroes its own margin-bottom for
   the same reason — this is that same fix for the other four pages,
   controlled by the --panel-footer-gap variable in :root. */
main:not(.cta-panel) {
  margin-bottom: var(--panel-footer-gap);
}

/* Item list (offerings / services) */

.item {
  padding: 1.75rem 0;
  border-top: 1px solid var(--rule);
}

.item:first-child {
  padding-top: 0;
  border-top: none;
}

.item:last-child {
  padding-bottom: 0;
}

.item h3 {
  font-size: 1.05rem;
  margin-bottom: 0.35rem;
}

.item p {
  color: var(--muted);
  margin: 0;
}

/* Quote band */

/* `.quote` (used on the homepage and About) is immediately followed by
   the slim `<footer>` (two tagline lines + copyright) rather than
   another `.band`, so it doesn't need the full shared `.band` bottom
   padding — that combined with `.footer-slim`'s own padding read as
   an oversized gap before the footer. */
.quote {
  text-align: center;
  padding-bottom: 0.1rem;
}

/* About-only: `.quote` here follows `main` directly (no `.hero`/`.cta-panel`
   band above it), so the shared `.band` top padding plus main's own bottom
   margin read as an oversized gap before the Goethe quote. */
main:not(.cta-panel) + .quote {
  padding-top: 0rem;
}

/* Homepage-only: shrinks the gap between the "Book a Reading" CTA
   panel and the quote below it, at the user's request. `main.cta-panel`
   has its own `margin-bottom: 0` (see above), so this gap was just the
   shared `.band` top padding (7rem) plus the panel's own 1.75rem
   bottom padding — reduced here rather than touching the shared `.band`
   value, which also sets the hero→cta-panel gap and every other page's
   intro spacing. */
main.cta-panel + .quote {
  padding-top: 2rem;
}

.quote blockquote {
  margin: 0;
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 1.2rem;
  line-height: 1.8;
}

.quote-attr {
  margin: 1.25rem 0 0;
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Bottom banner — a daytime lake closing the page, set against the
   night-sky header at the top: same reflection motif, opposite hour.
   Uses the same `background-attachment: fixed` + `cover` technique as
   `header` so the image is a window into a viewport-sized backdrop
   rather than being sized to this element's own box. With
   `background-position: bottom`, the image's bottom edge stays pinned
   to the viewport's bottom edge, so the banner shows the bottom of
   the photo first as it scrolls up into view, revealing more of it
   (upward through the image) the further you scroll — mirroring the
   header's reveal effect. A JS-driven pan (scaling the image to the
   element's own box instead of the viewport) was tried here to get a
   more dramatic reveal on short/mobile viewports, but it read as an
   odd zoomed crop compared to header's plain `cover` look — reverted
   in favor of matching header exactly. How much actually gets
   revealed depends on how far this box travels across the screen
   while scrolling, i.e. its own on-screen height — `header.header-full`
   gets a strong reveal on every viewport because it's pinned to a full
   `100vh` regardless of width, but this box's height came from
   `aspect-ratio` alone, which on a narrow phone (full-bleed width,
   landscape-ratio image) works out to a short strip — too little
   travel for much to reveal. At `1440px` and below (see the media
   query below), a full-viewport-height box (`100lvh`, with `100vh`
   as fallback) takes over from `aspect-ratio`, matching
   `header.header-full` rather than just improving on the short
   strip: since this element is the very last thing on the page, its
   bottom is pinned flush with the viewport bottom once the page is
   scrolled all the way down, so its top can only ever climb to
   `viewport height − its own height` above the viewport top —
   anything less than a full-viewport box leaves that much of the
   image's top permanently out of reach, no matter how far down the
   page you scroll. `background-size: cover` just crops the image
   differently to fit, same as it already does at every other size.
   At 640px and below, this rule's own `background-attachment: fixed`
   is replaced by a `.bottom-banner-bg` layer (see below), since
   `fixed` is unreliable on iOS Safari. */

.bottom-banner-wrap {
  position: relative;
}

.bottom-banner {
  width: 100%;
  aspect-ratio: 3527 / 1984;
  background-image: url("../Bottom-banner.jpg");
  background-size: cover;
  background-position: center bottom;
  background-attachment: fixed;
}

/* Raised from 640px at the user's request — the `aspect-ratio` box's
   height only ever depends on width (roughly `width * 9/16`), but a
   lot of real windows between 640-1440px wide are taller relative to
   their width than that (a portrait-ish laptop window, a narrower
   browser resized tall), so the box came out shorter than the actual
   viewport and the image's top could never scroll into reach — the
   same underlying issue the sub-640px case already had. Reapplying
   that same full-viewport-height fix over this wider range
   guarantees the full image is reachable regardless of the window's
   own aspect ratio; above 1440px, wide desktop windows are reliably
   wider-than-tall enough that the `aspect-ratio` box holds up on its
   own, so the gradual scroll-reveal effect (see the comment above)
   still plays out there rather than showing the full image outright,
   as it effectively does under this rule. */
@media (max-width: 1440px) {
  .bottom-banner {
    aspect-ratio: auto;
    height: 100vh;
    /* `lvh` (was `dvh`) for the same reason as `header.header-full`'s
       own min-height — see its comment. `dvh` continuously
       recalculating this box's height as iOS Safari's browser chrome
       collapses/expands mid-scroll was reflowing the page underneath
       it, reported as a stutter; `lvh` is static, so there's nothing
       to reflow, while still guaranteeing the box is always tall
       enough to keep the full image reachable in any chrome state. */
    height: 100lvh;
  }
}

/* `background-attachment: fixed` is unreliable on real iOS Safari —
   was observed rendering visibly over-zoomed/cropped on an iPhone 11
   for both header variants. A real `position: fixed` layer +
   JS-driven `clip-path` (`.header-full-bg` below, and `js/fixed-bg-
   clip.js`) fixes that and additionally recreates the desktop "pinned
   while scrolling" reveal effect — for `header.header-full` only. The
   shorter nav-only header on the other four pages doesn't get this:
   it's short enough (nav content height only) that a scroll-reveal
   motion isn't meaningful there, so it just needs the zoom bug fixed,
   not the full pinned-photo treatment.

   The fixed-layer technique was tried on both header variants once
   before and reverted — choppy scroll performance and the header
   intermittently vanishing until a forced repaint — back when the
   fixed layer was sized with `inset: 0` (implicitly tracking the
   *current* viewport) and the JS assumed that height always equalled
   `window.innerHeight`. Both assumptions broke during iOS Safari's
   browser-chrome collapse/expand transitions. `.bottom-banner-bg` hit
   the same class of bug afterwards and was fixed by sizing it
   statically with `lvh` instead, and having the JS read the layer's
   own actual rendered height rather than assuming it matches
   `window.innerHeight` (see its own comment) — `header.header-full`
   now uses that same, already-proven approach. */
@media (max-width: 640px) {
  header.header-full {
    background-image: none;
  }

  .header-full-bg {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    height: 100lvh;
    background-size: cover;
    background-position: center;
    background-image: url("../Banner-top.webp");
    clip-path: inset(100% 0 0 0);
  }

  /* Positioned elements (including position: fixed) always paint
     above plain unpositioned content per CSS2.1's stacking order
     regardless of DOM order — without this, the fixed photo layer
     would cover the header's nav/title text instead of sitting behind
     it. `.brand` specifically (not the whole `.nav-wrap`): giving
     `.nav-wrap` itself `position: relative` previously broke
     `.nav-toggle`'s absolute positioning (it became the new nearest
     positioned ancestor instead of `header`) and trapped the mobile
     nav overlay's `z-index: 200` inside `.nav-wrap`'s own new stacking
     context instead of letting it escape to out-rank `.scroll-top`/
     `.scroll-down`. `.nav-toggle` and `nav` don't need this
     themselves — both are already positioned elements with their own
     explicit stacking. */
  header.header-full .brand,
  header.header-full .header-invoke-stack {
    position: relative;
    z-index: 1;
  }
}

/* The shorter nav-only header shared by About/Book a Reading/FAQ/
   Contact — no pinned-photo treatment (see the big comment above),
   just a plain local background sized to match the homepage header's
   crop. Its own box (nav content height only) is much shorter than
   `header.header-full`'s, so plain `cover` sizing the image against
   that tiny box scaled/cropped it far more tightly than the homepage
   — `background-size` explicit `auto 100lvh` (`100vh` fallback first)
   forces the same viewport-height-based scale `header.header-full`'s
   own `cover` lands on instead. `background-position: top` (not the
   base rule's plain `center`, which centers vertically too) matters
   because this short box has real vertical slack to center within,
   where `header.header-full`'s own box doesn't (it's exactly as tall
   as the scaled image) — `center` was showing the image's own
   vertical *middle* instead of matching what `header.header-full`
   shows at *its* top edge. */
@media (max-width: 640px) {
  header:not(.header-full) {
    background-attachment: scroll;
    background-size: auto 100vh;
    background-size: auto 100lvh;
    background-position: top center;
  }
}

/* .bottom-banner-bg — the real position:fixed layer + JS-clip-path
   technique (see the big comment above and js/fixed-bg-clip.js) that
   .bottom-banner uses on mobile, same as `header.header-full` (but
   not the shorter nav-only header on the other four pages). Its own
   background-image is switched off so the two don't double up. */
@media (max-width: 640px) {
  .bottom-banner {
    background-image: none;
  }

  .bottom-banner-bg {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    /* Sized to the *largest* possible viewport (assumes the browser
       chrome is fully collapsed) rather than `inset: 0`'s implicit
       sizing to the *current* one — iOS Safari's address bar/toolbar
       collapsing and expanding as the page scrolls was making this
       (and the image inside it) visibly resize mid-scroll, reported
       as a stutter. `lvh` never changes value while scrolling, so
       there's nothing left to animate. `100vh` first as a fallback
       for browsers without `lvh` support. js/fixed-bg-clip.js reads
       this element's own actual rendered height for its clip-path
       math rather than assuming it equals `window.innerHeight`, since
       that's no longer true once the browser chrome is showing (this
       box is now taller than the visible area in that state). */
    height: 100vh;
    height: 100lvh;
    background-size: cover;
    background-position: center bottom;
    background-image: url("../Bottom-banner.jpg");
    clip-path: inset(100% 0 0 0);
  }
}

/* The footer message overlaid on the bottom banner, centered at its
   base — reuses `.footer-message`'s serif/italic styling but repoints
   it from a static block to an absolutely positioned caption; the
   compound selector's higher specificity guarantees these overrides
   win regardless of stylesheet order (`max-width` is the one property
   shared with the base rule at the same value, kept implicit rather
   than repeated here). Centering here is `left: 50%` +
   `translateX(-50%)`, so the reveal below is done with a plain
   transition rather than a `translateY`-based @keyframes animation
   like `.header-invoke-sub`'s — that would overwrite the whole
   `transform` property and wipe out the `translateX(-50%)` centering
   once the reveal ended. `js/scroll-reveal.js` adds
   `.is-visible` when this element scrolls into view (rather than on a
   page-load timer), since it sits at the very bottom of the page and a
   load-timed animation would always finish playing long before anyone
   scrolls down to see it. */
.footer-message.bottom-banner-text {
  position: absolute;
  left: 50%;
  bottom: 2.5rem;
  transform: translateX(-50%) translateY(14px);
  width: 100%;
  margin: 0;
  padding: 0 1.5rem;
  text-align: center;
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.6);
  opacity: 0;
  filter: blur(10px);
  transition: opacity 1.4s ease-out, filter 1.4s ease-out, transform 1.4s ease-out;
  /* The base `.footer-message` rule's `1.77rem` is a flat size at every
     width, which reads oversized on narrow phones relative to this
     caption's own (fairly narrow, unconstrained by `.band-inner`) box.
     Scales with `vw` instead, only for this fading homepage instance —
     the static `.footer-message` on the other four pages' `<footer>`
     is untouched. Floor holds until ~510px, then grows smoothly,
     reaching the original 1.77rem right around 752px and staying flat
     there — so desktop's appearance is unchanged, only phones scale
     down proportionally. */
  font-size: clamp(1.1rem, 4vw, 1.77rem);
}

.footer-message.bottom-banner-text.is-visible {
  opacity: 1;
  filter: blur(0);
  transform: translateX(-50%) translateY(0);
}

/* About */

.portrait {
  display: block;
  width: clamp(10rem, 60vw, 22rem);
  height: clamp(10rem, 60vw, 22rem);
  border-radius: 50%;
  border: 1.5px solid #2c114f;
  margin: 0 auto 0rem;
  object-fit: cover;
}

/* A labeled sub-section within `main`'s bio text — the "Influences"
   paragraph is set apart from the three bio paragraphs above it by a
   rule + a small uppercase label (matching `.faq-group h2`'s label
   style) rather than a bordered/boxed container, staying consistent
   with the site's rule-based dividers instead of reintroducing a
   card look. */
.influences {
  margin-top: 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--rule);
}

.influences p {
  margin: 0;
}

/* About-only: swaps which body text reads grey vs. white within the
   panel, at the user's request. `.section-label` ("Influences") used
   to be the grey one (`var(--muted)`) against plain white body
   paragraphs — now the reverse. `main.panel-text-swap p:not(.section-
   label)` catches every paragraph in `main` regardless of nesting
   depth (the three intro paragraphs, direct children of `main`, plus
   the one inside `.influences`, nested a level deeper) without
   needing a class on each one individually. */
main.panel-text-swap p:not(.section-label) {
  color: var(--muted);
}

/* About-only, at the user's request: drops the shared outlined-panel
   look (see the base `main` rule) so the bio text sits directly on
   the dark page instead of inside a bordered box. `padding-top` is
   also cut from the shared `3rem` — with the border gone, the
   combined gap to the portrait above (`.page-intro`'s own `1.5rem`
   bottom padding, plus this) read as too much empty space, more
   noticeable now that there's no box edge to anchor it visually. */
main.panel-text-swap {
  border: none;
  padding-top: 0rem;
}

.section-label {
  font-family: var(--font-sans);
  font-size: 0.75rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--paper);
  margin-bottom: 0.5rem;
}

/* FAQ */

.faq-group {
  margin-bottom: 2.5rem;
}

.faq-group:last-child {
  margin-bottom: 0;
}

/* Swapped grey/white at the user's request: the category label used
   to be the grey one against white questions/answers — now the
   category label and answers are white, and the question is grey. */
.faq-group h2 {
  font-family: var(--font-sans);
  font-size: 0.75rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--paper);
  margin-bottom: 0.5rem;
}

details {
  border-top: 1px solid var(--rule);
  padding: 1.1rem 0;
}

.faq-group details:first-of-type {
  border-top: none;
}

.faq-group details:last-of-type {
  padding-bottom: 0;
}

summary {
  cursor: pointer;
  font-size: 1.05rem;
  list-style: none;
  color: var(--muted);
}

summary::-webkit-details-marker {
  display: none;
}

summary::after {
  content: "+";
  float: right;
  color: var(--accent);
}

details[open] summary::after {
  content: "\2212";
}

details p {
  margin: 0.85rem 0 0;
  color: var(--paper);
}

/* Form */

form {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.name-row {
  display: flex;
  gap: 1.5rem;
}

.name-row > div {
  flex: 1;
}

/* Swapped grey/white at the user's request: labels used to be the
   grey ones against white input text — now the reverse. */
label {
  display: block;
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper);
  margin-bottom: 0.5rem;
}

input,
textarea {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--rule);
  color: var(--muted);
  padding: 0.5rem 0;
  font-family: inherit;
  font-size: 1rem;
}

textarea {
  border: 1px solid var(--rule);
  padding: 0.75rem;
  resize: vertical;
}

input:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
}

/* Visual styling now comes from `.btn.btn-light` (the "Send Message"
   button carries both classes in the markup, matching the homepage's
   buttons) — this rule only keeps the layout/behavior bits `.btn`
   doesn't cover: without `align-self: center`, the button would
   stretch to the form's full width (its flex column's default
   `align-items: stretch`) — centered rather than flex-start now that
   the form spans the full panel width, at the user's request;
   `cursor: pointer` in case a future button here isn't a native
   `<button>` (which gets it for free from the browser anyway).
   `margin-top: 0` cancels out `.btn`'s own `1.5rem` top margin —
   `form`'s `gap: 1.5rem` already spaces every child (including this
   button) evenly, so `.btn`'s margin would otherwise stack on top of
   that gap and double the space above the button. */
button {
  align-self: center;
  cursor: pointer;
  margin-top: 0;
}

/* Footer — closes back into the open field */

footer {
  text-align: center;
  padding: 3rem 1.5rem 4rem;
}

/* Used on all five pages: `.footer-message` lives on the bottom banner
   (see `.bottom-banner-wrap`) rather than in `<footer>`, so this footer
   only holds the short tagline lines + copyright — the default footer
   padding, sized for a taller block, left it looking like an oversized
   gap around that shorter content. */
.footer-slim {
  padding: 1rem 1.5rem;
}

footer p {
  margin: 0;
  font-size: 0.77rem;
  letter-spacing: 0.08em;
  color: var(--muted);
}

/* Scroll-to-top — fixed on every page, bottom-right. Transparent
   background; Fire-symbol.png's black outline is inverted to white
   via filter so it reads against the dark page. */
.scroll-top,
.scroll-down {
  position: fixed;
  bottom: 1.5rem;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  background: transparent;
  border: 1px solid var(--paper);
  border-radius: 50%;
  transition: background 0.2s ease;
}

.scroll-top {
  right: 1.5rem;
}

.scroll-down {
  left: 1.5rem;
}

.scroll-top:hover,
.scroll-down:hover {
  background: #2c114f;
}

/* Two stacked icons per button (default + hover) rather than one, so
   the swap (fire→air, water→earth) is a pure CSS opacity crossfade —
   no JS needed to change the `src`. */
.scroll-top img,
.scroll-down img {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.5rem;
  height: 1.5rem;
  transform: translate(-50%, -50%);
  filter: invert(1);
  transition: opacity 0.2s ease;
}

.scroll-top .icon-hover,
.scroll-down .icon-hover {
  opacity: 0;
}

.scroll-top:hover .icon-default,
.scroll-down:hover .icon-default {
  opacity: 0;
}

.scroll-top:hover .icon-hover,
.scroll-down:hover .icon-hover {
  opacity: 1;
}

.footer-message {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 1.77rem;
  letter-spacing: normal;
  line-height: 1.6;
  max-width: 46rem;
  margin: 0 auto 1.5rem;
}

/* Taijitsu (☯) in the footer tagline — clickable, opens .tao-modal
   with the Tao Te Ching's first chapter (trans. Stephen Mitchell).
   Plain reset button so the glyph keeps sitting inline in the
   tagline's own text/font, just made interactive. */
.taijitsu-btn {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-size: 1.15em;
  color: inherit;
  line-height: 1;
  cursor: pointer;
  display: inline-block;
  transition: transform 0.4s ease, opacity 0.2s ease;
}

.taijitsu-btn:hover,
.taijitsu-btn:focus-visible {
  transform: rotate(180deg);
  opacity: 0.8;
}

/* Locks page scroll while the Tao Te Ching pop-up is open, same
   technique as body.menu-open for the mobile nav overlay — kept as
   its own class/rule rather than reusing menu-open so the two stay
   independent (the modal can open on desktop, where menu-open never
   applies). */
body.tao-modal-open {
  overflow: hidden;
}

/* Fixed full-viewport wrapper; [hidden] keeps it out of layout/tab
   order entirely until js/tao-modal.js clears the attribute. No
   opacity/transform transition — the pop-up appears instantly rather
   than fading/sliding in, at the user's request. */
.tao-modal {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

.tao-modal[hidden] {
  display: none;
}

.tao-modal-overlay {
  position: absolute;
  inset: 0;
  background: rgba(11, 6, 19, 0.85);
}

.tao-modal-panel {
  position: relative;
  width: fit-content;
  min-width: 20rem;
  max-width: min(34rem, calc(100% - 3rem));
  max-height: 85vh;
  overflow-y: auto;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--paper);
  border-radius: 18px;
  padding: 3rem 2.25rem;
  text-align: left;
}

.tao-modal-close {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  width: 2.25rem;
  height: 2.25rem;
  background: none;
  border: none;
  color: var(--paper);
  font-family: var(--font-serif);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.tao-modal-eyebrow {
  margin: 0 0 1.75rem;
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--paper);
}

.tao-modal-text p {
  margin: 0 0 1.5rem;
  font-family: var(--font-serif);
  font-size: 1.15rem;
  line-height: 1.8;
  color: var(--muted);
}

.tao-modal-text p:last-child {
  margin-bottom: 0;
}
