/*
  marselion — shared stylesheet

  Typography below is measured directly off the live site's computed styles (not
  estimated) at two reference viewports — 375px (mobile) and 1440px (desktop) — since
  the real site's display headings use fluid/viewport-scaled sizing, not fixed px. Each
  --fs-display-* token is a clamp() built from those two real data points. Body/UI text
  (Poppins, 16px, weight 300, letter-spacing 0.07em, line-height 1.8) is NOT fluid on the
  live site — confirmed identical at both breakpoints — so it's just a fixed value here.

  Two real font families are in play on the live site:
  - Poppins (SIL Open Font License, freely self-hostable — loaded from Google Fonts in
    every page's <head>) for nav, body copy, buttons, poem lines.
  - Adobe "Acumin Pro" (Typekit/Adobe Fonts, license-gated — can't self-host) for large
    display headings only: logo, hero name, "Experience"/chapter titles, "Shop", essay
    eyebrows/titles, product titles/prices, store card titles. Falls back to Jost (Google
    Fonts, similar light-geometric proportions) until the client's Adobe Fonts kit
    <script> embed is added to every page's <head> — no other change needed once that
    happens, since everything already references 'Acumin Pro' first.
*/

:root {
  --bg: #0b0b0c;
  --text: #f2f0ec;
  --text-muted: #bdc4ce;
  --text-dim: #7c8189;
  --accent: #fafafa;
  /* No darkening scrim and no text-shadow — measured the live site's actual overlays
     directly (getComputedStyle on the element sitting over each hero image) and found
     ~0-15% black at most, often none at all; text-shadow measured as literally "none" on
     every heading checked. The live site just accepts whatever contrast the photo gives
     it — don't reintroduce either as a "legibility fix" without re-checking the source. */
  --overlay: rgba(0, 0, 0, 0);
  --text-shadow: none;
  --sans-body: 'Poppins', 'Century Gothic', 'Futura', sans-serif;
  --sans-display: 'Acumin Pro', 'Figtree', 'Century Gothic', 'Futura', sans-serif; /* Figtree: free stand-in for Acumin Pro (client has no Adobe Fonts subscription) — chosen by measuring against live's Acumin: widths within ~1.6%, cap-height/x-height within 1% */
  --sans: var(--sans-body); /* legacy alias */

  /* fluid display-heading tokens, clamp(mobile-375px, linear-interpolation, desktop-1440px) */
  --fs-display-sm: clamp(21.85px, calc(20.4px + 0.386vw), 25.96px);   /* hero name, store card title, product price */
  --fs-display-md: clamp(33.54px, calc(29.2px + 1.158vw), 45.87px);   /* essay eyebrow/title, product h1 */
  --fs-display-lg: clamp(59.85px, calc(48.99px + 2.895vw), 90.68px);  /* "Experience", chapter titles */
  --fs-display-xl: clamp(98.82px, calc(78.32px + 5.469vw), 157.07px); /* "Shop" */
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans-body);
  font-weight: 300;
  font-size: 16px;
  line-height: 1.8;
  letter-spacing: 0.07em;
  -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

.skip-link {
  position: absolute; left: -999px; top: 0;
  background: var(--bg); color: var(--text);
  padding: 0.75em 1.25em; z-index: 100;
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* ---------- header / nav ---------- */
.site-header {
  position: absolute; top: 0; left: 0; right: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  padding: 2rem 3rem;
  font-family: var(--sans-body);
  font-weight: 400;
  font-size: 14.05px; /* measured mobile (375px); desktop override below */
  letter-spacing: 0.13em;
  text-transform: uppercase;
  text-shadow: var(--text-shadow); /* nav sits over photos on dark pages; no-op (invisible) on light-theme pages since text there is already dark */
}
.site-nav { display: flex; gap: 1.75rem; flex: 1; }
.site-nav a { opacity: 0.88; transition: opacity 0.25s ease; }
.site-nav a:hover, .site-nav a:focus-visible { opacity: 1; }
.site-logo {
  flex: 1; text-align: center;
  font-family: var(--sans-display);
  font-weight: 300;
  font-size: 21px; /* measured desktop (1440px); not fluid-sampled, static */
  letter-spacing: normal;
}
.site-header::after { content: ""; flex: 1; }

@media (min-width: 1024px) {
  .site-header { font-size: 12.54px; } /* measured desktop (1440px) — nav shrinks slightly as viewport grows */
}

@media (max-width: 720px) {
  .site-header { padding: 1.25rem 1.5rem; flex-wrap: wrap; gap: 0.75rem; }
  .site-nav { gap: 1rem; flex-wrap: wrap; order: 2; flex-basis: 100%; justify-content: center; }
  .site-logo { order: 1; flex-basis: 100%; }
  .site-header::after { display: none; }
}

/* ---------- footer ----------
   The TRUE sitewide footer is just the copyright line — confirmed by checking it on
   every page type (chapters, essays, store, product pages): it's always present,
   always identical, height ~28.8px with no section padding. The "Donate/Store/Patreon/…"
   link rows seen on some pages (home, essays index, gifteconomy, the final chapter
   cruel-light) are NOT a shared footer — they're page-specific closing content that
   varies per page (some have their own background photo even) and don't appear on most
   pages at all (confirmed absent on begin, every other chapter, music, store, product
   pages, both other essays). Use `.closing-links` directly in a page's own markup for
   that content — never put link rows back in partials/footer.html. */
.site-footer {
  padding: 0.5rem 1.5rem;
  text-align: center;
  background: #0b0b0c; /* always dark, even on theme-light (essay) pages — confirmed the live footer bar stays black regardless of the page's own theme, not var(--bg) */
}
.footer-copyright { font-size: 16px; color: #1c1b1b; margin: 0; }
.footer-copyright .fc-text { color: #1c1b1b; } /* owner's picks from the tuning sliders (41% of the earlier #454343) */
.footer-copyright .fc-sym  { color: #212020; }   /* the © and ℗ (48%) */
/* the whole copyright line links to /privacy (owner, 2026-10-06) -- same colours as before; underline only on hover/focus */
.footer-copyright .fc-link { color: inherit; text-decoration: none; }
.footer-copyright .fc-link:hover, .footer-copyright .fc-link:focus-visible { text-decoration: underline; text-underline-offset: 3px; }
/* (not on /armada: <body data-plain-floor> keeps its copyright line plain text -- js/include.js) */
 /* owner asked for it closer to black / less immediately visible; was #595757 (live measures ~rgb(89,87,87) and rgb(74,71,71)) */
/* Sitewide footer link row (owner-requested; live's footer is copyright-only). Same look as .closing-links but with a hard-coded light color — the bar is always near-black, even on the light-theme essay pages where var(--text) is dark. */
.footer-links { font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; margin: 0.35rem 0 0.1rem; display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap; } /* flex + 2.5rem gap = the same spacing as .closing-links, for the blog posts' "Back" + "Experience" pair */
.footer-links a { color: #f2f0ec; opacity: 0.8; transition: opacity 0.25s ease; }
.footer-links a:hover, .footer-links a:focus-visible { opacity: 1; }
/* Blog posts (body has data-footer-back): the footer row is just "Back" — no "Experience" link. */
body[data-footer-back] .footer-links a:not(.footer-back) { display: none; }
/* The Experience chapter pages (body has data-no-footer-link) have no footer link row at all. */
body[data-no-footer-link] .footer-links { display: none; }
/* Pages with their own closing-link row (home, essays, gifteconomy, cruel-light) carry "Experience" inside that row instead, so all links share one line — hide the footer's separate row there. */
.has-closing-links .footer-links { display: none; }

/* Page-specific closing links — only on the handful of pages that really have them. */
.closing-links {
  position: relative; z-index: 2; /* needed when used inside a .section with a photo bg — otherwise it paints behind the absolutely-positioned image */
  display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap;
  font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 3.1rem 1.5rem 1.8rem; /* 2/3 of the original 5rem/3rem — bar height 149px → ~99px */
}
.closing-links a { opacity: 0.8; }
.closing-links a:hover, .closing-links a:focus-visible { opacity: 1; }

/* ---------- full-bleed photo sections (home + chapters) ---------- */
.section {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.section-bg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  z-index: 0;
}
/* a second .section-bg stacked after the first (Armada section 3): starts invisible and crossfades in over the
   whole water-reveal schedule (see "Water reveal" below) -- a slow change of the photo itself while the poem's
   still coming into view, not a hard cut. */
.section-bg-crossfade { opacity: 0; transition: none; }
/* Armada section 2 (IMG_0283): a slow, nearly imperceptible right-to-left drift of the photo itself, giving the
   subtle impression the light spot in its centre is travelling across the screen. Driven entirely from JS now
   (js/pan-drift.js writes `transform` directly from a rAF loop) -- see that file for why: two different CSS
   @keyframes approaches (object-position, then transform) both failed to animate in Safari despite the usual
   mitigations, so this moved to the one mechanism already proven reliable there on this exact site (the same
   direct-transform rAF approach js/parallax.js already uses successfully). This rule just gives the wrapper its
   layout box; `.pan-drift-wrap` sits between `.bg-breathe` and the actual <img> so this effect, breathing, and
   (on sections that also have data-parallax) parallax's own transform on the <img> itself each own a different
   element -- no property fights between them. */
.pan-drift-wrap { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.section-bg::after { content: none; }
.section-overlay {
  position: absolute; inset: 0;
  background: var(--overlay);
  z-index: 1;
  pointer-events: none;   /* a plain decorative tint — was intercepting clicks/hover on anything under it that
                              didn't have its own higher z-index (the established workaround elsewhere in this
                              file); the real bug for hover-to-reveal, which needs 20+ individual word spans to
                              be hoverable, not just one or two elements worth promoting */
}
.section-content {
  position: relative; z-index: 2;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
  text-align: center;
  text-shadow: var(--text-shadow); /* inherited by everything inside — legibility without darkening the photo */
}
/* ---------- blurred-bg + inset-photo section (home, section 2) ---------- */
.blur-section .section-bg {
  filter: blur(28px);
  transform: scale(1.12); /* hides the unblurred edge that blur() would otherwise reveal */
}
/* This one's overlay stays — it's not darkening a photo, it's the light wash that makes
   the deliberately-blurred background read as a backdrop rather than a sharp photo. */
.blur-section .section-overlay { background: rgba(0,0,0,0.15); }
.blur-section .section-content {
  max-width: 1100px;
  display: flex;
  align-items: center;
  justify-content: center; /* photo + text column sit as one group, centered: equal free space left and right (owner) */
  gap: 4rem;
  text-align: left;
  padding: 2rem 3rem;
}
.inset-photo {
  flex: 0 0 auto;
  width: min(38vw, 420px);
  aspect-ratio: 4 / 5;
  object-fit: cover;
  box-shadow: 0 20px 60px rgba(0,0,0,0.4);
}
/* The four sentences as ONE column, top to bottom, beside the photo (desktop). Each block keeps
   live's narrow shape: min(14.7vw, 212px) wide (212px at 1440), centered text, links inline;
   size/font/color unchanged. Blocks are stacked with a fixed 1-line gap (1.8rem; was 2.7rem, 3.6rem, then space-between) — owner wanted
   them closer together — and the column is centered on the photo (the parent flex row centers
   both), so it overhangs the photo's top and bottom edges equally (about 2 lines each at 1440;
   the overhang is whatever is left after the tighter spacing). */
.home-quad {
  flex: 0 0 auto; margin-left: 6vw; width: min(14.7vw, 212px);
  display: flex; flex-direction: column; justify-content: center; gap: 1.8rem;
}
.home-quad .home-copy { margin: 0; text-align: center; } /* .home-quad prefix: the shared .pre-fade margin is defined later and would win */
.home-copy .cta-link { display: inline; margin-top: 0; }
/* Desktop: when the Instagram carousel is showing, add 9.5rem of extra section height and bottom padding (min-height is inline on the section, hence !important). Content centers within the same 138vh area as before, so the photo and column do not move, but the carousel (up to 168px tall + 3rem buffer) gets its own room below them instead of overlapping the column. */
@media (min-width: 1100px) { .blur-section.has-ig { padding-bottom: 9.5rem; min-height: calc(138vh + 9.5rem) !important; } }

/* below 1100px the narrow side-by-side column gets cramped — use the plain stack (photo, then text) */
@media (max-width: 1099px) {
  .blur-section .section-content { flex-direction: column; text-align: center; padding: 2rem 1.5rem; }
  .inset-photo { width: min(70vw, 320px); }
  .blur-section { flex-direction: column; justify-content: center; } /* stack photo, then the text, on small screens */
  .blur-section.has-ig { padding-bottom: calc(224px + 5rem); } /* room for the carousel (up to 224px tiles + 3rem buffer + spare) so it never covers the last paragraph */
  .home-quad { display: block; flex: none; margin-left: 0; width: 100%; } /* small screens: no square — plain centered stack under the photo */
  .home-quad .home-copy { width: auto; max-width: 34ch; margin: 0 auto 1rem; padding: 0 1.5rem; }
}

/* ---------- hero / home copy ---------- */
.hero-content { top: -2vh; } /* nudges the hero text slightly higher (~18px at 900px tall) — owner request */
.hero-title {
  font-family: var(--sans-display);
  font-size: var(--fs-display-sm);
  font-weight: 300;
  letter-spacing: normal;
  text-transform: uppercase;
  margin: 0 0 0.4em;
}
.hero-line {
  display: block; color: #fff; /* was var(--text-muted) #bdc4ce; owner asked for whiter */
  font-family: var(--sans-body); font-size: 16px; font-weight: 300;
  letter-spacing: 0.07em; text-transform: none;
}
.lede {
  font-size: 1rem;
  line-height: 1.8;
  letter-spacing: 0.07em;
  color: var(--text-muted);
  max-width: 30ch;
  margin: 0 auto 1.25em;
}
.lede strong { color: var(--text); font-weight: 500; }
.cta-link {
  display: inline-block;
  margin-top: 0.5em;
}
/* One shared treatment for every bolded run in the home "Gift Economy" block — "gift
   economy" itself is the template, and the three CTA links after it use exactly the same
   size/weight (live has all four at 16px / 700 / black; ours are bold white — brighter than
   the #fafafa body text, at the owner's request). */
.home-copy strong,
.home-copy .cta-link {
  font-size: 1rem;
  font-weight: 700;
  color: #fff;
}

/* ---------- experience teaser (home) ----------
   Left-aligned block starting ~15.6% from the left edge (measured on the live site, not
   centered like the rest of the site's photo sections) — this one section is genuinely
   different. Colors are also specific, not the usual --text/--text-muted: the two
   headings use a pale blue-gray (~rgb(195,205,208)), the description and price/button
   row use a dark warm gray/black — confirmed via computed styles on nested spans (the
   live markup wraps each in sqsrte-text-color--custom spans with explicit hex colors). */
/* Each line gets its own margin-left as a % of the full section width — reproduces the
   live site's rightward cascade (mark/title at 15.6%, desc further right at 23.3%,
   enter+price further still at 58.1%/65.8%) — a shared container indent isn't enough,
   every line sits at a genuinely different horizontal start. */
.experience-content {
  text-align: left;
  padding: 0 5% 0 0;
  max-width: none;
  margin: 0;
}
.experience-mark {
  display: block;
  font-family: var(--sans-display);
  font-style: normal;
  font-size: var(--fs-display-lg);
  font-weight: 300;
  line-height: 1.148; /* 104px/91px, measured */
  color: #c3ced0;
  margin: 0 0 0.1em 15.6%;
}
.experience-mark a { color: inherit; }
.mark-accent { color: #740503; } /* the trailing "-.-" — confirmed a distinct dark red on the live site, not the pale blue-gray of the rest of the mark */
.experience-title {
  font-family: var(--sans-display);
  font-style: normal;
  font-size: var(--fs-display-lg);
  font-weight: 300;
  line-height: 1.148; /* 104px/91px, measured */
  letter-spacing: normal;
  text-transform: uppercase;
  color: #bdc4ce;
  margin: 0 0 0.5em 15.6%;
}
.experience-desc {
  font-family: var(--sans-body); /* explicit, not inherited — don't rely on the cascade for this */
  font-weight: 300;
  font-size: 16px;
  line-height: 29px;
  letter-spacing: 0.07em;
  color: #5b5757;
  max-width: 88ch; /* 2x the original 44ch */
  margin: 0 0 1.5em 23.3%;
}
.experience-row { display: flex; align-items: center; gap: 2em; margin-left: 65.8%; } /* +7.7 pts from 58.1% — one "tab" using the same step size as the title→desc jump */
.enter-btn {
  display: inline-block;
  font-family: var(--sans-body);
  border: 1px solid #000;
  color: #000;
  padding: 0.85em 2.4em;
  border-radius: 999px;
  letter-spacing: 0.07em;
  font-weight: 300;
  font-size: 14.3px; font-style: italic; /* live (re-measured): "Enter" is an <em> — Poppins 300 italic 14.3px; not uppercase */
  transition: background 0.3s ease, color 0.3s ease;
  white-space: nowrap;
}
.enter-btn:hover, .enter-btn:focus-visible { background: #000; color: #fff; }
.price-line { color: #000; font-size: 16px; letter-spacing: 0.07em; white-space: nowrap; }
.price-line .was { text-decoration: line-through; }

/* ---------- instagram carousel (home) ----------
   .ig-seam is a zero-height block placed exactly between two sections, above both (z-index 6,
   sections' overflow:hidden can't clip it because it sits outside them). .ig-seam-inner is
   anchored to that line but lifted so its BOTTOM edge sits 3rem above the boundary (owner wanted a
   comfortable buffer, not straddling). Its box is centered on the page. Built by js/instagram.js;
   hidden until it has posts (the [hidden] attribute).
   Motion is done in JS (physics scroller): .ig-viewport clips, .ig-track is moved with a transform,
   so there is no native scrolling/snapping here. Tiles are one third bigger than the first version
   (168px max -> 224px max; gap 12 -> 16px). */
.ig-seam[hidden] { display: none; }
.ig-seam { position: relative; z-index: 6; height: 0; }
.ig-seam-inner {
  position: absolute; top: 0; left: 50%;
  transform: translate(-50%, calc(-100% - 3rem)); /* bottom edge sits 3rem ABOVE the section boundary (a comfortable buffer), not on it */
  width: min(1500px, 100%); padding: 0 3rem; box-sizing: border-box; /* wider window (was 1100px) so more tiles show at once: ~5.6 at 1440 */
}
.ig-viewport {
  overflow: hidden; display: flex; justify-content: safe center; /* few pictures: centered; many: start-aligned and scrollable */
  touch-action: pan-y; overscroll-behavior-x: contain; cursor: grab; user-select: none; -webkit-user-select: none;
}
.ig-viewport.is-dragging { cursor: grabbing; }
.ig-track {
  list-style: none; margin: 0; padding: 0;
  display: flex; gap: 16px; width: max-content; flex: 0 0 auto;
}
.ig-track li { flex: 0 0 clamp(160px, 20.7vw, 224px); }
.ig-track a { position: relative; display: block; -webkit-user-drag: none; }
.ig-track img {
  display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover; -webkit-user-drag: none;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.38);
  transition: transform 0.35s ease, opacity 0.35s ease;
}
.ig-track a:hover img, .ig-track a:focus-visible img { transform: scale(1.03); }
.ig-id { /* only shown with ?ig=ids */
  position: absolute; left: 0; right: 0; bottom: 0; padding: 4px 6px;
  font: 11px/1.3 monospace; letter-spacing: 0; color: #fff; background: rgba(0, 0, 0, 0.7);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 720px) {
  .ig-seam-inner { padding: 0 1.5rem; }
}

/* ---------- /begin page — precisely positioned against the live site (this session) ---------- */
/* Absolute-positioned within the hero section rather than normal document flow, since the
   live site's 3 text blocks sit at specific, independent coordinates (not stacked with
   natural spacing) — measured via getBoundingClientRect() at a 1440x900 reference
   viewport and expressed here as % of the section box, same method as the "Experience"
   section cascade. */
.begin-hero { position: relative; }
.begin-heading {
  position: absolute; left: 7.9%; top: 39.4%; width: 53.4%; z-index: 2; /* sits above .section-overlay (z-index:1), which otherwise intercepts clicks even when fully transparent */
  font-family: var(--sans-display);
  font-size: var(--fs-display-md);
  font-weight: 300;
  line-height: 1.3;
  text-transform: uppercase;
  text-align: center;
  margin: 0;
}
.begin-heading .g1 { color: rgb(147, 156, 160); }
.begin-heading .g2 { color: rgb(170, 167, 167); }
.begin-heading .g3 { color: rgb(211, 204, 204); }
/* the sentence itself is what should "stand out a bit" — same color/weight/font as the rest of the body copy,
   just a quiet halo (no bold, no different font, per the owner's explicit ask). */
.begin-emphasis { text-shadow: 0 0 10px rgba(255, 255, 255, 0.5), 0 0 20px rgba(255, 255, 255, 0.25); }
.begin-body {
  position: absolute; left: 23.3%; top: 59.7%; width: 53.3%; z-index: 2; /* nudged down ~1 line-height (28.8px) from the measured 57.1%, per request */
  font-family: var(--sans-body);
  font-size: 16px;
  font-weight: 300;
  line-height: 28.8px;
  letter-spacing: 0.07em;
  color: rgb(227, 227, 227);
  text-align: center; /* centered, tapering in on the last short line — a deliberate request, not the measured live value */
  margin: 0;
}
.begin-mark-line {
  position: absolute; left: 70.7%; top: 74%; width: 12.7%; z-index: 2; /* nudged up from the measured 79.8%, per request */
  text-align: center;
  font-size: 24.3px;
  white-space: nowrap;
}
.begin-mark-line a { display: inline-block; transition: transform 0.35s ease, letter-spacing 0.35s ease; }
.begin-mark-line a:hover, .begin-mark-line a:focus-visible { transform: scale(1.08); letter-spacing: 0.05em; }
.begin-mark-line .dots { color: rgb(227, 227, 227); font-weight: 700; transition: color 0.35s ease; } /* live: the "•••" runs are <strong> (Poppins 700) while the mark itself stays 300 */
.begin-mark-line .mark { color: rgb(186, 3, 3); transition: color 0.35s ease; }
.begin-mark-line a:hover .dots, .begin-mark-line a:focus-visible .dots { color: #fff; }
.begin-mark-line a:hover .mark, .begin-mark-line a:focus-visible .mark { color: #ff3b30; }

/* ---------- "Free Wallpapers" heading (its own photo section) ---------- */
.wallpapers-heading {
  /* Re-measured directly against the live site (previous left-aligned/15.6%-inset/lavender
     values were wrong — likely copied from the unrelated "Experience" section's treatment
     rather than measured on this element itself). Real values: centered box at 19.04%-80.96%
     of the section width, vertically placed at 53.48% of section height (this is what makes
     it read as nestled into the mountain in the photo, not near the top), pure white,
     line-height exactly 1 (not inherited 1.8). */
  position: absolute;
  left: 19.04%; top: 57.5%; width: 61.92%; /* live's value is 53.48%; owner asked for it just slightly lower (+4 points, ~31px at 1440x900). A bigger drop to 69% (over the mountains) was tried and reverted. */
  z-index: 2;
  font-family: var(--sans-display);
  font-size: clamp(24px, calc(7.1px + 4.536vw), 72.5px); /* was clamp(33.48px, calc(9.9px + 6.287vw), 100.44px) for "Free Wallpapers"; x0.7215 because "Prints and Wallpapers" is 1.386x wider in Figtree, so it still fits the 61.92% box on ONE line. (That earlier value = live's old clamp x0.9: Squarespace fit-text renders this Acumin heading at ~90% of its nominal size so it fills the 61.92% box; Figtree tracks Acumin widths, so the same factor reproduces the live line width (~892px at 1440) */
  line-height: 1;
  font-weight: 300;
  letter-spacing: normal; /* live is `normal`; the body's inherited 0.07em made this wider and, with wrapping on, stacked it on two lines */
  white-space: nowrap; /* live is nowrap — always one line */
  display: flex; justify-content: center; /* centers the line on the box even if the font is ever wider than the box — plain text-align would push the overflow off the right edge only */
  text-transform: uppercase;
  text-align: center;
  color: #fff;
  margin: 0;
}

/* ---------- wallpaper gallery grid ---------- */
/* 2 columns, 4:3 images — measured on the live site (was a much-denser auto-fill grid
   of small tiles before; real site runs a spacious 2-up masonry-ish layout). */
.wallpaper-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.9rem;
  padding: 0.9rem;
  max-width: 1400px;
  margin: 0 auto;
}
.wallpaper-grid img { width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; } /* height:auto — the <img width/height> attributes (there to stop layout shift) would otherwise beat the aspect-ratio */
@media (max-width: 640px) {
  .wallpaper-grid { grid-template-columns: 1fr; }
}

/* ---------- wallpaper grid links + wide photo viewer (js/lightbox.js) ---------- */
.wp-item { display: block; position: relative; overflow: hidden; cursor: zoom-in; }
.wp-item img { transition: transform 0.9s cubic-bezier(0.2, 0.7, 0.2, 1); }
.wp-item:hover img, .wp-item:focus-visible img { transform: scale(1.025); }
.wp-item:focus-visible { outline: 2px solid #f2f0ec; outline-offset: -4px; }
html.lb-open { overflow: hidden; }

.lb {
  border: 0; padding: 0; margin: 0;
  width: 100vw; height: 100vh; height: 100dvh; max-width: none; max-height: none;
  background: #070708; color: #f2f0ec;
  font-family: var(--sans-body); font-weight: 300; letter-spacing: 0.07em;
}
.lb[open] { display: flex; flex-direction: column; }
.lb::backdrop { background: transparent; } /* the dialog itself is full-window; a transparent backdrop lets the page show while it fades in */
.lb-fly { position: fixed; z-index: 2; object-fit: cover; pointer-events: none; box-shadow: 0 30px 90px rgba(0, 0, 0, 0.55); will-change: left, top, width, height; }
/* the picture takes nearly the whole window: only a slim margin, the bottom bar, and arrows that float over it */
.lb-stage { position: relative; flex: 1 1 auto; min-height: 0; display: flex; overflow: hidden; padding: 16px 20px 0; }
.lb-img {
  display: block; margin: auto; max-width: 100%; max-height: 100%; object-fit: contain;
  cursor: zoom-in; user-select: none; -webkit-user-drag: none;
}
/* zoomed to actual pixels: the stage scrolls/pans; JS sets the image's exact size */
.lb-stage.is-zoomed { overflow: auto; padding: 0; scrollbar-width: thin; cursor: grab; }
.lb-stage.is-zoomed:active { cursor: grabbing; }
.lb-stage.is-zoomed .lb-img { max-width: none; max-height: none; cursor: zoom-out; }
.lb-status {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
  font-size: 12px; padding: 0.45em 1.1em; border-radius: 999px; background: rgba(0, 0, 0, 0.62);
  opacity: 0; transition: opacity 0.3s ease; pointer-events: none; white-space: nowrap;
}
.lb-status.on { opacity: 1; }

.lb-btn {
  position: absolute; z-index: 3; display: grid; place-items: center;
  width: 52px; height: 52px; border: 0; border-radius: 50%;
  background: rgba(11, 11, 12, 0.55); color: #f2f0ec; cursor: pointer;
  font: 300 34px/1 var(--sans-body); letter-spacing: 0; padding: 0 0 4px;
  transition: background 0.25s ease;
}
.lb-btn:hover, .lb-btn:focus-visible { background: rgba(11, 11, 12, 0.9); }
.lb-close { top: 14px; right: 14px; font-size: 38px; }
.lb-prev { left: 14px; top: calc(50% - 40px); }
.lb-next { right: 14px; top: calc(50% - 40px); }
.lb-stage.is-zoomed ~ .lb-prev, .lb-stage.is-zoomed ~ .lb-next { display: none; }

.lb-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 24px; padding: 14px 24px 18px; font-size: 12.5px; }
.lb-meta { display: flex; align-items: baseline; gap: 16px; flex: 1 1 0; min-width: 0; }
.lb-count { opacity: 0.6; font-variant-numeric: tabular-nums; white-space: nowrap; }
.lb-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-hint { opacity: 0.45; text-align: center; flex: 0 1 auto; }
.lb-buy {
  flex: 0 0 auto; display: inline-block; white-space: nowrap;
  border: 1px solid #f2f0ec; border-radius: 999px; padding: 0.85em 2em;
  color: #f2f0ec; font-size: 13px; letter-spacing: 0.07em;
  transition: background 0.3s ease, color 0.3s ease;
}
.lb-buy::after { content: "\2192"; margin-left: 0.7em; }
.lb-buy:hover, .lb-buy:focus-visible { background: #f2f0ec; color: #0b0b0c; }
@media (max-width: 720px) {
  .lb-stage { padding: 8px 8px 0; }
  .lb-bar { flex-wrap: wrap; gap: 10px 16px; padding: 10px 14px 14px; }
  .lb-hint { display: none; }
  .lb-btn { width: 44px; height: 44px; font-size: 28px; }
  .lb-close { top: 8px; right: 8px; }
  .lb-prev { left: 6px; } .lb-next { right: 6px; }
}
@media (prefers-reduced-motion: reduce) { .wp-item img, .lb-status, .lb-buy, .lb-btn { transition: none; } }

/* ---------- long-form (essays) ---------- */
/* Essays run on a light theme on the live site (pale gradient background, dark text) —
   distinct from the dark photo-driven theme used on the home/chapter pages. */
body.theme-light {
  --bg: #e9eaec;
  --text: #17181a;
  --text-muted: #3a3d42;
  --text-dim: #86898f;
}
.longform {
  max-width: 68ch;
  margin: 0 auto;
  padding: 9rem 1.5rem 6rem;
  line-height: 1.85;
  text-align: center;
}
.longform .eyebrow {
  font-family: var(--sans-display); font-size: var(--fs-display-md); font-weight: 300;
  letter-spacing: normal; text-transform: uppercase; color: var(--text-dim); margin-bottom: 3rem;
}
.longform .intro { max-width: 46ch; margin: 0 auto 4rem; letter-spacing: 0.07em; color: var(--text); }
.longform .intro strong { font-weight: 700; } /* live: Poppins 700 (was 600, which we do not load, so it rendered 700 anyway) */
.core-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 2.5rem; text-align: left; margin: 0 0 6rem; }
.core-columns h3 { font-size: 1rem; font-weight: 700; letter-spacing: 0.07em; margin: 0 0 1em; }
.core-columns ul { margin: 0; padding-left: 1.1em; letter-spacing: 0.07em; color: var(--text-muted); }
.core-columns li { margin-bottom: 0.9em; }
.longform h1 {
  font-family: var(--sans-display); font-size: var(--fs-display-md); font-weight: 300;
  letter-spacing: normal; text-transform: uppercase; margin: 4rem 0 0.5em; color: var(--text-dim);
}
.longform .byline { color: var(--text-dim); font-size: 1rem; margin: 0 0 2.5em; letter-spacing: 0.07em; display: flex; justify-content: center; gap: 1.5em; }
.longform sup { font-size: 0.65em; line-height: 0; letter-spacing: 0; margin-left: 0.15em; } /* footnote numbers in the essay text — kept as small raised numerals so they don't stretch the line height */
.longform p { margin: 0 0 1.4em; letter-spacing: 0.07em; color: var(--text-muted); max-width: 58ch; margin-left: auto; margin-right: auto; }
/* privacy policy: same longform look, but left-aligned body text and plain section headings (a policy is read, not staged) */
.longform.privacy h1 { margin-bottom: 0.4em; }
.longform.privacy h2 { font-size: 1.05rem; font-weight: 500; letter-spacing: 0.07em; text-align: left; max-width: 58ch; margin: 2.2em auto 0.8em; color: var(--text); }
.longform.privacy p, .longform.privacy ul { text-align: left; }
.longform.privacy ul { max-width: 58ch; margin: 0 auto 1.4em; padding-left: 1.2em; letter-spacing: 0.07em; color: var(--text-muted); }
.longform.privacy li { margin-bottom: 0.8em; }
.longform.privacy a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }

/* Essay pages actually run a real photo behind the pale theme (confirmed via computed
   styles on the live site) — not a flat color as first assumed. Fixed full-page photo,
   pale wash on top so the dark text stays legible, content scrolls over it. */
.longform-photo-bg {
  position: fixed; inset: 0; z-index: -1;
  width: 100%; height: 100%; object-fit: cover;
}
.longform-photo-wash {
  position: fixed; inset: 0; z-index: -1;
  background: rgba(233, 234, 236, 0.72);
}

/* ---------- gift economy page ----------
   Rebuilt against the live page, measured at 1440x900. Live is NOT black-on-sharp-photo
   (an earlier read of this page was wrong): each of its three sections sits on a
   pre-BLURRED photo file (the blur is baked into the image, no CSS filter), with no overlay
   or wash, and the text is one centered column with a distinct pale color per paragraph
   plus bold black key phrases. Nav/logo are black on this page. Pixel gaps below are the
   live blocks' measured spacing; padding on each section holds the live top/bottom offsets. */
body.theme-gift .site-header { color: #000; }
.gift-section { min-height: 0; align-items: flex-start; }
.gift-col {
  position: relative; z-index: 2;
  width: 53.35%; margin: 0 auto;
  text-align: center;
}
.gift-col--wide { width: 68.81%; }
.gift-title {
  font-family: var(--sans-display); font-size: var(--fs-display-md); font-weight: 300;
  line-height: 1.3; letter-spacing: normal; text-transform: uppercase;
  color: rgb(66, 135, 49); margin: 0 0 19px;
}
.gift-col strong { font-weight: 700; color: #000; }
.gift-reasons { list-style: disc; text-align: left; margin: 0 0 53px; padding-left: 40px; }
.gift-reasons li { margin: 0 0 53px; }
.gift-reasons li:last-child { margin-bottom: 0; }
.gift-reasons li::marker { color: #000; }
.gift-col .gift-things-label { text-align: center; color: rgb(239, 240, 242); margin: 0 0 16px; } /* .gift-col prefix: the shared .pre-fade color is defined later in this file and would otherwise win */
.gift-list { list-style: none; text-align: center; margin: 0; padding: 0; } /* centered on the page (was left-aligned with a 40px indent) */
.gift-list li { color: rgb(212, 214, 217); margin: 0 0 8px; } /* a tad darker than the label above it (rgb(239,240,242)); owner found live's grey rgb(130,140,141) illegible over this photo */

/* ---------- chapter / experience pages ---------- */
.chapter-entry {
  position: fixed; inset: 0; z-index: 50;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2rem; padding: 2rem; text-align: center;
  background: var(--bg);
  transition: opacity 1.1s ease, visibility 1.1s ease;
}
.chapter-entry.hidden { opacity: 0; visibility: hidden; pointer-events: none; }
.chapter-entry p { max-width: 36ch; margin: 0; color: var(--text-muted); line-height: 1.7; font-size: 1rem; letter-spacing: 0.07em; }
.chapter-mark { font-family: var(--sans-display); font-size: 1.3rem; letter-spacing: 0.1em; color: var(--text-dim); }
.begin-btn {
  font-family: var(--sans-body);
  font-size: 0.85rem; letter-spacing: 0.14em; text-transform: uppercase;
  background: none; border: 1px solid var(--text); color: var(--text);
  padding: 0.9em 2.6em; border-radius: 999px; cursor: pointer;
  transition: background 0.3s ease, color 0.3s ease;
}
.begin-btn:hover, .begin-btn:focus-visible { background: var(--text); color: var(--bg); }

.chapter-main { opacity: 0; transition: opacity 1.2s ease; }
.chapter-main.visible { opacity: 1; }

.chapter-hero {
  min-height: 100vh; position: relative; display: flex; align-items: center; justify-content: center;
}
.chapter-hero-title {
  position: relative; z-index: 2;
  font-family: var(--sans-display);
  font-size: var(--fs-display-lg); font-weight: 300;
  letter-spacing: normal; text-transform: uppercase; text-align: center;
  color: var(--accent);
  text-shadow: var(--text-shadow);
}

/* ---------- chapter poem staging (start / armada / cannot-see / revolver / cruel-light) ----------
   The live site places every stanza as its own Squarespace "Fluid Engine" block with an
   explicit grid position, independent of source order — which is *why* live stanzas
   sometimes read in a different order than they're positioned on screen (grid placement
   doesn't care about DOM order). Rather than fight that, each .stanza below is absolutely
   positioned using left/top/width percentages pulled directly from
   getBoundingClientRect() on the live page at a 1440px reference viewport (same method as
   .begin-body/.experience-mark elsewhere in this file) — so position matches precisely
   while the underlying HTML stays in true top-to-bottom reading order. Colors/fonts are
   per-chapter (each measured off its own live page), so they're set inline per element;
   these classes only carry what's structurally shared. */
.stanza { position: absolute; }
.stanza .pre-fade { color: #000; margin: 0 0 1em; } /* measured: live line-to-line spacing is ~44.8px at 16px/28.8lh — that's exactly 1em of margin-bottom, not the old guessed 0.35em (found via the fingerprint-diff tool's cumulative TOP drift within multi-line stanzas — see .claude/skills/rebuild-chapter-page/) */
[data-stanza-center] .stanza { will-change: transform; }   /* js/stanza-center.js writes transform every frame while gliding/reverting */
.chapter-mark {
  position: absolute;
  font-family: var(--sans-display);
  font-size: var(--fs-display-lg); font-weight: 300; line-height: 1.148;
  text-align: center;
  margin: 0;
}
.chapter-glyph {
  position: absolute;
  font-family: var(--sans-display);
  font-size: var(--fs-display-md); font-weight: 300; line-height: 1.3;
  text-align: left;
  margin: 0;
}
.start-intro {
  position: absolute;
  font-family: var(--sans-body);
  font-weight: 300; font-size: 16px; line-height: 28.8px; letter-spacing: 0.07em;
  color: #fcfbfb;
  text-align: justify;
  margin: 0;
}
.next-link {
  position: absolute;
  display: inline-block;
  font-family: var(--sans-display);
  font-size: 25.96px; font-weight: 300; line-height: 35.47px;
  text-align: center;
  text-transform: none; letter-spacing: normal;
  color: var(--link-color, #000);
  transition: color 0.35s ease, transform 0.35s ease;
}
.next-link:hover, .next-link:focus-visible { color: var(--link-hover-color, #ff3b30); transform: scale(1.06); }
/* /start's hidden "Armada": barely lighter than the near-black picture behind it, brightening slowly (colour only, no scale) on hover */
.next-link--quiet { width: auto; white-space: nowrap; transition: color 0.9s ease; font-family: 'DM Serif Display', Georgia, serif; font-weight: 400; } /* DM Serif Display (Google Fonts, loaded by start/index.html) — it only comes in regular weight */
.next-link--quiet:hover, .next-link--quiet:focus-visible { transform: none; }
/* Armada's "Cannot See" — Parisienne (Google Fonts, loaded by armada/index.html), sized up since script faces read smaller than their point size */
.next-link--script { font-family: 'Parisienne', cursive; font-size: 1.6em; }

/* .fade-group is primarily a JS hook (reveal.js) for the scroll-reveal IntersectionObserver.
   No layout rules here on purpose — it's applied both to full-bleed <section> wrappers
   (where .section-content controls width) and to standalone narrow text blocks (which set
   their own max-width inline). A max-width here previously leaked onto every full-bleed
   section that also carried this class, squashing them to a narrow column — see CLAUDE.md. */
.fade-group {
  position: relative; z-index: 2;
}
.pre-fade {
  white-space: pre-wrap;
  opacity: 0;
  font-size: 16px;
  line-height: 1.8;
  letter-spacing: 0.07em;
  margin: 0 0 0.35em;
  color: #fafafa;
}
.fade-group.in-view .pre-fade { opacity: 1; }
/* Armada: the poem-line text (not the "Armada" title/glyph) is Bitter on the live site — confirmed via its
   dev tools (Bitter, weight 400, 16px) — loaded by armada/index.html. */
body.armada-page .pre-fade { font-family: 'Bitter', serif; }
/* Armada's title — Cormorant Garamond at its lightest usable weight (400; the owner asked for 500 pulled
   slighter), colour untouched (#d0e5ff, set inline on the element already) */
body.armada-page .chapter-mark { font-family: 'Cormorant Garamond', serif; font-weight: 400; }
/* Cannot See: everything as measured off live (dev tools, 1440x900) -- poem lines Bitter 400 16px/24px, normal
   tracking, 16px between lines; the title Shrikhand 45.87px (3.1856vw) / 51.08px, -0.02em tracking. Both are free
   Google Fonts, loaded by cannot-see/index.html. The title's opacity is driven by js/title-fade.js. */
body.cannot-see-page .pre-fade { font-family: 'Bitter', serif; font-weight: 400; line-height: 24px; letter-spacing: normal; }
body.cannot-see-page .stanza .pre-fade { margin: 0 0 16px; }
.cs-title {
  position: absolute; z-index: 2; margin: 0;
  font: 400 3.1856vw/1.1136 'Shrikhand', serif; letter-spacing: -0.02em;
  text-align: center; white-space: pre-wrap;
  opacity: 0;
}
.cs-next { text-decoration: none; }
/* Cannot See section 1: blurred photo that clears as the visitor scrolls, fully sharp halfway down the section
   (owner, 2026-10-06; js/scroll-unblur.js writes the live blur). A backdrop blur over the photo rather than a filter on
   the photo itself: a filter treats everything outside the picture as transparent, which would leave a soft dark
   fringe along the section's top edge where the parallax holds the picture flush. The title sits above it, sharp. */
/* Cannot See parallax strength on sections 1, 2, 4, 5 (owner, 2026-10-06: "more aggressive" than live's 30) */
:root { --cs-parallax: 60; }
:root { --cs-s1-blur: 22px; --cs-gap-2: 0.98; --cs-gap-3: 0.67; --cs-gap-4: 0.93; --cs-gap-5: 1.5; --cs-gap-6: 1.05; }   /* blur + spacing: owner, 2026-10-06 */
/* x1: section 2's whole text group (all its blocks and flourishes) moves sideways together, on top of each block's own offset.
   Section 2's values (x1 8%, "sightless father" 0.9%, "to lightlessness" 0.25%, spacing 0.98): owner, 2026-10-06; its sliders removed. */
:root { --cs-x-g2: 8%; --cs-x-g3: -4.1%; --cs-x-3-fl2: 6.7%; }   /* + section 3's group (x1) and its bottom flourish's own offset */
/* section 3: x2 moves its whole group vertically (% of the section's height); its two flourishes are tied to their own
   lines -- the top one above "movement ability,", the bottom one below "stillnesses" -- by the same visible gap, measured
   to the lettering itself (owner, 2026-10-06: the two gaps looked different); and the bottom flourish's own width */
:root { --cs-y-g3: 4.2%; --cs-fl-gap-3: 34px; --cs-fl-w-3-2: 2.85%; }   /* owner, 2026-10-06 */
/* section 4's two flourishes: own horizontal offsets + widths (live's widths to start) */
:root { --cs-x-4-fl1: 0%; --cs-x-4-fl2: 0%; --cs-fl-w-4-1: 18.5625%; --cs-fl-w-4-2: 10.8319%; }
/* each text block's own horizontal offset (% of page width; ?tune=1 "Section N · first words (horizontal)"), all 0 until set */
:root { --cs-x-1-1: 0%; --cs-x-2-1: 0.9%; --cs-x-2-2: 0%; --cs-x-2-3: 0%; --cs-x-2-4: 0%; --cs-x-2-5: 0%; --cs-x-2-6: 0%; --cs-x-2-7: 0.25%; --cs-x-3-1: 2.4%; --cs-x-3-2: 6.45%; --cs-x-3-3: 4.75%; --cs-x-4-1: 0%; --cs-x-4-2: 0%; --cs-x-4-3: 0%; --cs-x-4-4: 0%; --cs-x-5-1: 0%; --cs-x-5-2: 0%; --cs-x-5-3: 0%; --cs-x-5-4: 0%; --cs-x-6-1: 0%; --cs-x-6-2: 0%; --cs-x-6-3: 0%; --cs-x-6-4: 0%; --cs-x-6-5: 0%; --cs-x-6-6: 0%; }
.cs-unblur { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
/* Cannot See sections 2-6: --cs-gap-N draws that section's text and flourishes together around their middle
   (1 = as laid out; owner's values above; the ?tune=1 slider label gives the value that restores the spacing from before the sections were
   made taller). Each element's top is calc(middle + offset * gap), written into cannot-see/index.html. */

/* Hover-to-reveal (js/hover-reveal.js): words sit barely visible until the pointer finds them -- 0.025, down
   from an earlier 0.07 (owner: "even more illegible... not completely invisible, but closer to it"). --hv-dur
   is tunable (?hvdur=140 or the ?tune=1 panel); the transition is linear BY DEFAULT so "smooth but nearly
   instant" doesn't read as an ease-out lingering at the end -- --hv-ease exists so a specific section can
   override the CURVE too, not just the duration, when it wants an actually smooth (not just slow) fade
   (Armada's IMG_0292 section: --hv-dur/--hv-ease both set inline on the section, see armada/index.html). */
:root { --hv-dur: 140ms; --hv-poke-dur: 220ms; }
[data-hover-reveal] .hv-word { opacity: 0.025; transition: opacity var(--hv-dur) var(--hv-ease, linear); }
[data-hover-reveal] .hv-word.hv-on { opacity: 1; }
/* once a word has been revealed for the first time (js/hover-reveal.js sets .hv-was-revealed permanently on
   that first mouseenter), it fades back down to TRUE invisibility instead of the barely-visible hint every
   subsequent time — the hint's only job is inviting that first hover. :not(.hv-on) keeps this from fighting
   the rule above while the word is actively revealed. */
[data-hover-reveal] .hv-word.hv-was-revealed:not(.hv-on) { opacity: 0; }
/* data-hover-reveal="permanent": every word starts at TRUE invisibility (0, not the usual 0.07 hint) and
   js/hover-reveal.js never removes .hv-on from any of them once hovered — same --hv-dur reveal speed as the
   default variant otherwise. Same-specificity attribute selector as the base rule above, so this only wins
   because it's declared after it — keep it below. */
[data-hover-reveal="permanent"] .hv-word { opacity: 0; }
/* The title ("Armada") plays by different rules, and its opacity/brightness are driven by --glow-progress (a
   0-1 value js/hover-reveal.js writes every frame via requestAnimationFrame, NOT a CSS `transition`) rather
   than an .hv-on class -- the letter-poke feature below needs to read the CURRENT progress at any instant,
   which a `transition: opacity 32s ease` can't expose to JS mid-flight. Fully invisible (--glow-progress
   defaults to 0 until JS starts writing it) until any OTHER word in the section is hovered, then eases up over
   32s to full opacity plus a brightness boost past "normal" full visibility -- a glow, not just a reveal
   (started at 1.7x brightness per the owner's own first spec; toned down to 1.3x, "a little much" at 1.7;
   duration started at 16s, doubled to 32s). The `transition` here is only for the letter-poke's own quick
   fades below -- at a 220ms default against a 32s glow it's an imperceptible ~0.7% lag behind the "true" curve,
   which is why one shared transition can serve both without the .hv-fast-class-override hack an earlier
   version of this needed. */
[data-hover-reveal] .hv-word.hv-title {
  opacity: var(--glow-progress, 0);
  filter: brightness(calc(1 + 0.3 * var(--glow-progress, 0)));
  transition: opacity var(--hv-poke-dur, 220ms) ease, filter var(--hv-poke-dur, 220ms) ease;
}
/* Letter-poke (title only, wrapped letter-by-letter): hovering a letter TOGGLES it hidden/shown, independent of
   whether the glow-in above has started, finished, or hasn't happened at all yet.
   .hv-touched is added PERMANENTLY the first time a letter is ever poked (same one-shot-marker pattern as
   .hv-was-revealed elsewhere in this file): from then on its "shown" baseline is tethered to a 0.18 floor
   instead of the raw (possibly still-zero) glow progress, so poking visibly does something even before the
   stanza's ever been touched. Before that first poke, a letter has no baseline override at all — it just shows
   the ambient glow like every other title letter, exactly as if poking didn't exist yet.
   .hv-poked is the ACTUAL hidden state — this is the part an earlier version of this got backwards: it had
   .hv-poked showing an ELEVATED, tethered opacity, meaning neither toggle state was ever actually hidden once
   the glow had progressed at all, which is why poking stopped doing anything visible in practice. Now .hv-poked
   always means "hidden" (opacity 0), full stop, at any glow progress. */
[data-hover-reveal] .hv-word.hv-title.hv-touched { opacity: max(0.18, var(--glow-progress, 0)); }
[data-hover-reveal] .hv-word.hv-title.hv-poked { opacity: 0; }
@media (prefers-reduced-motion: reduce) { [data-hover-reveal] .hv-word { opacity: 1; transition: none; filter: none; } }

/* Water reveal (js/water-reveal.js, [data-water-reveal] on a section -- Armada section 3). This replaces the old
   "shimmer" sweep entirely. ONE master clock (that file) starts when the whole poem is on screen and drives all of
   it together: the letters popping in one at a time, the flourish, the crossfade from the sharp photo to the
   blurred one, and the moving light that makes the words visible at all. The words themselves are nearly
   invisible BY DESIGN (--wl-rest): each letter flashes once as it's "spoken", then sinks to that near-zero
   opacity, so the poem only reads through light travelling across it -- as if it lay under the surface of the
   photo. Everything here is plain CSS driven by classes the clock adds; no per-frame styling from JS. */
:root { --wl-rest: 0.05; --wl-speed: 7.4; --flourish-glow-amt: 0.5; --flourish-glow-dur: 20s; --stanza-glow-amt: 0.1; --s5-glow-amt: 1.2; }   /* --wl-speed: multiplies how fast the water-light bands glide (wl-flow/wl-wobble below) -- baked in from the owner's ?tune=1 slider. --flourish-glow-amt/--flourish-glow-dur: the flourish's pulse strength and cycle length (see "flourish-glow" below) -- both baked in from the owner's sliders. --stanza-glow-amt: strength of Armada section 4's hover-centre finale glow (js/stanza-center.js reads this directly every frame, not via a CSS animation -- see that file) -- baked in from the owner's slider. --s5-glow-amt: a separate, static, always-on legibility glow for section 5's three stanzas (see the rule below) -- baked in from the owner's slider; independent of that section's existing hover-reveal/glow-cascade work, not a replacement for it */
/* Section 5 (Armada's IMG_0292, [data-glow-cascade]) legibility glow -- a plain static text-shadow, not animated
   and not gated on hover/reveal state, layered on top of the colour work already done for this stanza rather
   than changing it. Scoped to [data-glow-cascade] specifically (currently unique to this section) so it can't
   leak onto any other section's text. */
[data-glow-cascade] .stanza .pre-fade { text-shadow: 0 0 calc(2.5px * var(--s5-glow-amt, 0)) rgba(255, 255, 255, calc(0.55 * var(--s5-glow-amt, 0))); }
[data-water-reveal] .pre-fade { opacity: 1 !important; transition: none !important; }   /* the lines are always "there"; their letters (below) are what appear */
/* kerning off on the line AND its overlay so the split-up letters (inline-blocks) and the un-split overlay text lay
   out identically -- otherwise the two could drift a pixel or two apart along a long line */
.wl-line { position: relative; font-kerning: none; font-feature-settings: "kern" 0, "liga" 0; }
.wl-line::before { font-kerning: none; font-feature-settings: "kern" 0, "liga" 0; }
.wl-ch { display: inline-block; opacity: 0; }
/* A second look at this, per the owner ("looks a bit unofficial... don't like how the letters jump out"): the
   original version moved AND resized each letter (translateY + scale, overshooting past its own rest size) on
   top of the opacity change -- three properties changing at once is what read as a mechanical "pop", not a
   letter quietly catching light. This version touches ONLY opacity -- no transform at all -- so the letters
   never move or resize, just brighten then settle; still a clear flash (near-full brightness at 35%, matching
   the old pop's "arrives quickly" timing) but uniform across every letter and every line, since nothing here
   depends on the letter's own size/shape the way a scale overshoot subtly did (a wide letter's scale reads
   differently than a narrow one's). Kept the class name (wl-pop) since it's an internal hook, not user-facing. */
.wl-ch.wl-pop { animation: wl-pop 1.1s ease-out forwards; }
@keyframes wl-pop {
  0%   { opacity: 0; }
  35%  { opacity: 0.92; }
  100% { opacity: var(--wl-rest); }
}
/* The water-light: a separate overlay copy of the line's text (content: attr(data-text)), painted only where
   moving bands of light fall on it (background-clip: text), so the light only ever lands on letterforms.
   Three bands at different angles, widths and speeds (one running the opposite way) slide across at once, so
   they slip in and out of alignment like real reflections rather than marching in step; a slight wobble
   makes the whole layer waver. --e (set by the clock) is how far along the line has been "spoken" so far. */
.wl-line::before {
  content: attr(data-text);
  position: absolute; inset: 0;
  white-space: pre-wrap;
  pointer-events: none;
  color: transparent; -webkit-text-fill-color: transparent;
  background-image:
    linear-gradient(72deg,  transparent 0 34%, rgba(226, 241, 255, 0.95) 41%, transparent 49% 100%),
    linear-gradient(108deg, transparent 0 40%, rgba(200, 226, 255, 0.80) 46%, transparent 55% 100%),
    linear-gradient(88deg,  transparent 0 22%, rgba(255, 255, 255, 0.60) 26%, transparent 31% 100%);
  background-size: 700px 100%, 900px 100%, 460px 100%;
  background-repeat: repeat-x;
  -webkit-background-clip: text; background-clip: text;
  -webkit-mask-image: linear-gradient(to right, #000 calc(var(--e, 0px) - 4px), transparent var(--e, 0px));
          mask-image: linear-gradient(to right, #000 calc(var(--e, 0px) - 4px), transparent var(--e, 0px));
  opacity: 0.55;
  animation: wl-flow 52s linear infinite, wl-wobble 13s ease-in-out infinite alternate;
  animation-duration: calc(52s / var(--wl-speed, 1)), calc(13s / var(--wl-speed, 1));   /* --wl-speed: see :root */
  animation-delay: var(--wl-phase, 0s), var(--wl-phase2, 0s);
}
/* each band moves by exactly its own tile width per loop (700 / -900 / 460px), so the loop is seamless */
@keyframes wl-flow { from { background-position: 0 0, 0 0, 0 0; } to { background-position: 700px 0, -900px 0, 460px 0; } }
@keyframes wl-wobble { from { transform: translate(0, -0.6px) skewX(-0.6deg); } to { transform: translate(1px, 0.6px) skewX(0.6deg); } }
/* Per-word hover: each .wl-word (js/water-reveal.js) gets its OWN copy of the exact same water-light overlay,
   sized to just that word. Hovering an already-fully-popped word (js/water-reveal.js only ever arms this once
   it has -- text that hasn't been "spoken" yet still can't be jumped ahead of) fades this ONE layer in quickly,
   reading as the light gathering to a focal point at the cursor; leaving fades it back out slowly, which simply
   reveals the line's own ambient overlay underneath again -- that one is never touched by any of this, so
   there's nothing to "resync", the hovered word just stops covering it.
   The fade itself is driven FRAME-BY-FRAME from js/water-reveal.js (--wl-hot, 0..1, written every rAF tick),
   NOT a CSS `transition`. That was the first version, and testing found it genuinely unreliable: a CSS opacity
   transition sharing an element with this same pseudo-element's own always-declared wl-flow/wl-wobble animations
   would frequently just never start (confirmed directly -- toggling the class alone, no hover involved, opacity
   would sit stuck at its old value indefinitely on roughly half of individual, isolated tries, with or without
   animation-play-state in the mix) -- a known class of engine flakiness (a transition and a running/declared
   animation competing for the same compositor layer) that Safari hits harder than Chromium. That's the real
   explanation for "hover works for a few words, then appears to just stop": not a load problem, a transition
   silently failing to engage. JS interpolating --wl-hot itself sidesteps the whole class of bug, matches how
   every other animation in this file is driven (one rAF clock owns the timing, CSS only owns the look), and
   still resumes from wherever a word's fade was instead of restarting at 0 when re-hovered mid-fade.
   animation-play-state is still paused while a word is fully at rest (--wl-hot at 0) and set running the moment
   it's non-zero (js toggles a class for this) -- a poem-length line can have dozens of these overlays, and
   running all of them (background-clip:text + 3 gradient layers, expensive to composite) permanently, even at
   opacity 0, is real wasted compositor load; this part of the original diagnosis was correct and worth keeping
   even though it wasn't the actual cause of the stuck-hover bug. */
/* The line's own ambient light is `.wl-line::before`, not `::after` -- deliberately: a `::before` paints BEFORE
   (underneath) real children by default paint order, so `.wl-word` (a real child) and its OWN `::after` overlay
   always sit on top of it automatically, no z-index needed. An earlier version used `::after` for the line and
   relied on an explicit z-index to win the stacking fight -- correct per spec, but z-index + pseudo-elements +
   inline-block has a real history of Safari-specific quirks, which was the actual suspect once hover started
   working for some words and not others. Removing the fight entirely (rather than trying to win it more
   forcefully) is the more robust fix. */
.wl-word { position: relative; display: inline-block; --wl-hot: 0; }
.wl-word::after {
  content: attr(data-word-text);
  position: absolute; inset: 0;
  white-space: pre-wrap;
  pointer-events: none;
  font-kerning: none; font-feature-settings: "kern" 0, "liga" 0;
  color: transparent; -webkit-text-fill-color: transparent;
  background-image:
    linear-gradient(72deg,  transparent 0 34%, rgba(226, 241, 255, 0.95) 41%, transparent 49% 100%),
    linear-gradient(108deg, transparent 0 40%, rgba(200, 226, 255, 0.80) 46%, transparent 55% 100%),
    linear-gradient(88deg,  transparent 0 22%, rgba(255, 255, 255, 0.60) 26%, transparent 31% 100%);
  background-size: 700px 100%, 900px 100%, 460px 100%;
  background-repeat: repeat-x;
  -webkit-background-clip: text; background-clip: text;
  opacity: var(--wl-hot, 0);   /* js/water-reveal.js writes this every frame -- no CSS transition, see comment above */
  animation: wl-flow 52s linear infinite, wl-wobble 13s ease-in-out infinite alternate;
  animation-duration: calc(52s / var(--wl-speed, 1)), calc(13s / var(--wl-speed, 1));   /* --wl-speed: see :root */
  animation-delay: var(--wl-phase, 0s), var(--wl-phase2, 0s);
  animation-play-state: paused, paused;   /* only .wl-word-live below (js-toggled) sets this to running */
}
.wl-word.wl-word-live::after { animation-play-state: running, running; }
@media (prefers-reduced-motion: reduce) { .wl-word::after { display: none; } }
/* js/tune.js's data-tune-var-restart support: browsers don't resample a running animation's used values
   (duration, delay, or any property it drives via a custom property) from an updated custom property, so a
   slider briefly adds this class (then removes it after a reflow) to force the animation to restart and pick
   up the new value. !important + covering both the element itself and its pseudo-elements, since different
   sliders restart different targets (--wl-speed restarts .wl-line/.wl-word's own ::before/::after; the
   flourish glow slider restarts the .flourish element's own animation directly). */
.tune-restart, .tune-restart::before, .tune-restart::after { animation: none !important; }
/* the light strengthens over the whole schedule, in step with the photo crossfade below */
[data-water-reveal].wl-go .wl-line::before { opacity: 1; transition: opacity var(--wl-total, 34s) linear; }
/* a line that wraps onto a second row (narrow screens) can't use a left-to-right edge -- it fades in whole instead */
/* (selectors deliberately as specific as the ramp-up rule above -- a shorter one loses to it, and a wrapped line then lights up before its first letter) */
[data-water-reveal] .wl-line.wl-wrapped::before,
[data-water-reveal].wl-go .wl-line.wl-wrapped::before { -webkit-mask-image: none; mask-image: none; opacity: 0; transition: none; }
[data-water-reveal].wl-go .wl-line.wl-wrapped.wl-started::before { opacity: 1; transition: opacity 3s ease; }
/* the slow crossfade from the sharp photo to the blurred one runs on the same master clock */
[data-water-reveal] .section-bg-crossfade { opacity: 0; transition: none; }
[data-water-reveal].wl-go .section-bg-crossfade { opacity: 1; transition: opacity var(--wl-total, 34s) linear; }
/* reduced motion: no clock, no animation -- the poem just shows, and the blurred photo is simply there */
[data-water-reveal].wl-static .section-bg-crossfade { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .wl-line::before { display: none; } }
.audio-note {
  position: relative; z-index: 2;
  margin: 0 auto 3rem; max-width: 30ch; text-align: center;
  font-size: 0.75rem; letter-spacing: 0.06em; color: var(--text-dim);
  border: 1px dashed var(--text-dim); border-radius: 8px; padding: 1em 1.25em;
}
.next-chapter {
  display: block; text-align: center; padding: 6rem 1.5rem 8rem;
  font-size: 0.85rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--text-muted);
}
.next-chapter:hover, .next-chapter:focus-visible { color: var(--text); }

/* Top-to-bottom wipe reveal (js/reveal.js): a block marked data-wipe stays hidden until the WHOLE block
   is on screen, then a soft-edged mask slides down it in --wipe-dur (default 0.88s). While it is armed
   the lines' own per-line fade is switched off (the wipe replaces it). The mask is a 3x-tall gradient
   (solid, then a soft edge, then clear) whose position is slid from "all clear" to "all solid". Only
   applies once JS has armed it, so without JS the text just shows. */
.stanza[data-wipe] .pre-fade { opacity: 1 !important; transition: none !important; }
.stanza[data-wipe].wipe-armed {
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 33.3%, transparent 66.6%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0, #000 33.3%, transparent 66.6%, transparent 100%);
  -webkit-mask-size: 100% 300%;           mask-size: 100% 300%;
  -webkit-mask-repeat: no-repeat;         mask-repeat: no-repeat;
  -webkit-mask-position: 0 100%;          mask-position: 0 100%;
}
/* the transition lives ONLY on the triggered state — if it were on the armed state, arming (adding the mask at load) would itself animate from "visible" to "hidden" and the text would flash on first */
.stanza[data-wipe].wipe-armed.wipe-in {
  -webkit-mask-position: 0 0; mask-position: 0 0;
  -webkit-transition: -webkit-mask-position var(--wipe-dur, 2.64s) cubic-bezier(0.37, 0, 0.63, 1);
          transition: mask-position var(--wipe-dur, 2.64s) cubic-bezier(0.37, 0, 0.63, 1);
}

/* Line-by-line left-to-right reveal (js/reveal.js, [data-line-reveal="1,3.3"] on a section): every text line of
   the section is its own segment; once the whole text is on screen each one is revealed by a soft-edged mask
   sliding left to right in --lr-dur, the k-th one starting k x --lr-gap seconds after the first. The lines'
   own opacity fade is switched off (!important beats reveal.js's inline transitions). Lines shrink to the
   width of their text (fit-content) so the wipe runs across the words, not across empty space. */
.lr-line { opacity: 1 !important; width: fit-content; max-width: 100%; }
/* centred / right-aligned lines shrink to their text, so they must be pushed back to where their alignment puts them */
.stanza .lr-c { margin-left: auto; margin-right: auto; }   /* (classes added by js/reveal.js from each line's text-align) */
.stanza .lr-r { margin-left: auto; }
.lr-line.lr-armed {
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 36%, transparent 60%, transparent 100%);
          mask-image: linear-gradient(to right, #000 0, #000 36%, transparent 60%, transparent 100%);
  -webkit-mask-size: 300% 100%;           mask-size: 300% 100%;
  -webkit-mask-repeat: no-repeat;         mask-repeat: no-repeat;
  -webkit-mask-position: 100% 0;          mask-position: 100% 0;
  transition: none !important;              /* (armed state must not animate: see the wipe note above) */
}
.lr-line.lr-armed.lr-go {
  -webkit-mask-position: 0 0; mask-position: 0 0;
  transition-property: -webkit-mask-position, mask-position !important;
  transition-duration: var(--lr-dur, 1s) !important;
  transition-timing-function: cubic-bezier(0.37, 0, 0.63, 1) !important;
  transition-delay: var(--lr-delay, 0s) !important;
}

/* Scroll-linked left-to-right reveal (js/reveal.js, [data-scroll-reveal] on a section): each text line is
   uncovered by a soft edge that JS moves along the words as you scroll (--e, in px from the line's left edge);
   the edge position follows the scroll exactly (no transition), and reverses when you scroll back up. */
.sr-line {
  opacity: 1 !important; transition: none !important;
  -webkit-mask-image: linear-gradient(to right, #000 calc(var(--e, 0px) - var(--band, 80px)), transparent var(--e, 0px));
          mask-image: linear-gradient(to right, #000 calc(var(--e, 0px) - var(--band, 80px)), transparent var(--e, 0px));
}
@media (prefers-reduced-motion: reduce) {
  .chapter-entry, .chapter-main, .pre-fade { transition: none !important; }
  .stanza[data-wipe].wipe-armed { -webkit-mask-image: none; mask-image: none; }
  .lr-line.lr-armed { -webkit-mask-image: none; mask-image: none; }
  .sr-line { -webkit-mask-image: none; mask-image: none; }
}

/* /start's ending, in three stages (all plain masks/gradients — no backdrop-filter, which Safari doesn't mask):
   1) the LAST red picture melts at its bottom: two blurred copies of itself (.bg-melt, same parallax) fade in, then a
      black gradient (.bg-melt-black) takes over — all in the free space below the last poem line;
   2) a stretch of pure black (the closing section's own background);
   3) the closing section's blue/black picture fades up out of that black. */
.bg-melt { position: absolute; inset: 0; overflow: hidden; z-index: 0; pointer-events: none; }
.bg-melt .bg-copy { filter: blur(12px); }
.bg-melt--1 { -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent 63%, rgba(0,0,0,0.3) 69%, rgba(0,0,0,0.75) 75%, #000 82%, #000 100%);
                      mask-image: linear-gradient(to bottom, transparent 0, transparent 63%, rgba(0,0,0,0.3) 69%, rgba(0,0,0,0.75) 75%, #000 82%, #000 100%); }
.bg-melt--2 .bg-copy { filter: blur(34px); }
.bg-melt--2 { -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent 72%, rgba(0,0,0,0.3) 78%, rgba(0,0,0,0.75) 84%, #000 91%, #000 100%);
                      mask-image: linear-gradient(to bottom, transparent 0, transparent 72%, rgba(0,0,0,0.3) 78%, rgba(0,0,0,0.75) 84%, #000 91%, #000 100%); }
.bg-melt-black { position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(to bottom, rgba(0,0,0,0) 0, rgba(0,0,0,0) 66%, rgba(0,0,0,0.3) 76%, rgba(0,0,0,0.7) 86%, rgba(0,0,0,0.93) 95%, #000 100%); }
.section--blend-in { background: #000; }
/* fine film grain over the whole closing section: near-black gradients only have a handful of 8-bit levels, which
   shows as contour "lines" and block edges once the picture is enlarged; a little noise breaks those steps up so the
   darkness reads as natural. (A tiled SVG noise, drawn once and repeated — cheap.) */
.section--blend-in::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: 0.09;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1.1 -.35'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
}
.section--blend-in .next-link { z-index: 2; }
/* A dark overlay on the closing picture that hides its faint artifacts (owner's idea): strongest through the black and
   blue part, then fading out over the last ~35% of the section so it is gone by the time the red at the very bottom comes
   into view. Its strength is the element's opacity (default 0.7; try ?tune=1 for a slider). */
.veil-dark {
  position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: 0.7;
  background: linear-gradient(to bottom, #000 0, #000 60%, rgba(0,0,0,0.85) 70%, rgba(0,0,0,0.6) 79%, rgba(0,0,0,0.3) 87%, rgba(0,0,0,0.1) 93%, rgba(0,0,0,0) 97%, rgba(0,0,0,0) 100%);
}
.section--blend-in .section-bg {
  /* The picture file is PRE-CROPPED (IMG_4462-strip-*.jpg): the centre 32% of the original's width x its top 75.5%
     (so the file ends where the red glow's first ~3% is showing) — at the size it is shown the picture is an ordinary
     section-sized image, not a 4,800px-wide monster that has to be tiled, masked and scaled by the browser (which is
     where straight vertical/horizontal seams came from). It is pinned to the BOTTOM (object-position) so the red always
     ends the page whatever the screen width. */
  object-position: 50% 100%;
  /* black first, then it fades up: percentages of the section (300vh): black to 16.4% (49vh), fully clear by 57% (171vh) */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent 16.4%, rgba(0,0,0,0.06) 21.8%, rgba(0,0,0,0.2) 29.6%, rgba(0,0,0,0.45) 39%, rgba(0,0,0,0.75) 48.4%, #000 57%, #000 100%);
          mask-image: linear-gradient(to bottom, transparent 0, transparent 16.4%, rgba(0,0,0,0.06) 21.8%, rgba(0,0,0,0.2) 29.6%, rgba(0,0,0,0.45) 39%, rgba(0,0,0,0.75) 48.4%, #000 57%, #000 100%);
}

/* "Breathing" backgrounds (/start): each section's picture(s) sit in a .bg-breathe box (same size as the section)
   that slowly swells and relaxes — scale 1 -> --breath-scale and a small lift -> --breath-drift over --breath-dur,
   forever, at all times (it no longer holds still while scrolling). The scale never goes below 1, so the picture always
   covers the section. */
:root { --breath-scale: 1.035; --breath-drift: -9.8px; --breath-dur: 17.5s; }
.bg-breathe { position: absolute; inset: 0; z-index: 0; pointer-events: none; animation: bg-breathe var(--breath-dur) ease-in-out infinite; }
@keyframes bg-breathe { 0%, 100% { scale: 1; translate: 0 0; } 50% { scale: var(--breath-scale); translate: 0 var(--breath-drift); } }
@media (prefers-reduced-motion: reduce) { .bg-breathe { animation: none; } }

/* the guided descent (js/descent.js) switches the visitor's own scrolling off while it runs */
html.is-descending, html.is-descending body { overflow: hidden !important; overscroll-behavior: none; }
html:has([data-auto-descent]) { scrollbar-gutter: stable; }   /* no layout jump when scrollbars are switched off */

/* /start "flourishes": thin horizontal rules that help the poem flow. The live site draws them as 1px black Squarespace
   line blocks (no animation); left/top/width are set per rule in the page (% of the section). */
.flourish { position: absolute; height: 1px; background: #000; z-index: 2; pointer-events: none; transform-origin: left center; }
/* A flourish next to a [data-drift] stanza needs to drift WITH it (js/parallax.js writes translate3d onto
   [data-drift] elements) but the flourish itself already owns `transform` for its own reveal (scaleX above) —
   an inline translate3d there would silently replace it. So the drift goes on a plain wrapper instead, and
   the flourish just fills it (0/0/100%, its position now set on the wrapper). */
.flourish-drift { position: absolute; pointer-events: none; }
.flourish-drift .flourish { position: absolute; left: 0; top: 0; width: 100%; }
/* Flourishes animate WITH their section's text (all by growing left to right):
   - plain fade-group sections: draw when the group comes into view (with the text's own fade), 1.6s;
   - [data-line-reveal] sections: on the timeline set in js/reveal.js (data-fl-start / data-fl-after-last / data-dur);
   - [data-scroll-reveal] sections: in step with the scroll, as slices in reading order (js/reveal.js).
   The armed (hidden) state has no transition; only the triggered state animates. */
.fade-group:not([data-line-reveal]):not([data-scroll-reveal]) .flourish:not(.fl-wipe):not(.fl-fade-timed) { transform: scaleX(0); }
.fade-group:not([data-line-reveal]):not([data-scroll-reveal]).in-view .flourish:not(.fl-wipe):not(.fl-fade-timed) { transform: scaleX(1); transition: transform 1.6s cubic-bezier(0.37, 0, 0.63, 1) 0.3s; }
/* a flourish that fades in at a fixed time instead of growing left-to-right with the rest — it's excluded from
   the scaleX rule above entirely (never scaled down), just invisible until its own opacity transition (with a
   delay) brings it in. Whatever positioning transform it already carries inline (e.g. translate3d) is
   untouched — inline always wins over these class rules for `transform` anyway, which is exactly what's
   wanted here (this flourish needs its own translate, not a scaleX). */
.flourish.fl-fade-timed { opacity: 0; transition: none; }
/* Armada section 3's flourish: invisible until the master clock (js/water-reveal.js) adds .wl-fl-go, at its turn in
   the reading order -- after the last line of the first stanza, before the second stanza starts. (It used to fade
   in after a fixed 10.97s counted from the section being 15% on screen, while the poem only starts once the WHOLE
   block is on screen -- so with any scrolling in between it was already "due" the moment the text began.)
   Colour is the photo's own: the blurred picture averaged over the strip the rule sits on (measured off the actual
   image, not guessed) -- rgb(26,37,44), --wl-flourish -- so it reads as part of the water rather than a black bar.
   Opacity 2/3 and the pulsing glow are as before; the glow starts the moment it does. Cycle length 7.7s (was
   6.6s) -- owner: cycle from off to on less, i.e. slower/less frequent pulses; same shape (glow up, then
   fade), just a longer period between them. */
[data-water-reveal] .flourish.fl-fade-timed { background: var(--wl-flourish, #1a252c); }
[data-water-reveal] .flourish.fl-fade-timed.wl-fl-go {
  opacity: 0.667;
  transition: opacity 1.8s ease;
  animation: flourish-glow 7.7s ease-in-out infinite;
  animation-duration: var(--flourish-glow-dur, 20s);   /* see :root -- baked in from the owner's slider */
}
/* --flourish-glow-amt (strength, baked in) / --flourish-glow-dur (cycle length, baked in); slider removed --
   (armada/index.html); browsers don't resample a running animation's used values from an updated custom
   property, so both sliders use data-tune-var-restart to force a restart on every drag tick. */
@keyframes flourish-glow {
  0%, 100% { box-shadow: 0 0 0 rgba(255, 255, 255, 0); }
  50% { box-shadow: 0 0 calc(8px * var(--flourish-glow-amt, 1)) calc(2px * var(--flourish-glow-amt, 1)) rgba(255, 255, 255, calc(0.5 * var(--flourish-glow-amt, 1))); }
}
[data-line-reveal] .flourish.fl-armed { transform: scaleX(0); }
[data-line-reveal] .flourish.fl-armed.fl-go { transform: scaleX(1); transition: transform var(--fl-dur, 2s) cubic-bezier(0.37, 0, 0.63, 1) var(--fl-delay, 0s); }
.flourish.fl-wipe { transform: scaleX(0); }   /* waits for the section's wipe reveal (js/reveal.js), then draws */
.flourish.fl-wipe.fl-go { transform: scaleX(1); transition: transform var(--fl-dur, 2s) cubic-bezier(0.37, 0, 0.63, 1) var(--fl-delay, 0s); }
[data-scroll-reveal] .flourish { will-change: transform; }
@media (prefers-reduced-motion: reduce) { .flourish { transform: none !important; transition: none !important; } }

/* ---------- Experience chapters on phone/tablet ----------
   The desktop art-relative absolute positioning above (calibrated at 1440x900, scattering lines across
   specific coordinates over the photo) doesn't survive text reflow at this width — confirmed directly:
   paragraphs overlapped at 576px, and most lines were already wrapping into 2 rows even at 768px. Owner
   (2026-09-22): mobile/tablet only needs to stay LEGIBLE with flourishes "reasonably spaced" — not a
   pixel-matched miniature of the desktop scatter. So below this breakpoint, stanzas and flourishes give up
   their coordinates and read top-to-bottom in one stacked column instead; parallax, breathing and the
   reveal animations are untouched (none of them depend on this positioning scheme, and the tall inline
   min-height per section is left alone so they keep the same scroll room to play out in).
   position:relative (not static) is deliberate: .stanza/.chapter-mark/.start-intro/.flourish carry no
   z-index of their own — they only paint above the background photo (z-index:0) and its tint (z-index:1)
   because they're a POSITIONED element that comes after them in source order (see the .stanza rule above).
   position:static would drop them out of that paint layer and hide them behind the photo. */
@media (max-width: 1024px) {
  .chapter-hero { flex-direction: column; align-items: flex-start; text-align: left; padding: 6rem 6vw 3rem; gap: 1.2em; }
  .chapter-main .chapter-mark,
  .chapter-main .start-intro,
  .chapter-main .fade-group .stanza {
    position: relative !important; left: auto !important; top: auto !important; right: auto !important; bottom: auto !important;
    width: 100% !important; margin: 0;
  }
  .chapter-main .fade-group {
    /* .section's base rule sets align-items:center for its default ROW layout (vertically centering full-bleed
       content) — overriding to a column here without correcting align-items would keep that centering, but
       now horizontally: invisible for the 100%-wide stanzas above, but it was quietly centering the narrower
       (55%-wide) flourishes instead of keeping them flush left with the stanzas. flex-start fixes both. */
    display: flex; flex-direction: column; align-items: flex-start; justify-content: center; padding: 3rem 6vw; gap: 1.3em;
  }
  .chapter-main .fade-group .flourish,
  .chapter-main .fade-group .flourish-drift {
    position: relative !important; left: auto !important; top: auto !important; right: auto !important; bottom: auto !important;
    width: 55% !important; margin: 0.4em 0;
  }
  /* "i tell myself constantly" section: the flourish after the stanza sits under just "quireme" — from the "q"
     to the last "e" in "requirement", not the whole word — measured at 375px with a text Range on exactly
     those characters: 22.6% of the stanza's own width, ending 7.1% short of its right edge (the line is
     right-aligned, with "nt " trailing after the last "e"). */
  .chapter-main .fade-group .flourish.fl-mob-req { width: 22.6% !important; margin: 0.4em 7.1% 0.4em auto; }
  /* scroll-reveal section: owner wants only ONE opening divider on mobile, not two stacked at the top. */
  .chapter-main .fade-group .flourish.fl-mob-hide { display: none; }
  /* Any flourish that sits next to a text-align:center line (this section's one remaining divider; the
     "i tell myself constantly" section's opening flourish, next to its centred first line) needs centring —
     the general flush-left rule above is only correct where the adjacent text is itself left-aligned. */
  .chapter-main .fade-group .flourish.fl-mob-center { margin-left: auto; margin-right: auto; }
}

/* ---------- store ---------- */
.shop-title {
  font-family: var(--sans-display); font-size: var(--fs-display-xl); font-weight: 300;
  letter-spacing: normal; text-transform: uppercase; text-align: center; margin-top: 9rem;
}
.store-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 2.5rem; max-width: 1100px; margin: 0 auto; padding: 9rem 1.5rem 6rem;
}
.store-card { text-align: center; }
.store-card img { width: 100%; aspect-ratio: 4/5; object-fit: cover; margin-bottom: 1em; }
.store-card h2 {
  font-family: var(--sans-display); font-size: var(--fs-display-sm); font-weight: 300;
  letter-spacing: normal; text-transform: uppercase; margin: 0 0 0.3em;
}
.store-card .price { color: var(--text-muted); font-size: 16px; font-weight: 400; letter-spacing: 0.01em; }

.product-page { max-width: 900px; margin: 0 auto; padding: 9rem 1.5rem 6rem; display: grid; gap: 3rem; grid-template-columns: 1fr; }
@media (min-width: 720px) { .product-page { grid-template-columns: 1fr 1fr; align-items: start; } }
.product-crumb { font-size: 16px; letter-spacing: 0.07em; color: var(--text-dim); margin-bottom: 1.5rem; }
.product-crumb a { color: var(--text-muted); }
.product-gallery img { width: 100%; margin-bottom: 0.75rem; }
.product-info h1 {
  font-family: var(--sans-display); font-size: var(--fs-display-md); font-weight: 300;
  margin: 0 0 0.3em; text-transform: uppercase; letter-spacing: normal;
}
.product-info .price {
  font-family: var(--sans-display); font-size: var(--fs-display-sm); font-weight: 700;
  text-transform: uppercase; color: var(--text-muted); margin: 0 0 1.5em;
}
.product-info p.desc { font-size: 12.5px; letter-spacing: 0.07em; color: var(--text-muted); line-height: 1.8; margin: 0 0 2em; }
.buy-btn {
  display: inline-block; font-family: var(--sans-body); background: var(--text); color: var(--bg);
  padding: 0.9em 2.6em; border-radius: 999px; font-size: 16px; font-weight: 500;
  letter-spacing: 0.07em; text-transform: uppercase;
}
.buy-btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

.placeholder-note {
  border: 1px dashed var(--text-dim); border-radius: 8px; padding: 1em 1.25em;
  font-size: 0.78rem; color: var(--text-dim); margin: 1.5em 0; text-align: left;
}

/* Armada's exit animation (js/portal.js): the fall layer -- fixed, screen-sized, hidden until the trigger. The drop plays
   inside it as transforms (a snowbank copy over the fall composite), never by scrolling the page. Every one of these
   numbers is written directly from JS every frame, never via a CSS transition -- this project has repeatedly found a
   transition sharing an update tick with continuous JS writes to the same element unreliable (see
   js/water-reveal.js's header comment). --fall-start-speed / --fall-end-speed: ?tune=1 sliders -- the scroll's gradual
   speed (0-3.62s) and top speed (6.33s on), in screen-heights per second; owner-set 2026-10-05 (locked, docs/SPECS.md).
   The timeline itself is fixed in js/portal.js. */
:root { --fall-start-speed: 0.87; --fall-end-speed: 9.1; }
.fall-layer { position: fixed; inset: 0; z-index: 9998; overflow: hidden; pointer-events: none; display: none; }
.fall-content { position: absolute; inset: 0; transform-origin: 50% 50%; will-change: transform; }
.fall-strip { position: absolute; left: 0; width: 100%; }
.fall-strip img { display: block; width: 100%; height: auto; }
/* snowbank's copy + the smear below it (its own bottom rows stretched straight down), blurred together as ONE group:
   a CSS blur treats an element's outside as transparent, so blurring them separately left a half-see-through band at
   the seam. The smear fades into the composite beneath; the backplate (an oversized, heavily blurred snowbank behind
   everything) is what shows through the group's outer blur fringe, instead of the crisp page under the layer. */
.fall-snowgroup { position: absolute; inset: 0; will-change: filter; }
.fall-snowbank { position: absolute; max-width: none; }
.fall-smear {
  position: absolute;
  -webkit-mask-image: linear-gradient(to bottom, #000 30%, transparent 100%); mask-image: linear-gradient(to bottom, #000 30%, transparent 100%);
}
.fall-backplate { position: absolute; max-width: none; filter: blur(40px); }
.portal-whiteout {
  position: fixed; inset: 0; z-index: 9999; background: #fff; opacity: 0; pointer-events: none;
}

/* Armada's closing diagonal + snowbank "window", measured off live (Squarespace section divider + background
   parallax). The section above the gate is cut on a straight diagonal 6vw tall -- full height at the left edge,
   rising to (100% - 6vw) at the right -- with a 6px black stroke along the cut. The gate's photo box starts 6vw
   above the gate so it shows through the cut. Gate height is live's Fluid Engine grid: four rows of
   2.15% x min(1383px, 92vw) plus one 30.4px content row. The photo inside the box is sized and moved by
   js/parallax.js (data-parallax on .divider-window), with live's settings for it.
   --window-scale: the owner's deliberate departure from live -- the whole window (diagonal rise AND gate strip)
   scaled together; 1 = live's exact size (owner set 1.2, 2026-10-02). ?tune=1 slider. --snowbank-window-blur: snowbank's
   resting blur in the window, as a % of 16px; the snowbank reveal releases it to 0 (js/portal.js). */
/* --snowbank-window-shift: moves snowbank up (+) or down (-) inside the window, on top of its parallax (js/parallax.js
   reads it via data-shift-var). --snowbank-reveal-rate: how tightly the shutter follows UPP (js/portal.js; higher = tighter).
   Both have ?tune=1 sliders. */
/* owner-set 2026-10-02 (locked, docs/SPECS.md): window size 1.2, snowbank position +10px, window blur 76% (of 16px) */
:root { --window-scale: 1.2; --snowbank-window-shift: 10px; --snowbank-window-blur: 76; --snowbank-reveal-rate: 8; }
.divider-diagonal { z-index: 3; clip-path: polygon(0 0, 100% 0, 100% calc(100% - 6vw * var(--window-scale)), 0 100%); }
[data-portal-gate] {
  min-height: 0; height: calc((0.086 * min(1383px, 92vw) + 30.4px) * var(--window-scale)); overflow: visible;
}
.divider-window { position: absolute; left: 0; right: 0; top: calc(-6vw * var(--window-scale)); bottom: 0; overflow: hidden; pointer-events: none; }
.divider-stroke {
  position: absolute; left: 0; top: calc(-6vw * var(--window-scale)); width: 100%; height: calc(6vw * var(--window-scale)); overflow: visible; z-index: 4; pointer-events: none;
}
.divider-stroke line { stroke: #000; stroke-width: 6px; stroke-linecap: square; }
[data-portal-gate] .next-link { z-index: 5; }

/* ---------- music floater (js/floater.js; docs/SPECS.md FLOAT-*) ----------
   Bare controls spanning the page directly above the site title on the Experience pages: play/pause, the seek line
   (filling the rest of the width), a short vertical playhead. Owner, 2026-10-06: full page width, 24px tall, and no
   background at all -- the glass pill was dropped ("just keep the elements themselves"). Everything is a multiple of --floater-h (the pill's height; ?tune=1 "Music
   floater height"), so the slider scales icons, line and playhead together. Nothing jagged: rounded icon joins,
   round-capped lines. No shadow, matching the nav, which sits on the same photos. The header makes room above the title only when the pill
   wouldn't otherwise fit in its top padding, so the title stays put at the default size. */
:root { --floater-h: 29px; --floater-side: 12px; --floater-y: 9px; --floater-played: 0.6; --floater-unplayed: 0.03; }   /* owner, 2026-10-06 (locked): height; side = gap between each page edge and the visible ends (play icon / end of the line); y = vertical nudge from the default spot (+ = down); the line's white opacity before / after the playhead */
body.has-floater { --fl-top: max(2rem, calc(var(--floater-h) + 12px)); }
@media (max-width: 720px) { body.has-floater { --fl-top: max(1.25rem, calc(var(--floater-h) + 12px)); } }
body.has-floater .site-header { padding-top: var(--fl-top); }
.floater {
  --h: var(--floater-h);
  position: absolute; z-index: 21;
  left: var(--floater-side); right: var(--floater-side); top: calc(var(--fl-top) - 6px - var(--h) + var(--floater-y));
  height: var(--h);
  display: flex; align-items: center; gap: calc(var(--h) * 0.18);
  color: rgba(255, 255, 255, 0.86);
  opacity: 0; transition: opacity 1.2s ease;
  -webkit-tap-highlight-color: transparent;
  user-select: none; -webkit-user-select: none;
}
.floater.is-in { opacity: 1; }
.floater-play {
  flex: none; width: var(--h); height: var(--h); padding: 0; border: 0; background: none;
  margin: 0 0 0 calc(var(--h) * -0.415);   /* the play triangle's own inset in its button (icon 0.6h centred = 0.2h, + the path's 8.6/24 of the icon), so the visible left end sits exactly on the side margin, like the line's right end */
  display: grid; place-items: center; color: inherit; cursor: pointer; border-radius: 3px;
}
.floater-play svg { width: calc(var(--h) * 0.6); height: calc(var(--h) * 0.6); display: block; fill: currentColor; shape-rendering: geometricPrecision; opacity: 0.86; transition: opacity 0.25s ease; }
.floater-play:hover svg { opacity: 1; }
.floater-seek {
  position: relative; flex: 1 1 auto; min-width: 0; height: 100%;
  display: flex; align-items: center; cursor: pointer; touch-action: none; outline: none;
}
.floater-track {
  position: relative; width: 100%; height: max(1px, calc(var(--h) * 0.085));
  border-radius: 999px; overflow: hidden;
}
/* the two parts of the line meet at the playhead and never overlap, so each shows exactly its own opacity */
.floater-fill { position: absolute; inset: 0; background: rgba(255, 255, 255, var(--floater-played)); transform-origin: left center; transform: scaleX(0); }
.floater-rest { position: absolute; inset: 0; background: rgba(255, 255, 255, var(--floater-unplayed)); transform-origin: right center; transform: scaleX(1); }
.floater-head {
  position: absolute; top: 50%; left: 0;
  width: max(1.5px, calc(var(--h) * 0.09)); height: calc(var(--h) * 0.56);
  margin: calc(var(--h) * -0.28) 0 0 calc(max(1.5px, calc(var(--h) * 0.09)) / -2);
  border-radius: 999px; background: rgba(255, 255, 255, 0.92);
  will-change: transform;
}
.floater-play:focus-visible, .floater-seek:focus-visible { box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.45); border-radius: 3px; }
@media (prefers-reduced-motion: reduce) { .floater { transition: none; } }
