/* ═══════════════════════════════════════════════════════════════════════════
   MODERN JAZZ GUITAR BOOKS
   Design system — "paper + brass"

   Keeps the WalkNessebar site's architecture (alternating bands, classical
   serif headings over a system sans, generous vertical rhythm, scroll-reveal)
   but runs it LIGHT: white and warm off-white grounds, near-black text, brass
   as the single accent. A publisher's catalog should feel like fine printed
   paper, and white lets 323 pieces of cover art carry the colour instead of
   competing with a dark ground.

   Two deliberate dark bands remain — the press quotes and the footer. They give
   the long scroll a rest point and a base, the way a well-set book uses a plate
   section. Everything else is paper.

   No web fonts are loaded. The serif stack resolves to genuinely beautiful
   system faces (Cochin / Hoefler Text on Apple, Palatino / Georgia elsewhere),
   keeping first paint instant — which matters when the catalog page has to
   render hundreds of covers.
   ═════════════════════════════════════════════════════════════════════════ */

:root {
  /* paper — the primary ground */
  --paper:   #ffffff;
  --paper-2: #f7f6f3;   /* warmed a touch so it reads as paper, not grey screen */
  --paper-3: #eeebe5;
  --line:    #e4e0d8;   /* hairlines and card borders */

  /* ink — text and the two dark bands */
  --ink:    #101012;
  --ink-2:  #1a1a1e;
  --ink-3:  #26262c;

  /* brass — the single accent */
  --brass:     #b8873c;  /* the working tone on white: rich but passes contrast */
  --brass-lit: #d4a052;  /* highlights, gradients, dark-band text */
  --brass-dim: #8f6828;
  --ember:     #c0562a;

  /* the two extra print plates for the hero drawing */
  --plate-blue: #3f6fa8;
  --plate-gold: #c79a45;

  /* A brighter orange than the brass, for the one marker that has to survive
     every band on the site — the dock's arrow floats over paper, warm grey and
     the near-black plate sections alike. Picked for BOTH ends rather than tuned
     for one: it is the brightest orange that still clears 3:1 on white (the
     WCAG floor for a UI marker) while holding 6.3:1 on ink. The lit tone is
     hover only, where dipping below the floor is fine — the cursor is already
     on it. */
  --signal:     #e07a00;
  --signal-lit: #ff8c14;

  --text-dark:  #131316;
  --text-mid:   #5f5f66;
  --text-soft:  #83838c;
  --text-light: rgba(255, 255, 255, .76);
  --text-faint: rgba(255, 255, 255, .52);

  --serif: "Cochin", "Hoefler Text", "New York", "Iowan Old Style",
           "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  --sans:  -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text",
           "Helvetica Neue", "Segoe UI", Roboto, Arial, sans-serif;
  /* Display face for the wordmark only. A Didone — very high stroke contrast,
     vertical stress, fine hairline serifs — is the classic publisher's imprint
     letter, and reading it against the softer old-style body serif is what makes
     the mark feel like a logo rather than just more text. */
  --display: "Didot", "Bodoni 72", "Bodoni MT", "Playfair Display",
             "Hoefler Text", Didot, Georgia, serif;

  --wrap:  1120px;
  --nav-h: 58px;
  --pad:   clamp(22px, 5vw, 40px);
  --sec-y: clamp(84px, 12vw, 168px);

  --ease: cubic-bezier(.22, .61, .36, 1);
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 12px);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--text-dark);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.55;
  letter-spacing: -.01em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

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

::selection { background: rgba(184, 135, 60, .22); }

:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ───────────────────────────────────────────────────────────────── layout ── */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad);
}
.wrap--wide { max-width: 1320px; }

.sec {
  position: relative;
  padding-block: var(--sec-y);
  overflow: hidden;
}
.sec--paper { background: var(--paper); }
.sec--gray  { background: var(--paper-2); }
.sec--dark  { background: var(--ink);   color: #fff; }
.sec--dark h1, .sec--dark h2, .sec--dark h3 { color: #fff; }
.sec--dark p { color: var(--text-light); }
.sec--tight { padding-block: clamp(56px, 8vw, 104px); }

.center { text-align: center; }
.center .lede { margin-inline: auto; }

/* ─────────────────────────────────────────────────────────────────── type ── */

h1, h2, h3 { margin: 0; font-weight: 400; letter-spacing: -.015em; color: var(--text-dark); }

.eyebrow {
  margin: 0 0 20px;
  font-size: clamp(11px, 1.1vw, 12px);
  font-weight: 600;
  letter-spacing: .19em;
  text-transform: uppercase;
  color: var(--brass);
}
.sec--dark .eyebrow { color: var(--brass-lit); }

.h1 {
  font-family: var(--serif);
  /* bound by viewport height too, so the hero composition survives a laptop */
  font-size: clamp(34px, min(5.7vw, 8.6vh), 76px);
  line-height: 1.04;
  letter-spacing: -.022em;
}
.h2 {
  font-family: var(--serif);
  font-size: clamp(31px, 4.4vw, 58px);
  line-height: 1.08;
  letter-spacing: -.018em;
  text-wrap: balance;
}
.h3 {
  font-family: var(--serif);
  font-size: clamp(23px, 2.7vw, 34px);
  line-height: 1.15;
  text-wrap: balance;
}
.h4 { margin: 0 0 8px; font-size: 17px; font-weight: 600; letter-spacing: -.01em; }

.lede {
  margin: 22px 0 0;
  font-size: clamp(17px, 1.55vw, 21px);
  line-height: 1.52;
  color: var(--text-mid);
  max-width: 42ch;
}
.sec--dark .lede { color: var(--text-light); }
.lede--wide { max-width: 58ch; }

.brass { color: var(--brass); }
.sec--dark .brass { color: var(--brass-lit); }

.serif-i { font-family: var(--serif); font-style: italic; }

.rule { width: 46px; height: 1px; margin: 26px 0 0; background: var(--brass); opacity: .75; }
.center .rule { margin-inline: auto; }

/* ─────────────────────────────────────────────────────────────── buttons ── */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 13px 26px;
  border: 0;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.01em;
  cursor: pointer;
  transition: transform .3s var(--ease), background .3s var(--ease),
              box-shadow .3s var(--ease), border-color .3s var(--ease),
              color .3s var(--ease);
}
.btn--brass {
  background: var(--brass);
  color: #fff;
  box-shadow: 0 4px 14px rgba(184, 135, 60, .26);
}
.btn--brass:hover {
  background: var(--brass-dim);
  transform: translateY(-2px);
  box-shadow: 0 8px 22px rgba(184, 135, 60, .32);
}

.btn--ghost {
  background: transparent;
  color: var(--text-dark);
  border: 1px solid var(--line);
}
.btn--ghost:hover { border-color: var(--brass); color: var(--brass); transform: translateY(-2px); }

.sec--dark .btn--ghost { color: #fff; border-color: rgba(255, 255, 255, .28); }
.sec--dark .btn--ghost:hover { border-color: var(--brass-lit); color: var(--brass-lit); }

.btns { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }
/* Both forms: the button row nested INSIDE a centred block, and the common
   `class="center btns"` where the two land on the same element. A descendant
   selector alone silently misses the second, which left every "View all…"
   button on the page hard against the left margin. */
.center .btns,
.center.btns { justify-content: center; }

/* ─────────────────────────────────────────────────────────────────── nav ── */

.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 60;
  height: var(--nav-h);
  background: rgba(255, 255, 255, .82);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  border-bottom: 1px solid transparent;
  transition: background .4s var(--ease), border-color .4s var(--ease);
}
/* added by site.js once the page scrolls past the hero */
.nav--solid {
  background: rgba(255, 255, 255, .94);
  border-bottom-color: var(--line);
}
.nav__in {
  max-width: 1320px;
  height: 100%;
  margin-inline: auto;
  padding-inline: var(--pad);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand__mark { width: 28px; height: 28px; display: grid; place-items: center; flex: none; }
/* On paper the drawing needs no inversion — it is already black line art on
   white. Only the mint blocks are dropped, so the mark stays disciplined
   against the brass rather than introducing a third hue. */
.brand__mark img { width: 100%; filter: grayscale(1) contrast(1.1); }
/* THE IMPRINT
   Set in the Didone, all caps, widely tracked — the classic fine-press
   masthead. Three things make it work rather than merely be uppercase:

   · Generous tracking (.2em). Caps have no ascenders or descenders to create
     rhythm, so set tight they lock into an unreadable wall. Tracking is what
     turns capitals into an imprint instead of shouting.
   · A SMALLER size than the mixed-case version it replaces. Capitals read
     visually larger at the same point size, so 21px lowercase and 21px caps
     are not the same object — caps at 21px would dominate the whole bar.
   · A negative right margin equal to the tracking. Letter-spacing adds space
     AFTER the final letter too, which pushes the wordmark visibly off-centre
     against the hand mark. Trimming it re-optically-centres the pair. */
.brand__word {
  font-family: var(--display);
  font-size: 17px;
  line-height: 1;
  letter-spacing: .2em;
  margin-right: -.2em;
  text-transform: uppercase;
  color: var(--text-dark);
  white-space: nowrap;
}
/* Below ~560px the full imprint crowds the burger, so it steps down rather
   than wrapping or colliding. Tracking eases with it — the same .2em that
   gives it presence at 15px would push it off-screen at small sizes. */
@media (max-width: 560px) {
  .brand__word { font-size: 12.5px; letter-spacing: .14em; margin-right: -.14em; }
  .brand__mark { width: 23px; height: 23px; }
}
.nav__links { display: flex; align-items: center; gap: 26px; }
.nav__links a {
  font-size: 14px;
  font-weight: 500;
  color: var(--text-mid);
  /* Without this, "Areas of Study" breaks over two lines as soon as the bar
     tightens, which doubles the nav's height and shunts the whole page down. */
  white-space: nowrap;
  transition: color .25s var(--ease);
}
.nav__links a:hover { color: var(--text-dark); }
.nav__cta {
  padding: 8px 17px;
  border-radius: 999px;
  background: var(--brass);
  color: #fff !important;
  font-weight: 600;
}
.nav__cta:hover { background: var(--brass-dim); }

/* The social pair. Tighter than the text links beside them — icons at the same
   26px spacing read as two more menu items rather than as a set. */
.nav__social { display: flex; align-items: center; gap: 2px; margin-left: -12px; }
.nav__social a {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  color: var(--text-mid);
  transition: color .25s var(--ease), background .25s var(--ease);
}
.nav__social a:hover { color: var(--brass); background: rgba(184, 135, 60, .10); }
.nav__social svg { width: 18px; height: 18px; display: block; }
/* YouTube runs a touch larger — its glyph is a wide, short rectangle, so at the
   same box size it reads smaller than the Instagram square beside it. */
.nav__social a[data-net="youtube"] svg { width: 22px; height: 22px; }
/* and the play triangle keeps YouTube's red. A CSS fill beats the presentation
   attribute on the path, so it holds through the hover colour change too. */
.nav__social .yt-play { fill: #f00; }

.nav__burger {
  display: none;
  width: 40px; height: 40px;
  background: none; border: 0;
  cursor: pointer;
  flex-direction: column; justify-content: center; align-items: center; gap: 5px;
}
.nav__burger span { width: 20px; height: 1.5px; background: var(--text-dark); border-radius: 2px; }

.menu {
  position: fixed;
  inset: var(--nav-h) 0 auto;
  z-index: 55;
  display: none;
  flex-direction: column;
  padding: 18px var(--pad) 26px;
  background: rgba(255, 255, 255, .98);
  backdrop-filter: blur(18px);
  border-bottom: 1px solid var(--line);
}
.menu.open { display: flex; }
.menu a { padding: 13px 0; font-size: 17px; color: var(--text-dark); border-bottom: 1px solid var(--line); }
/* Instagram and YouTube wear their mark in the panel. The row keeps its full
   width and its 13px padding, so the touch target is the whole line — the icon
   is a label, not the button. */
.menu__soc { display: flex; align-items: center; gap: 11px; }
/* A FIXED SLOT, wider than either glyph, rather than each icon at its own width.
   YouTube's mark is a wide, short rectangle and Instagram's is a square, so
   sized to fit they came out 21px and 25px — and the two words then started at
   different x, a ragged edge running down a column of otherwise flush text.
   The viewBox letterboxes inside the slot, so both marks draw at the same height
   and centre themselves in the same gutter. */
.menu__soc svg {
  flex: 0 0 26px;
  width: 26px; height: 22px;
  display: block;
  color: var(--text-mid);
}
.menu__soc .yt-play { fill: #f00; }

/* Raised from 860px: the tracked all-caps imprint is materially wider than the
   mixed-case wordmark it replaced, so the links now start crowding it well
   before the old breakpoint. Measured — at 888px they were already wrapping. */
@media (max-width: 1020px) {
  .nav__links { display: none; }
  .nav__burger { display: flex; }
}

/* ────────────────────────────────────────────────────────────────── hero ── */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding-block: calc(var(--nav-h) + 26px) 60px;
  background: var(--paper);
  overflow: hidden;
}
/* a barely-there warm wash, so the white has depth rather than reading flat */
.hero__glow {
  position: absolute;
  left: 50%; top: 44%;
  width: min(1250px, 132vw); aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle,
              rgba(184, 135, 60, .10) 0%,
              rgba(184, 135, 60, .04) 36%,
              transparent 68%);
  pointer-events: none;
}
.hero__vignette { display: none; }   /* not needed on paper */
.hero__in { position: relative; z-index: 2; }

.hero .lede { max-width: 52ch; }

/* No text halos here. They were needed when the drawing ran at full strength;
   with it faded back the type sits cleanly on paper, and halos would only make
   the letterforms look slightly soft for no gain. */

/* The hand drawing: faded well back so the headline always wins, and turning
   very slowly so the hero feels alive rather than static.

   It turns about its VERTICAL axis (rotateY), so it reads as a drawing on a
   sheet pivoting in space rather than a flat spin. The `perspective()` is part
   of the transform chain and must sit BEFORE rotateY to have any effect —
   without it the rotation flattens to a meaningless horizontal squash.

   Twice per revolution the sheet passes edge-on and thins to nothing; that
   pause is the effect, not a glitch. At 45s a full turn takes about as long as
   reading the hero, so a visitor sees the drawing open, close and open again
   without it ever becoming the thing they are watching.

   The rotation is composed INTO the centring transform rather than layered on
   top of it — a `transform` in the keyframes replaces the element's transform
   outright, so omitting the translate here would fling the drawing off-centre
   the moment the animation began. */
/* The drawing is now a subject, not a watermark: full strength, sitting in the
   flow directly above the headline so the turn is plainly visible.

   The `min()` third term is the load-bearing one. The hand and the copy now
   stack, so together they must fit one viewport height. Sizing against svh as
   well as width keeps the composition intact on a short laptop, where a purely
   width-based size would push the buttons below the fold. */
.hero__handwrap {
  width: min(248px, 48vw, 24svh);
  margin: 0 auto clamp(6px, 1vh, 14px);
  perspective: 1500px;            /* on the parent, so the child turns in depth */
}

/* ── the hand drawing itself ─────────────────────────────────────────────────
   181 pen strokes, traced from the original scan down to their centrelines,
   each revealed by pulling its dash offset back to zero.

   Every path carries pathLength="1000", which re-scales its dash maths to a
   fixed 1000 units no matter its real length. That is what lets one CSS rule
   serve all 181 strokes: the dash numbers are identical everywhere, while the
   per-stroke --t0 and --dur (baked in by trace_hand.py, proportional to each
   stroke's true length) supply constant pen speed.

   `forwards` matters — without it every stroke would snap back to invisible the
   instant its animation ended, and the drawing would erase itself. */
/* Three plates stacked in register — blue, brass, ink — each nudged a few px
   off the others so the drawing reads like a screen print whose colours never
   quite line up. The wrapper takes the artwork's own proportions because every
   plate inside it is absolutely positioned and so contributes no height. */
.handline-wrap {
  position: relative;
  aspect-ratio: 1100 / 1416;
  line-height: 0;
}
.handplate {
  position: absolute;
  inset: 0;
  transform: translate(var(--dx, 0), var(--dy, 0));
  color: var(--plate, var(--text-dark));  /* paths stroke with currentColor */
  /* Multiply keeps the overlaps honest: where two plates cross, the colours
     darken into each other the way wet ink does, instead of the topmost plate
     simply hiding the ones beneath. */
  mix-blend-mode: multiply;
}
.handline {
  width: 100%;
  height: auto;
  overflow: visible;              /* round caps can sit a hair outside the box */
}
.handline path {
  /* Set here, not in the file, so line weight is tunable without re-tracing.
     The viewBox is 1100 wide but the drawing displays around 250px, so
     everything inside is scaled to roughly a fifth — a stroke-width that looks
     right in the SVG's own coordinates renders as a hairline on the page.
     5 user-units lands at about 1.1px on screen, matching the pen weight of
     Hristo's original at this size. */
  stroke-width: 5;
  /* 1010, not 1000 — deliberately 1% LONGER than the normalised path.

     At exactly 1000/1000 the dash boundary sits precisely on the path's end,
     and whether a waiting stroke is fully hidden comes down to floating-point
     rounding. Chrome rounds into the gap; Safari rounds a hair past it, and
     with `stroke-linecap: round` every leaked sub-pixel sliver is drawn as a
     little DOT — so the undrawn hand showed a constellation of dots along
     where the pen was going to go. The 1% overshoot parks the boundary safely
     beyond the path on every engine; once drawn (offset 0) the overshoot is
     invisible, and the 1% head start it gives each stroke is imperceptible. */
  stroke-dasharray: 1010;
  stroke-dashoffset: 1010;
  /* --t0 is the stroke's own place in the drawing order; --lag offsets the
     whole plate, so blue leads, brass follows and ink lands last. */
  animation: draw-stroke var(--dur, .3s) linear
             calc(var(--t0, 0s) + var(--lag, 0s)) forwards;
}
@keyframes draw-stroke {
  to { stroke-dashoffset: 0; }
}

/* (The white pool that used to sit behind the copy is gone: the drawing and
   the type no longer overlap, so it would only be a pale smudge.) */

.hero__scroll {
  position: absolute;
  left: 50%; bottom: 26px;
  transform: translateX(-50%);
  z-index: 2;
  font-size: 11px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-soft);
  animation: bob 2.6s var(--ease) infinite;
}
@keyframes bob { 0%,100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, 7px); } }

/* ────────────────────────────────────────────────────────────── stat band ── */

.stats {
  display: grid;
  /* Driven by the number of figures actually present rather than hard-coded to
     three: dropping one from the markup should re-centre the rest, not leave a
     gap where it used to be. */
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 20px;
  text-align: center;
}
.stat__n {
  font-family: var(--serif);
  font-size: clamp(30px, 4vw, 48px);
  line-height: 1;
  color: var(--brass);
}
.stat__l {
  margin-top: 9px;
  font-size: 12px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.sec--dark .stat__l { color: var(--text-faint); }
@media (max-width: 620px) {
  .stats { grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); gap: 34px 20px; }
}

/* ── THE DISPLAY ──────────────────────────────────────────────────────────
   A masterclass playing on a phone, with a fanned row of books standing in
   front of it — one from each of four different areas of study. The
   WalkNessebar composition translated to a bookseller's window: the lit screen
   behind, the physical objects in front of it, everything breathing slightly.

   Three nested elements per object, three separate transforms — the reveal
   offset, the float, and each book's own lean. Stacking any two onto one
   element means the later simply overwrites the earlier. */

.display {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: clamp(46px, 6vw, 86px);
}
/* The books stand clear of the screen rather than in front of it: the phone is
   the largest object here and nothing should cover any part of what is playing
   on it. They still read as one scene because they share a centre line and a
   ground. */
.display__phone { position: relative; z-index: 1; }
.display__books {
  position: relative;
  z-index: 2;
  margin-top: clamp(18px, 2.4vw, 34px);
}
/* the group's own soft ground, tying the objects to one surface */
.display::after {
  content: "";
  position: absolute;
  left: 50%; bottom: -10px;
  transform: translateX(-50%);
  width: min(620px, 86%); height: 30px;
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(28, 24, 16, .13), transparent 72%);
  pointer-events: none;
}

/* One base measure for the whole stack; every book's width and every overlap is
   a multiple of it. Overlap must NOT be a percentage: a percentage margin
   resolves against the CONTAINING BLOCK, not the element, so each book would be
   pulled by a fraction of the stack's own width — which the negative margins
   are themselves shrinking. The books collapse into one pile. */
.display__stack {
  --base: clamp(104px, 13vw, 176px);
  display: flex;
  align-items: flex-end;
}
.display__book {
  position: relative;
  z-index: var(--z);
  /* Sized, not scaled. A scale() would shrink the spine and the shadow with the
     cover, and a small book wearing a large book's blur reads as a sticker. */
  width: calc(var(--sc) * var(--base));
  margin-left: calc(var(--ov, 0) * var(--base));
  /* --bk lifts the book off the shared ground line — the further back it stands,
     the higher it sits. Done here rather than with align-items so the lean still
     pivots on the book's own bottom edge. */
  transform: translateY(calc(var(--bk, 0) * var(--base) * -1)) rotate(var(--rot));
  transform-origin: bottom center;
}
/* DEPTH-GRADED SHADOWS. The book in front throws a long shadow across the one
   behind it; each one further back throws a shorter, fainter one. --sh scales
   both the offset and the blur together, because a shadow that moves without
   spreading reads as a second copy of the book rather than as distance.

   Three classes deep on purpose: `.book--sm .book__face` further down the file
   carries its own shadow, and at equal specificity the later rule would win. */
.display__book .book .book__face {
  box-shadow:
    calc( 1px * var(--sh, 1)) calc( 1px * var(--sh, 1)) calc( 3px * var(--sh, 1)) rgba(28, 24, 16, .30),
    calc(13px * var(--sh, 1)) calc(12px * var(--sh, 1)) calc(24px * var(--sh, 1)) rgba(28, 24, 16, .26),
    calc(28px * var(--sh, 1)) calc(26px * var(--sh, 1)) calc(52px * var(--sh, 1)) rgba(28, 24, 16, .17);
}
.display__phone .device--land { width: clamp(300px, 42vw, 580px); }

/* Two different periods, deliberately not multiples of each other, so the pair
   never settles into moving as one object. */
.float   { animation: float-y 8.4s ease-in-out infinite; }
.float--b { animation-duration: 10.6s; animation-delay: -3.2s; }
@keyframes float-y {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-9px); }
}

@media (max-width: 560px) {
  .display__stack { --base: 26vw; }
  .display__phone .device--land { width: 88vw; }
  .display__books { margin-top: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .float { animation: none; }
}

/* ───────────────────────────────────────────────────────────── 3-D books ── */

/* A book is built from three faces: the cover art, a spine hinged on the left,
   and a sliver of page edges on the right. On paper the shadow does the heavy
   lifting — it has to be soft and warm-grey, never black, or the books look
   pasted onto the page instead of resting on it. */
.shelf {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: clamp(30px, 4.5vw, 58px);
}
/* Perspective sits on each item rather than on .shelf: CSS perspective only
   applies to a parent's DIRECT children, and .book is a grandchild of .shelf.
   Per-item perspective also keeps the outermost books in a wide row from
   skewing away from the viewer, which one shared vanishing point would do. */
.book-item { display: block; perspective: 1600px; }

.book {
  position: relative;
  transform-style: preserve-3d;
  transform: rotateY(-19deg) rotateX(2deg);
  transition: transform .55s var(--ease);
  /* NO `will-change: transform` here — it used to be, and it is what emptied
     the carousels in Safari. will-change forces every book onto its own GPU
     layer; with ~320 books a page (156 of them inside tracks that are
     themselves animating) Safari runs past its compositing-memory budget and
     starts silently DROPPING layers — whole runs of covers vanish, then
     flicker back as memory frees. Chrome degrades gracefully; Safari does not.
     The hint bought nothing anyway: hover promotion happens on demand, and a
     one-frame rasterise at hover start is invisible. If hover ever stutters,
     the fix is will-change on :hover only — never statically, never on all. */
}
/* Safari insurance for the same class of bug: with preserve-3d and rotated
   faces, Safari sometimes mis-culls a plate mid-composite and blinks it out.
   No face here ever shows its back, so declaring that is free. */
.book__face, .book__spine, .book__pages {
  -webkit-backface-visibility: hidden;
          backface-visibility: hidden;
}
.book:hover { transform: rotateY(-7deg) rotateX(1deg) translateY(-8px); }

/* THE CAST SHADOW, used by every book on the site.

   DIRECTIONAL — thrown down and to the RIGHT, because the raking highlight on
   every cover face comes from the upper left. A symmetric shadow under a lit
   object reads as no light source at all; this one reads as a solid thing on a
   surface. The treatment started in the series fans and is now the house style,
   so the same book looks like the same object wherever it appears.

   Three layers doing three different jobs:
     · a tight contact shadow, so the book meets the surface
     · a mid cast, which is what actually gives it weight
     · a wide soft cast, grounding it in the page */
.book__face {
  position: relative;
  /* THE COVER'S OWN SHAPE, passed in per book as --ar.

     The artwork is not 3:4. Nearly all of it is 1005x1422 — 1:sqrt(2), the
     A-series paper ratio — and forcing that into a 3:4 box with object-fit
     `cover` sliced 5.7% off the top and bottom of 312 of the 323 covers, which
     is why titles and artwork looked clipped. The fallback is that dominant
     shape rather than 3:4, so a book with no recorded size is still close. */
  aspect-ratio: var(--ar, 1005 / 1422);
  border-radius: 2px 6px 6px 2px;
  overflow: hidden;
  background: var(--paper-3);
  box-shadow:
     1px  1px  3px rgba(28, 24, 16, .30),
    13px 12px 24px rgba(28, 24, 16, .24),
    28px 26px 52px rgba(28, 24, 16, .16);
}
.book__face img { width: 100%; height: 100%; object-fit: cover; }
/* light raking across the cover from the left, as if from a lamp */
.book__face::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(100deg,
              rgba(255, 255, 255, .20) 0%,
              rgba(255, 255, 255, .05) 14%,
              transparent 34%,
              rgba(60, 50, 34, .10) 100%);
  pointer-events: none;
}
/* spine: rotated out from the left edge so it catches its own shading */
.book__spine {
  position: absolute;
  left: 0; top: 0;
  width: 18px; height: 100%;
  transform: translateX(-17px) rotateY(-74deg);
  transform-origin: right center;
  background: linear-gradient(90deg, rgba(90, 80, 62, 0), #6d6252 42%, #b4a894 100%);
  border-radius: 2px 0 0 2px;
}
/* page edges: a warm paper sliver on the fore-edge */
.book__pages {
  position: absolute;
  right: 0; top: 1.5%;
  width: 11px; height: 97%;
  transform: translateX(10px) rotateY(74deg);
  transform-origin: left center;
  background: repeating-linear-gradient(90deg, #f6f3ec 0 1px, #ddd8cb 1px 2px);
  border-radius: 0 2px 2px 0;
}

.book__meta { margin-top: 22px; }
.book__title {
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.28;
  color: var(--text-dark);
  text-wrap: balance;
}
.sec--dark .book__title { color: #fff; }
.book__sub { margin-top: 7px; font-size: 13px; color: var(--text-soft); }

.tag-free {
  display: inline-block;
  margin-left: 7px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(184, 135, 60, .12);
  border: 1px solid rgba(184, 135, 60, .38);
  color: var(--brass-dim);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  vertical-align: 2px;
}

/* ─────────────────────────────────────────────────────────── area cards ── */

.areas { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(18px, 2.6vw, 30px); }
@media (max-width: 820px) { .areas { grid-template-columns: 1fr; } }

.area {
  position: relative;
  display: block;
  padding: clamp(30px, 4vw, 48px);
  border-radius: 18px;
  background: var(--paper);
  border: 1px solid var(--line);
  overflow: hidden;
  transition: transform .45s var(--ease), border-color .45s var(--ease),
              box-shadow .45s var(--ease);
}
.area:hover {
  transform: translateY(-5px);
  border-color: rgba(184, 135, 60, .5);
  box-shadow: 0 18px 40px rgba(28, 24, 16, .09);
}
.area::before {
  content: "";
  position: absolute;
  right: -70px; top: -70px;
  width: 260px; aspect-ratio: 1;
  background: radial-gradient(circle, rgba(184, 135, 60, .09), transparent 68%);
  pointer-events: none;
}
.area__n { font-family: var(--serif); font-size: 13px; color: var(--brass); letter-spacing: .06em; }
.area h3 { margin: 12px 0 14px; }
.area p { font-size: 15.5px; color: var(--text-mid); margin: 0; }
.area__go { margin-top: 22px; display: inline-flex; align-items: center; gap: 7px; font-size: 14px; font-weight: 600; color: var(--brass); }
.area__sub { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 7px; }
.area__sub span {
  padding: 5px 11px;
  border-radius: 999px;
  background: var(--paper-2);
  border: 1px solid var(--line);
  font-size: 11.5px;
  letter-spacing: .04em;
  color: var(--text-mid);
}

/* ──────────────────────────────────────────────────────────────── quotes ── */

.quotes { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: clamp(24px, 3.4vw, 44px); }
.quote p {
  font-family: var(--serif);
  font-size: clamp(18px, 1.75vw, 22px);
  line-height: 1.42;
  font-style: italic;
  color: #fff;
  margin: 0;
}
.quote cite {
  display: block;
  margin-top: 16px;
  font-family: var(--sans);
  font-style: normal;
  font-size: 11.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--brass-lit);
}

/* ───────────────────────────────────────────────────────────── lyra band ── */

.lyra { display: grid; grid-template-columns: 1fr 1fr; align-items: center; gap: clamp(30px, 5vw, 70px); }
@media (max-width: 820px) { .lyra { grid-template-columns: 1fr; } }
.lyra__art {
  padding: clamp(26px, 4vw, 46px);
  border-radius: 20px;
  background: var(--paper-2);
  border: 1px solid var(--line);
}

/* ──────────────────────────────────────────────────────────────── footer ── */

.foot {
  background: var(--ink);
  padding-block: clamp(46px, 6vw, 74px);
  color: var(--text-faint);
  font-size: 14px;
}
/* The brand lockup is a flex row everywhere — but `.foot a { display: block }`
   below is more specific than `.brand`, so in the footer it was quietly
   collapsing to a block: the mark stacked on top of the wordmark with 4px
   between them, touching. Restored, and given its own full-width row on narrow
   screens, where half a two-column grid cannot hold the tracked capitals. */
.foot .brand { display: flex; }
.foot .brand__word { color: #fff; }
/* the footer is the one place the mark sits on ink, so it inverts back */
.foot .brand__mark img { filter: grayscale(1) invert(1) contrast(1.15); mix-blend-mode: screen; }
.foot__grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr; gap: clamp(26px, 4vw, 50px); }
@media (max-width: 760px) {
  .foot__grid { grid-template-columns: 1fr 1fr; }
  /* Half a column is 237px at 375px wide; the lockup needs 272px. Its own row
     rather than a shrunken wordmark — the tracking is what makes it an imprint. */
  .foot__grid > :first-child { grid-column: 1 / -1; }
}
.foot h5 {
  margin: 0 0 14px;
  font-size: 11.5px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: rgba(255, 255, 255, .86);
}
.foot a { display: block; padding: 5px 0; color: var(--text-faint); transition: color .25s var(--ease); }
.foot a:hover { color: var(--brass-lit); }
/* The address under the imprint — written out, because "Contact" hides the
   thing people actually want to copy. Brass at rest, unlike the grey utility
   links around it: it is the one line here that is an invitation.
   `.foot a.foot__mail`, because the plain class loses to `.foot a` above. */
.foot a.foot__mail { margin-top: 12px; color: var(--brass); letter-spacing: .02em; }
.foot__base {
  margin-top: clamp(30px, 4vw, 48px);
  padding-top: 22px;
  border-top: 1px solid rgba(255, 255, 255, .1);
  display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between;
  font-size: 12.5px;
}

/* ────────────────────────────────────────────────────────── scroll reveal ── */

.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .85s var(--ease), transform .85s var(--ease);
  transition-delay: calc(var(--d, 0) * 105ms);
}
.reveal.in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { opacity: 1; transform: none; transition: none; }
  .hero__scroll { animation: none; }
  .book, .area, .btn { transition: none; }
  /* Stop the turn and leave the drawing face-on. It sits in normal flow now,
     so clearing the transform is all that is needed. */
  /* Show the completed drawing at once rather than animating the pen. */
  .handline path { animation: none; stroke-dashoffset: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   BOOK VARIANTS — one component, many presentations

   The 3-D book (cover face + hinged spine + fore-edge pages) is the site's
   strongest object, so instead of inventing new shapes for each section the
   sections vary SCALE, ANGLE and ARRANGEMENT of the same object. That is what
   keeps a long page feeling composed rather than like five different websites.
   ═════════════════════════════════════════════════════════════════════════ */

/* --- angle variants ------------------------------------------------------ */

/* Mirrored: spine on the RIGHT, turning the other way. Achieved by moving the
   faces rather than scaleX(-1) on the whole book — a mirror would reverse the
   cover artwork and every word printed on it. */
.book--r { transform: rotateY(19deg) rotateX(2deg); }
.book--r:hover { transform: rotateY(7deg) rotateX(1deg) translateY(-8px); }
.book--r .book__spine {
  left: auto; right: 0;
  transform: translateX(14px) rotateY(74deg);
  transform-origin: left center;
  background: linear-gradient(270deg, #05050700, #0a0a0d 40%, #1b1b22 100%);
  border-radius: 0 2px 2px 0;
}
.book--r .book__pages {
  right: auto; left: 0;
  transform: translateX(-8px) rotateY(-74deg);
  transform-origin: right center;
  border-radius: 2px 0 0 2px;
}
/* light rakes from the other side too, or the mirrored book is lit impossibly */
.book--r .book__face::after {
  background: linear-gradient(260deg,
              rgba(255,255,255,.20) 0%, rgba(255,255,255,.05) 14%,
              transparent 34%, rgba(60,50,34,.10) 100%);
}
.book--r .book__face { border-radius: 6px 2px 2px 6px; }
/* and the cast falls the other way too, for the same reason */
.book--r .book__face {
  box-shadow:
    -1px   1px  3px rgba(28, 24, 16, .30),
    -13px 12px 24px rgba(28, 24, 16, .24),
    -28px 26px 52px rgba(28, 24, 16, .16);
}

/* Flatter — for dense arrangements where a hard angle would collide */
.book--flat { transform: rotateY(-9deg) rotateX(1deg); }
.book--flat:hover { transform: rotateY(-3deg) translateY(-6px); }

/* --- scale variants ------------------------------------------------------ */

/* A hero book carries a deeper spine and a heavier shadow. Both have to scale
   with it: a large book wearing a small book's shadow looks like a sticker. */
.book--hero .book__spine { width: 31px; transform: translateX(-29px) rotateY(-74deg); }
.book--hero .book__pages { width: 18px; transform: translateX(17px) rotateY(74deg); }
.book--hero .book__face {
  border-radius: 3px 10px 10px 3px;
  box-shadow:
     2px  2px  5px rgba(28, 24, 16, .32),
    22px 20px 40px rgba(28, 24, 16, .26),
    48px 44px 88px rgba(28, 24, 16, .18);
}
.book--sm .book__spine { width: 12px; transform: translateX(-11px) rotateY(-74deg); }
.book--sm .book__pages { width: 7px; transform: translateX(6px) rotateY(74deg); }
.book--sm .book__face {
  box-shadow:
     1px  1px  2px rgba(28, 24, 16, .28),
     8px  7px 15px rgba(28, 24, 16, .21),
    17px 16px 32px rgba(28, 24, 16, .14);
}

/* ═══════════════════════════════════════════════════════════════════════════
   SECTION LAYOUTS
   ═════════════════════════════════════════════════════════════════════════ */

/* ── SPOTLIGHT: one book large, three small, asymmetric ───────────────────── */

.spot {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(34px, 6vw, 90px);
  align-items: center;
}
.spot--flip > .spot__art { order: 2; }     /* alternate sides down the page */
@media (max-width: 860px) {
  .spot { grid-template-columns: 1fr; gap: 40px; }
  .spot--flip > .spot__art { order: 0; }
}

.spot__art { perspective: 1900px; }
.spot__lead { display: block; max-width: 340px; margin-inline: auto; }
.spot--flip .spot__lead { margin-right: 0; }


.spot__price {
  display: inline-flex; align-items: baseline; gap: 10px;
  margin-top: 20px;
  font-family: var(--serif);
  font-size: 22px;
  color: var(--brass);
}
.spot__price s { color: var(--text-soft); font-size: 15px; }

/* ── DRIFT: a shelf that slides sideways, forever ─────────────────────────
   Two rows travelling in opposite directions. The track holds the same run of
   books twice and translates exactly -50%, so the loop point lands on an
   identical frame and the seam is invisible. Hover pauses it, because a moving
   target you cannot click is hostile. */

.drift { position: relative; overflow: hidden; }
/* fade the ends into the ground rather than letting books get guillotined */
.drift::before, .drift::after {
  content: "";
  position: absolute; top: 0; bottom: 0; width: clamp(40px, 8vw, 140px);
  z-index: 2; pointer-events: none;
}
.drift::before { left: 0;  background: linear-gradient(90deg, var(--paper-2), transparent); }
.drift::after  { right: 0; background: linear-gradient(270deg, var(--paper-2), transparent); }
.sec--paper .drift::before { background: linear-gradient(90deg, var(--paper), transparent); }
.sec--paper .drift::after  { background: linear-gradient(270deg, var(--paper), transparent); }

.drift__track {
  display: flex;
  gap: clamp(20px, 2.6vw, 40px);
  width: max-content;
  padding-block: 26px;
  animation: drift-left 78s linear infinite;
}
.drift__track--rev { animation-name: drift-right; animation-duration: 94s; }
.drift:hover .drift__track { animation-play-state: paused; }

.drift__track .book-item { flex: 0 0 clamp(112px, 12vw, 156px); }
/* SAFARI'S 8192px TEXTURE CAP, and why every item in a moving track carries its
   own translateZ(0).

   An animated transform turns the whole track into ONE composited texture. A
   drift track is ~28 covers × ~170px = ~4,900 CSS px, and on a Retina screen
   that doubles to ~9,700 device px — past the 8,192px limit Safari will
   rasterise as a single texture. Everything beyond the cap silently fails to
   draw: whole runs of covers blank out, then flicker back as the loop wraps.
   Chrome tiles large animated layers and never hits the wall, which is why the
   bug was Safari-only. The narrower the window, the less it shows — it never
   reproduced in a 508px test pane, and always on a full-width screen.

   translateZ(0) on each ITEM breaks the giant texture apart: the track layer
   becomes an empty container and every cover rasterises as its own small tile,
   none anywhere near the cap. ~170 small layers is comfortable — the earlier
   Safari failure was ~1000 layers from will-change on every book PLUS its
   three preserve-3d plates; this is a fraction of that, and only inside the
   tracks that need it. */
.drift__track .book-item,
.reel__row .reel__card { transform: translateZ(0); }
/* AND `min-width: 0`, which is the line that ended launch night.

   A flex item's default `min-width: auto` lets its CONTENT's minimum size
   overrule the flex-basis. The moment a cover image finished loading, WebKit
   fed its intrinsic 600px width through the aspect-ratio face into that
   minimum — and the book ballooned from 112px to several hundred, row by row
   as images arrived. The track stretched to ~8x its measured width, the
   marquee's loop point was left pointing into the void, and the rows appeared
   to accelerate, unwind off-screen and dissolve into one. Chromium resolves
   the same tug-of-war in favour of the flex-basis, which is why no amount of
   desktop Chrome testing could ever show it. min-width: 0 ends the argument:
   the basis is the size, on every engine. */
.drift__track .book-item,
.reel__row .reel__card { min-width: 0; }
/* …and the belt to that suspender. Telemetry from the narrow-Safari hunt
   showed the sick books rendering at ~1005px — the cover images' NATURAL
   width — meaning WebKit, under some rendering condition we never fully
   cornered, abandons the assigned size and lets the image dictate. So the
   chain is now constrained at every link: the item has an explicit width
   (not merely a flex-basis an engine might out-argue), and nothing inside
   may exceed its parent. Whatever the trigger was, it has nowhere to go —
   and the marquee's once-a-second re-measure absorbs anything that still
   moves. */
.drift__track .book-item { width: clamp(112px, 12vw, 156px); }
.drift__track .book,
.drift__track .book__face,
.drift__track .book__face img,
.reel__row .reel__card img { max-width: 100%; }

@keyframes drift-left  { from { transform: translateX(0); }    to { transform: translateX(-50%); } }
@keyframes drift-right { from { transform: translateX(-50%); } to { transform: translateX(0); } }

/* ── MOSAIC: deliberate geometry, mixed sizes ─────────────────────────────
   A 6-column grid where each book claims a different span and sits at a
   different vertical offset, so the composition reads as arranged rather than
   as a grid that happens to have gaps. */

.mosaic {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: clamp(16px, 2.4vw, 34px);
  align-items: start;
}
.mosaic .book-item { grid-column: span 2; }
.mosaic .book-item:nth-child(1) { grid-column: span 3; margin-top: clamp(20px, 3vw, 46px); }
.mosaic .book-item:nth-child(2) { grid-column: span 3; }
.mosaic .book-item:nth-child(3) { margin-top: clamp(28px, 4vw, 62px); }
.mosaic .book-item:nth-child(5) { margin-top: clamp(18px, 2.6vw, 40px); }
@media (max-width: 760px) {
  .mosaic { grid-template-columns: repeat(4, 1fr); }
  .mosaic .book-item { grid-column: span 2; }
  .mosaic .book-item:nth-child(1),
  .mosaic .book-item:nth-child(2) { grid-column: span 4; }
  .mosaic .book-item:nth-child(n) { margin-top: 0; }
}

/* ── FREE SHELF: the open strings ─────────────────────────────────────────── */

.freeshelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(clamp(104px, 11vw, 140px), 1fr));
  gap: clamp(18px, 2.4vw, 34px);
}

@media (prefers-reduced-motion: reduce) {
  .drift__track { animation: none; }
  /* The reel still works stopped: the script measures rendered positions, so
     whichever card happens to sit nearest the phone is the one it shows. */
  .reel__row { animation: none; }
  .shorts__tilt { transition: none; }
}

/* ── PATH: a sequence of study ─────────────────────────────────────────────
   Books in a row, numbered, joined by a brass rule — the visual claim being
   that these are steps in an order, not a shelf of related titles. The rule is
   drawn BEHIND the numbers and trimmed at both ends so it never dangles past
   the first or last step. */

.path { position: relative; }

.path__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: clamp(14px, 2vw, 30px);
  align-items: start;
}
@media (max-width: 900px) {
  /* On a phone the row becomes a swipeable rail rather than crushing six books
     into 44px each. Scroll-snap so each step lands squarely. */
  .path__track {
    grid-auto-flow: column;
    grid-auto-columns: minmax(148px, 60%);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    padding-bottom: 14px;
    -webkit-overflow-scrolling: touch;
  }
  .path__step { scroll-snap-align: start; }
}

.path__step {
  position: relative;
  /* Grid items are min-content sized by default, so a long book title refuses to
     let its column shrink and the row overflows its container. min-width:0 lets
     the 1fr columns actually behave like 1fr. */
  min-width: 0;
}

/* the connecting rule: sits on the number row, behind the discs */
.path__num {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
}
.path__num::after {
  content: "";
  position: absolute;
  left: 30px; right: calc(-1 * clamp(14px, 2vw, 30px));
  top: 50%;
  height: 1px;
  background: var(--line);
}
.path__step:last-child .path__num::after { display: none; }

.path__disc {
  flex: none;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--brass);
  background: var(--paper);
  color: var(--brass);
  font-size: 11.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  position: relative; z-index: 1;
}
.sec--gray .path__disc { background: var(--paper-2); }

.path__why {
  margin: 14px 0 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--text-mid);
}
.path__step .book-item { display: block; }
.path__step .book__meta { margin-top: 16px; }
.path__step .book__title { font-size: 14.5px; }

.path__head { max-width: 62ch; }
.path__title {
  margin: 8px 0 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(17px, 1.7vw, 21px);
  color: var(--brass);
}
.path__area {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-soft);
}

/* ═══════════════════════════════════════════════════════════════════════════
   CATALOG
   ═════════════════════════════════════════════════════════════════════════ */

.page { padding-top: var(--nav-h); }

.prose { max-width: 62ch; margin-inline: auto; }
.prose p { font-size: clamp(16.5px, 1.4vw, 18.5px); line-height: 1.72; color: var(--text-mid); }
.prose p + p { margin-top: 1.2em; }

/* filter bar — sticks under the nav so the controls stay reachable while
   scrolling several hundred covers */
.catbar {
  position: sticky;
  top: var(--nav-h);
  z-index: 40;
  padding-block: 16px 14px;
  background: rgba(255, 255, 255, .93);
  backdrop-filter: saturate(180%) blur(16px);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  border-block: 1px solid var(--line);
}
.catbar__row {
  display: flex; flex-wrap: wrap; gap: 12px;
  align-items: center; justify-content: space-between;
}
.catbar__group { display: flex; flex-wrap: wrap; gap: 8px; }
.catbar__subs { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
/* An author `display` beats the UA's [hidden] rule, so the row has to be told
   explicitly. Without this, hiding it under "In The Style Of" does nothing. */
.catbar__subs[hidden] { display: none; }
.catbar__count { margin: 11px 0 0; font-size: 12.5px; color: var(--text-soft); }

.chip {
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--text-mid);
  font-size: 13px;
  cursor: pointer;
  transition: border-color .25s var(--ease), color .25s var(--ease),
              background .25s var(--ease);
}
.chip:hover { border-color: var(--brass); color: var(--brass); }
.chip.is-on {
  background: var(--brass);
  border-color: var(--brass);
  color: #fff;
}
.chip--area { font-weight: 600; }
/* Explicit, not relying on the UA default: the sub-category chips are hidden
   when they belong to the other area, and any future `display` on .chip would
   silently override [hidden] and bring them all back. */
.chip[hidden] { display: none; }

.catbar__search { flex: 1 1 220px; max-width: 320px; }
.catbar__search input {
  width: 100%;
  padding: 9px 15px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  font: inherit;
  font-size: 14px;
  color: var(--text-dark);
}
.catbar__search input:focus { outline: none; border-color: var(--brass); }

/* ── the filter bar on a phone ───────────────────────────────────────────────
   On desktop the chips wrap into two short lines and the bar earns its
   stickiness. On a phone the same wrapping stacked them into a column taller
   than the screen — and because the bar is sticky, the entire catalog scrolled
   underneath a wall of filters, with barely one row of books ever visible.
   Two changes below the breakpoint:

   1. Each chip row becomes ONE line that scrolls sideways under the thumb —
      the pattern every phone storefront uses. The rows bleed to the screen
      edge so a half-visible chip advertises that they scroll.
   2. The bar stops being sticky. Even compressed it is a fifth of a phone
      screen, and the pinning was the complaint itself: filters you scroll
      back up to beat filters that follow you down eating the books. */
@media (max-width: 760px) {
  .catbar { position: static; }
  /* Stacked, not flexed: as a flex item the scrolling group would need
     min-width gymnastics to shrink at all. As blocks, overflow just works. */
  .catbar__row { display: block; }
  .catbar__group,
  .catbar__subs {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* full-bleed: the row runs edge to edge of the SCREEN, not the column */
    margin-inline: calc(-1 * var(--pad));
    padding-inline: var(--pad);
  }
  .catbar__group::-webkit-scrollbar,
  .catbar__subs::-webkit-scrollbar { display: none; }
  /* nowrap flex shrinks its items to fit unless told otherwise — a squeezed
     chip wraps its label into two cramped lines */
  .catbar__group .chip,
  .catbar__subs .chip { flex: 0 0 auto; }
  .catbar__search { display: block; max-width: none; margin-top: 10px; }
}

.catgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(clamp(132px, 14vw, 178px), 1fr));
  gap: clamp(22px, 3vw, 44px);
}
.cat__item[hidden] { display: none; }

.catempty {
  margin: 40px auto 0;
  text-align: center;
  color: var(--text-soft);
}
.catempty a { color: var(--brass); text-decoration: underline; text-underline-offset: 3px; }

.nav__links a.is-here { color: var(--text-dark); font-weight: 600; }

/* ═══════════════════════════════════════════════════════════════════════════
   FEATURED SERIES — the flagship collections

   Each series is presented as a FAN: its volumes overlapped and splayed like
   books standing together on a shelf, rather than laid out in a grid. A grid
   says "here are some products"; a fan says "these belong together and there
   are more of them" — which is the actual claim being made about a series.

   Accents, in order of loudness:
     · a large ghosted Roman numeral behind each block
     · the series name set in the Didone display face, not the body serif
     · a brass hairline that grows on hover
     · a small brass volume-count seal
   ═════════════════════════════════════════════════════════════════════════ */

.series {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(28px, 4vw, 64px) clamp(24px, 3.4vw, 52px);
}
@media (max-width: 900px) { .series { grid-template-columns: 1fr; } }

.serie {
  position: relative;
  display: block;
  padding: clamp(24px, 3vw, 38px) clamp(22px, 2.6vw, 34px) clamp(26px, 3vw, 36px);
  border-radius: 16px;
  background: var(--paper);
  border: 1px solid var(--line);
  overflow: hidden;
  transition: transform .45s var(--ease), box-shadow .45s var(--ease),
              border-color .45s var(--ease);
}
.serie:hover {
  transform: translateY(-4px);
  border-color: rgba(184, 135, 60, .45);
  box-shadow: 0 20px 46px rgba(28, 24, 16, .10);
}

/* the ghosted numeral — set in the display face, cropped by the card */
.serie__numeral {
  position: absolute;
  right: -6px; bottom: -34px;
  font-family: var(--display);
  font-size: clamp(96px, 13vw, 168px);
  line-height: .8;
  color: var(--paper-3);
  pointer-events: none;
  user-select: none;
  z-index: 0;
}

/* The card fills its grid row, and the foot is pushed to the bottom of it, so
   "Explore the series →" sits on the same line in both cards of a row however
   long the two descriptions are. The grid stretches every card in a row to the
   tallest one regardless; without this the extra height collected as dead space
   below the shorter card's link, which reads as a card that failed to finish.
   With it, the space falls between the description and the foot, where it reads
   as room to breathe. Chromatic Methenyisms runs to eight lines against three
   for its neighbour, so this is a 120px difference, not a hairline.
   (The `margin-top: auto` that does the pushing lives on .serie__foot further
   down — it has to, or the existing rule there would overwrite it.) */
.serie { height: 100%; }
.serie__in {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* ── the fan ─────────────────────────────────────────────────────────────
   Covers overlapped and splayed like books someone set down together, not
   stepped by a formula. Position, lean and size come from a fixed table in the
   builder (--x/--y/--rot/--sc), so the arrangement is irregular but identical
   on every build.

   --x is a PERCENTAGE of the fan, so the spread widens with the card instead of
   bunching at a fixed pixel step on a wide screen. */
.fan {
  position: relative;
  /* Height is derived from the cover size, not from the viewport. Covers are
     3:4, so a viewport-based height drifts out of step with them and either
     clips the tallest cover or leaves a band of dead space. The +30px absorbs
     the vertical nudges and the hover lift. */
  --cover-w: clamp(112px, 13.5vw, 152px);
  /* 1.42 = the covers' true height-to-width ratio (1/0.707), not 1.34. With
     the 3:4 figure the tallest cover in the fan overflowed the card. */
  height: calc(var(--cover-w) * 1.42 + 30px);
  margin-bottom: clamp(22px, 2.6vw, 32px);
  perspective: 1600px;
}
.fan__book {
  position: absolute;
  top: var(--y, 0);
  left: var(--x, 0);
  width: var(--cover-w);
  transform: rotate(var(--rot, 0deg)) scale(var(--sc, 1));
  transform-origin: bottom center;
  transition: transform .55s var(--ease), left .55s var(--ease), top .55s var(--ease);
  /* the leftmost cover sits furthest back, so the stack overlaps like a shelf
     riffled from the left */
  z-index: calc(10 - var(--n));
}
/* At rest the covers sit clustered and centred, mostly overlapping — a closed
   stack. Hovering opens them OUTWARD to their own second position, so the fan
   spreads from the middle instead of marching right and escaping the card. */
.serie:hover .fan__book {
  left: var(--xh, var(--x, 0));
  top: calc(var(--y, 0px) - 6px);
  transform: rotate(var(--roth, var(--rot, 0deg))) scale(calc(var(--sc, 1) * 1.02));
}
/* the card leads the interaction; individual covers must not also tilt, or two
   competing 3-D transforms fight over the same element */
.fan .book,
.fan .book:hover { transform: rotateY(-13deg) rotateX(1deg); }

/* CAST SHADOWS BETWEEN THE STACKED COVERS
   The generic book shadow is symmetric — fine for a cover standing alone, but
   in a fan it gives no sense of one cover sitting ON another. These are
   DIRECTIONAL, thrown down and to the right, because the covers stack
   left-over-right (the leftmost has the highest z-index) and the page is lit
   from the upper left — the raking highlight on every cover face comes from
   that side, so shadows have to fall away from it or the light reads as coming
   from two places at once.

   Three layers doing different jobs:
     · a tight contact shadow, so each cover touches the one beneath it
     · a mid cast, which is the one that actually separates the stack
     · a wide soft cast, grounding the whole fan on the card */
.fan .book__face {
  box-shadow:
     1px  1px  3px rgba(28, 24, 16, .34),
    16px 14px 26px rgba(28, 24, 16, .30),
    34px 30px 58px rgba(28, 24, 16, .22);
}

/* Opened out, the covers no longer sit on each other, so the heavy mid cast
   would read as dirt rather than depth. It eases back as they separate. */
.serie:hover .fan .book__face {
  box-shadow:
     1px  1px  3px rgba(28, 24, 16, .26),
    11px 12px 24px rgba(28, 24, 16, .22),
    26px 26px 52px rgba(28, 24, 16, .16);
}

.serie__name {
  font-family: var(--display);
  font-size: clamp(21px, 2.3vw, 29px);
  line-height: 1.14;
  letter-spacing: .012em;
  color: var(--text-dark);
}
/* the brass hairline that grows */
.serie__rule {
  width: 34px; height: 2px;
  margin: 14px 0 15px;
  background: var(--brass);
  transition: width .45s var(--ease);
}
.serie:hover .serie__rule { width: 64px; }

.serie__desc {
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-mid);
  max-width: 40ch;
}

.serie__foot {
  /* auto pushes the foot to the bottom of the card (see .serie__in above); the
     padding keeps its 18px clear of the description when there is no slack to
     take up. A margin would be swallowed by the auto. */
  margin-top: auto;
  padding-top: 18px;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.serie__seal {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid rgba(184, 135, 60, .42);
  background: rgba(184, 135, 60, .08);
  color: var(--brass-dim);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.serie__go { font-size: 14px; font-weight: 600; color: var(--brass); }

/* ── three drifting rows, for the free shelf ─────────────────────────────── */
.drift--slow  .drift__track { animation-duration: 104s; }
.drift--mid   .drift__track { animation-duration: 86s;  }
.drift--quick .drift__track { animation-duration: 68s;  }

/* ═══════════════════════════════════════════════════════════════════════════
   THE PHONE — publication videos in a device frame

   The frame is a PNG with a transparent screen hole (the same artwork used on
   the WalkNessebar site). The video sits behind it and shows through, so the
   bezel, corner radius and camera cutout are real artwork rather than CSS
   approximations of a phone.

   The screen percentages below were MEASURED from the file — the transparent
   region is found by connected-component labelling and the one containing the
   centre is the screen. If the artwork is ever swapped, re-measure rather than
   nudging these by eye.

   Landscape uses a separately rotated copy of the artwork rather than a CSS
   rotation of the element. Rotating the element would rotate its children too,
   so the video would need a counter-rotation, and a transformed iframe is a
   reliable source of blurry text and mis-hit taps.
   ═════════════════════════════════════════════════════════════════════════ */

.device {
  position: relative;
  width: min(280px, 68vw);
  aspect-ratio: 666 / 1368;
  /* The shadow is cast from the element's ALPHA, so every layer inside the
     phone contributes its silhouette to it. That is why the screen box is
     declared once, as variables, and every full-bleed layer is pinned to it:
     one layer sitting at inset 0 would hand the shadow a plain rectangle and
     the phone would sit on a grey slab instead of casting a shadow. */
  --sx: 4.655%;  --sy: 1.974%;
  --sw: 90.691%; --sh: 96.053%;
  --sr: 11% / 5.2%;
  /* the phone body's OUTER corner, measured from the artwork's alpha channel:
     a ~101px radius on a 666px-wide frame */
  --dr: 15% / 7.3%;
}
.device--land {
  width: min(560px, 92vw);
  aspect-ratio: 1368 / 666;
  --sx: 1.974%;  --sy: 4.655%;
  --sw: 96.053%; --sh: 90.691%;
  --sr: 5.2% / 11%;
  --dr: 7.3% / 15%;
}

/* THE PHONE'S SHADOW — cast by a shape, not by a filter.

   This used to be `filter: drop-shadow()` on .device, which traces the whole
   subtree's alpha and therefore gives a perfect phone-shaped shadow — right up
   until a visitor presses play. A YouTube iframe cannot be rasterised into a
   filtered subtree, so the browser falls back to compositing it as an opaque
   layer and a hard rectangle appears around the phone the moment the video
   loads.

   A dedicated caster avoids the whole problem: an empty element the size of
   the phone with the phone's own corner radius, sitting behind everything.
   `box-shadow` draws OUTSIDE the border box regardless of background, so it
   needs no fill of its own and can never show through the artwork's soft
   edges — and nothing here filters the iframe. */
.device::before {
  content: "";
  position: absolute;
  /* TUCKED UNDER THE PHONE, not flush with the box.
     The artwork's body does not reach the edges of its own image — it is inset
     4-7px of 666 horizontally. At inset 0 the caster was fractionally WIDER
     than the phone, and since an outer box-shadow is clipped to outside its own
     border box, that sliver of background never got darkened: a pale rim
     between the phone and its own shadow, which is the white outline that
     showed up around every device on the site. 1.6% clears the widest of those
     margins in either orientation and disappears behind the artwork. */
  inset: 1.6%;
  z-index: 0;
  border-radius: var(--dr);
  box-shadow:
    0  4px  10px rgba(28, 24, 16, .20),
    0 30px  52px rgba(28, 24, 16, .34),
    0 60px 100px rgba(28, 24, 16, .22);
  pointer-events: none;
}

.device__frame {
  position: absolute; inset: 0; z-index: 3;
  width: 100%; height: 100%;
  pointer-events: none;
}
.device__screen {
  position: absolute;
  left: var(--sx); top: var(--sy);
  width: var(--sw); height: var(--sh);
  z-index: 1;
  overflow: hidden;
  border-radius: var(--sr);
  background: #0b0b0d;
}
.device__screen img,
.device__screen iframe {
  width: 100%; height: 100%;
  border: 0;
  display: block;
  object-fit: cover;
  object-position: center;
}

/* Click-to-load, as everywhere else on the site: the poster is a plain image
   and no YouTube player exists until someone actually asks for it. */
/* Pinned to the SCREEN, not to the whole device. At inset 0 its dark gradient
   spilled past the phone's rounded corners and — worse — gave the drop-shadow a
   full rectangle to trace, so each phone appeared to sit on a dark slab rather
   than cast a shadow of its own shape. */
.device__play {
  position: absolute; z-index: 2;
  left: var(--sx); top: var(--sy);
  width: var(--sw); height: var(--sh);
  border-radius: var(--sr);
  display: grid; place-items: center;
  border: 0; padding: 0; cursor: pointer;
  background: linear-gradient(180deg, rgba(0,0,0,.04), rgba(0,0,0,.34));
  transition: background .4s var(--ease);
}
.device__play i {
  display: grid; place-items: center;
  width: 54px; height: 54px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .18);
  border: 1px solid rgba(255, 255, 255, .55);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  transition: transform .35s var(--ease), background .35s var(--ease);
}
.device__play i::after {
  content: "";
  margin-left: 4px;
  border-style: solid;
  border-width: 9px 0 9px 15px;
  border-color: transparent transparent transparent #fff;
}
.device:hover .device__play i { transform: scale(1.07); background: rgba(255, 255, 255, .3); }
.device.is-playing .device__play { opacity: 0; visibility: hidden; }

.device__cap {
  margin: 16px auto 0;
  max-width: 26ch;
  text-align: center;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-soft);
}

/* a stage that mixes one landscape device with vertical ones either side */
.stage {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(24px, 4vw, 56px);
}
.stage__item { display: flex; flex-direction: column; align-items: center; }
@media (max-width: 780px) {
  /* the landscape device leads on a phone; the vertical pair follows beneath */
  .stage { flex-direction: column; }
  .stage__item--land { order: -1; }
}

/* ── SHORTS STRIP ─────────────────────────────────────────────────────────
   Three vertical phones dropped at angles, like they were set down on a desk
   rather than arranged. The tilt sits on an INNER element: the scroll-reveal
   is itself a transform and an element holds only one, so putting both on the
   same node means the reveal wipes out the angle. */

.strip {
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: 620px;
  margin: clamp(54px, 7vw, 92px) auto clamp(24px, 3.2vw, 40px);
}
.strip::before, .strip::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line));
}
.strip::after { background: linear-gradient(270deg, transparent, var(--line)); }
.strip span {
  flex: 0 0 auto;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.shorts {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(2px, 1.4vw, 24px);
}
.shorts__tilt {
  transform: rotate(var(--rot)) translateY(var(--dy));
  transition: transform .55s var(--ease);
}
/* Straighten and lift the one being looked at — the others stay scattered,
   which is what makes the pile read as a pile. */
.shorts__item:hover .shorts__tilt {
  transform: rotate(0deg) translateY(calc(var(--dy) - 12px));
}
.shorts .device { width: min(190px, 25vw); }
.shorts .device__cap { margin-top: 13px; max-width: 19ch; font-size: 12.5px; }

@media (max-width: 640px) {
  /* Three across a phone screen: narrower, tighter, with shorter captions so
     a long title cannot push one column taller than the others. */
  .shorts { gap: 4px; }
  .shorts .device { width: 29vw; }
  .shorts .device__cap { margin-top: 9px; max-width: 14ch; font-size: 11px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE REEL — square video cards travelling behind one large phone

   A dark panel set INTO the light page rather than a full-width dark band.
   The page already has its dark bands (quotes, footer); another one here would
   break the rhythm, and a screen needs a dark surround to look lit at all.

   The card row is a plain CSS animation. The script does not drive it — it
   measures which card is closest to the phone's centre a few times a second
   and swaps the poster to match. That keeps the pairing exact whether the row
   is running, paused under the cursor, throttled in a background tab, or
   stopped entirely by reduced-motion.
   ═════════════════════════════════════════════════════════════════════════ */

.reel {
  position: relative;
  margin-top: clamp(52px, 6.5vw, 92px);
  padding: clamp(30px, 4vw, 54px) 0 clamp(26px, 3.4vw, 44px);
  border-radius: clamp(18px, 2.2vw, 30px);
  background: radial-gradient(130% 150% at 50% -20%, #2b2b33 0%, var(--ink) 62%);
  color: #fff;
  overflow: hidden;
  box-shadow: 0 30px 66px rgba(24, 20, 12, .20);
}

.reel__head { text-align: center; padding-inline: var(--pad); }
.reel__head .eyebrow { color: var(--brass-lit); }
.reel__head .h3 { color: #fff; margin: 8px 0 0; }
.reel__head > p {
  margin: 12px auto 0;
  max-width: 48ch;
  font-size: 15px;
  color: var(--text-faint);
}

.reel__stage {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: clamp(26px, 3.4vw, 46px);
}

/* The rail is absolutely positioned so the stage's height comes from the phone
   alone — the cards can then be any size without moving anything below them. */
.reel__rail {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 13%, #000 87%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 13%, #000 87%, transparent);
}
.reel__row {
  display: flex;
  gap: clamp(10px, 1.4vw, 20px);
  /* The animation is the NO-JS FALLBACK. site.js replaces it with a
     scroll-driven marquee the moment it runs — see "THE MARQUEES DRIVE
     THEMSELVES" there for why (Safari blanked composited animated tracks
     during scroll). Speed still lives here: the script reads this duration.
     No will-change — that hint is what helped Safari over the edge. */
  animation: drift-left 64s linear infinite;
}
/* The runtime scroller site.js wraps each drift track in. Plain overflow is
   the whole point: scrolled content is rasterised by the engine's ordinary
   tile machinery, which is the one path Safari never gets wrong. */
.marquee-scroll { overflow: hidden; }
.reel__card {
  flex: 0 0 clamp(88px, 10.5vw, 132px);
  aspect-ratio: 1;
  border-radius: 12px;
  overflow: hidden;
  /* held back so the lit phone is unmistakably the subject */
  opacity: .42;
  box-shadow: 0 10px 24px rgba(0, 0, 0, .38);
}
.reel__card img { width: 100%; height: 100%; object-fit: cover; }

/* Hovering holds the row still. The link below the phone changes as the reel
   turns, so without this you would be reaching for a destination that swaps
   under the cursor on the way. Pausing while a video plays is the same idea:
   the reel must not keep turning past what you are watching. */
.reel:hover .reel__row,
.reel.is-playing .reel__row { animation-play-state: paused; }

.reel__phone { position: relative; z-index: 2; }
/* Sized against the viewport rather than the panel, and deliberately held back
   from filling it: if the phone spans the whole width there is no room left for
   the cards to be seen passing behind it, and the whole point of the panel is
   watching them arrive. */
.reel__phone .device { width: min(470px, 60vw); }
/* warm spill, so the phone reads as a lit screen rather than a cut-out */
.reel__phone::before {
  content: "";
  position: absolute;
  inset: -16% -12%;
  z-index: -1;
  background: radial-gradient(52% 58% at 50% 50%, rgba(212, 160, 82, .22), transparent 70%);
  filter: blur(16px);
  pointer-events: none;
}
.reel__poster { transition: opacity .28s linear; }

.reel__now {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  margin-top: clamp(22px, 2.8vw, 36px);
  padding-inline: var(--pad);
  text-align: center;
}
.reel__title {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(17px, 2vw, 21px);
  line-height: 1.35;
  color: #fff;
  /* reserve the line so a shorter title does not jog the button upward */
  min-height: 1.35em;
}
.reel__go, .reel__back {
  font-size: 14px;
  font-weight: 600;
  color: var(--brass-lit);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  transition: color .3s var(--ease);
}
.reel__go:hover, .reel__back:hover { color: #fff; }
.reel__back[hidden] { display: none; }

@media (max-width: 560px) {
  .reel__head > p { font-size: 14px; }
  /* Smaller cards so at least one whole one is still visible either side of
     the phone — a sliver reads as a rendering fault, not as a moving row. */
  .reel__card { flex-basis: clamp(64px, 17vw, 96px); border-radius: 9px; }
  .reel__phone .device { width: 64vw; }
  .reel__rail {
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
            mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE DETAIL SHEET — a glassy card over the page

   Clicking any book anywhere on the site opens this instead of throwing the
   visitor straight out to Gumroad. The book is shown as the same 3-D object it
   is everywhere else, its video plays in the real phone artwork, and the full
   description, the topics it covers and the buy button are all here.

   Frosted rather than solid on purpose (the WalkNessebar app's own surface):
   the page stays faintly visible behind it, so the sheet reads as something
   laid ON the catalog rather than a new screen the visitor has been sent to.

   The markup is built by the script, not baked into four templates. With no
   script the books are still ordinary links to the shop, which is exactly the
   right fallback.
   ═════════════════════════════════════════════════════════════════════════ */

.sheet {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(12px, 3vw, 40px);
}
.sheet[hidden] { display: none; }

.sheet__scrim {
  position: absolute;
  inset: 0;
  background: rgba(16, 14, 10, .46);
  -webkit-backdrop-filter: blur(7px);
          backdrop-filter: blur(7px);
  animation: sheet-fade .32s var(--ease) both;
}

.sheet__card {
  position: relative;
  width: min(1040px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  border-radius: clamp(20px, 2.6vw, 32px);
  /* Thin enough to genuinely see the catalog through it. The heavy blur is
     what keeps the text on top readable — it turns whatever is behind into an
     even wash instead of detail competing with the words. */
  background: rgba(255, 255, 255, .52);
  -webkit-backdrop-filter: blur(50px) saturate(2);
          backdrop-filter: blur(50px) saturate(2);
  border: 1px solid rgba(255, 255, 255, .5);
  /* THE EDGE, which is what sells it as a pane of glass rather than a
     translucent rectangle. A bright catch along the top rim, a dimmer one
     underneath, a rim down both sides, and — the part that reads as THICKNESS —
     a band of light bleeding inward from the top and bottom edges, the way it
     does through the polished edge of real glass. */
  box-shadow:
    inset 0  1.5px 0 rgba(255, 255, 255, .95),
    inset 0 -1.5px 0 rgba(255, 255, 255, .60),
    inset  1.5px 0 0 rgba(255, 255, 255, .55),
    inset -1.5px 0 0 rgba(255, 255, 255, .55),
    inset 0  30px 44px -30px rgba(255, 255, 255, .95),
    inset 0 -30px 44px -30px rgba(255, 255, 255, .65),
    0 44px 110px rgba(20, 17, 10, .40);
  animation: sheet-rise .42s var(--ease) both;
}

@keyframes sheet-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes sheet-rise {
  from { opacity: 0; transform: translateY(22px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* The CARD is the scroll container, not the page: a long description scrolls
   inside the glass while the frame, and the close button, stay put. */
.sheet__scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: clamp(24px, 3.4vw, 52px);
}

.sheet__x {
  position: absolute;
  top: 12px; right: 12px;
  z-index: 2;
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border: 1px solid rgba(20, 17, 10, .12);
  border-radius: 50%;
  /* Nearly opaque, and blurred: it floats above the scrolling content, so text
     passes underneath it. At .72 the two read as one smudge. */
  background: rgba(255, 255, 255, .92);
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
  box-shadow: 0 2px 10px rgba(20, 17, 10, .10);
  color: var(--text-mid);
  font-size: 19px; line-height: 1;
  cursor: pointer;
  transition: background .25s var(--ease), color .25s var(--ease);
}
.sheet__x:hover { background: #fff; color: var(--text-dark); }

.sheet__in {
  display: grid;
  grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr);
  gap: clamp(26px, 3.6vw, 54px);
  align-items: start;
}

/* ── the art side ── */
.sheet__art { display: flex; flex-direction: column; align-items: center; gap: clamp(18px, 2.4vw, 30px); }

/* THE PLINTH — the book, standing on its own reflection.

   The reflection is a second copy of the same 3-D book, flipped in place and
   faded out downward, so the cover appears to be standing at the edge of still
   water. Two details make it read as a reflection rather than a copy:

     · it is CLIPPED to a shallow band. A full-height mirror image looks like a
       printing error; only the first inch or so of any real reflection is
       legible before the surface breaks it up.
     · the flip is `scaleY(-1)` about the element's own centre, so the book's
       BOTTOM edge lands against the real book's bottom edge — which is the one
       thing that has to be exact for the illusion to hold.

   The band's height comes from `aspect-ratio`, so it is a fraction of the
   book's own width and stays in proportion at every size. */
.sheet__plinth { position: relative; width: min(288px, 84%); }
.sheet__plinth .book-item { width: 100%; }
/* The book is the largest object on the card and sits on a pale, translucent
   ground, so it needs a deeper cast than it does on the page to lift off it. */
.sheet__plinth .book__face {
  box-shadow:
     2px  3px  6px rgba(28, 24, 16, .34),
    24px 26px 44px rgba(28, 24, 16, .30),
    52px 54px 96px rgba(28, 24, 16, .22);
}

.sheet__reflect {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 0.40;
  opacity: .20;
  filter: blur(1.1px);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent 88%);
          mask-image: linear-gradient(to bottom, #000, transparent 88%);
}
.sheet__reflect-in { transform: scaleY(-1); }
/* A mirrored cast shadow would rake upward into the book it belongs to. */
.sheet__reflect .book__face { box-shadow: none; }
/* and the reflection must not answer the cursor — it is not an object */
.sheet__reflect .book, .sheet__reflect .book:hover { transition: none; }

/* Generic first, landscape second. Both selectors carry the same specificity,
   so whichever is written last wins — with the order reversed every landscape
   phone silently rendered at the upright phone's width. */
.sheet__art .device { width: min(196px, 56%); }
.sheet__art .device--land { width: min(400px, 100%); }

/* ── the text side ── */
.sheet__cat {
  margin: 0 0 10px;
  font-size: 11.5px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--brass-dim);
}
.sheet__title {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(23px, 3vw, 34px);
  line-height: 1.14;
  letter-spacing: -.012em;
  color: var(--text-dark);
  text-wrap: balance;
}
.sheet__price {
  margin: 14px 0 0;
  font-family: var(--serif);
  font-size: clamp(19px, 2.2vw, 25px);
  color: var(--brass);
}
.sheet__desc {
  margin: clamp(16px, 2vw, 22px) 0 0;
  font-size: 15.5px;
  line-height: 1.62;
  /* Near-black, not the page's mid grey. This card is only 52% opaque, so text
     is competing with whatever shows through it; mid grey that reads perfectly
     on solid paper goes soft here. */
  color: #23232a;
}
/* Between paragraphs, not just after the price above them. */
.sheet__desc + .sheet__desc { margin-top: 1.05em; }

/* Bullet lists Hristo wrote as bullets — "the concepts explored are designed
   to strengthen: …". Set as a proper list rather than run together as prose,
   with brass markers so they read as points rather than more paragraphs. */
.sheet__points {
  list-style: none;
  margin: clamp(12px, 1.4vw, 16px) 0 0;
  padding: 0;
  display: grid;
  gap: 7px;
}
.sheet__points li {
  position: relative;
  padding-left: 20px;
  font-size: 15px;
  line-height: 1.5;
  color: #23232a;
}
.sheet__points li::before {
  content: "";
  position: absolute;
  left: 4px; top: .62em;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--brass);
}
.sheet__points + .sheet__desc { margin-top: clamp(14px, 1.6vw, 20px); }

/* WHAT'S INCLUDED — lifted out of the last line of the description.

   This is the answer to "what do I actually get for my money", and in the
   source text it is the final sentence of a wall of prose. Given its own panel,
   with a line and an icon per item, it can be read in a glance. Brass-tinted
   rather than white so it reads as a different KIND of thing from the
   description above it — a spec, not more writing. */
.incl {
  margin: clamp(22px, 2.6vw, 30px) 0 0;
  padding: clamp(15px, 1.9vw, 21px) clamp(15px, 2vw, 22px);
  border-radius: 16px;
  /* Near-solid. On a card that is only half opaque, a lightly tinted panel
     reads as a smudge rather than as its own object — this has to look like a
     spec sheet laid on the glass, not a wash over it. */
  background: linear-gradient(180deg, rgba(252, 248, 240, .93), rgba(250, 244, 233, .88));
  border: 1px solid rgba(184, 135, 60, .30);
}
.incl__head {
  display: flex; align-items: center; gap: 11px;
  margin: 0 0 12px;
  font-size: 11px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: #8a6320;
}
.incl__head::after {
  content: "";
  flex: 1; height: 1px;
  background: linear-gradient(90deg, rgba(184, 135, 60, .34), transparent);
}
.incl__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.incl__item {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 13px;
  border-radius: 11px;
  background: rgba(255, 255, 255, .92);
  border: 1px solid rgba(184, 135, 60, .14);
  font-size: 14px; line-height: 1.4;
  color: #17171b;
  /* The entry animation moves the row with `translate`, NOT `transform`. An
     animation with fill-mode `both` keeps applying its final value forever,
     and a final `transform: none` would outrank the hover rule below and kill
     it. The two properties compose instead of fighting. */
  animation: incl-in .5s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 80ms + 120ms);
  transition: transform .3s var(--ease), background .3s var(--ease),
              box-shadow .3s var(--ease);
}
.incl__item:hover {
  transform: translateX(4px);
  background: #fff;
  box-shadow: 0 6px 18px rgba(28, 24, 16, .09);
}
@keyframes incl-in {
  from { opacity: 0; translate: 0 9px; }
  to   { opacity: 1; translate: 0 0; }
}
.incl__ic {
  flex: 0 0 auto;
  width: 31px; height: 31px;
  display: grid; place-items: center;
  border-radius: 9px;
  background: rgba(184, 135, 60, .17);
  color: var(--brass);
}
.incl__ic svg { width: 17px; height: 17px; display: block; }

@media (prefers-reduced-motion: reduce) {
  .incl__item { animation: none; }
}

/* THE TOPICS — the publication's own tags, as highlighted pills. A plain
   comma-separated line would read as more description; these have to look like
   labels at a glance, which is the entire point of showing them. */
.sheet__topics {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: clamp(20px, 2.4vw, 28px) 0 0;
  padding: 0;
}
.sheet__topics li {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 13px 6px 11px;
  border-radius: 999px;
  background: rgba(250, 243, 231, .95);
  border: 1px solid rgba(184, 135, 60, .34);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .01em;
  color: #7d5a1c;
}
.sheet__topics li::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--brass);
}

/* The buy button: at the end of the card, aligned right.

   It used to be sticky, floating above the description with a wash behind it
   to keep the text legible as it passed underneath. Every version of that wash
   — white, then frosted — read as a pale rectangle laid across the glass, so
   the whole mechanism is gone: no sticky, no wash, no scroll listener, no
   marker element. The button simply sits at the end of what it is selling.

   Right-aligned because everything above it is left-aligned prose, and an
   action set against the opposite edge reads as the end of the card rather
   than one more line of it. */
.sheet__cta {
  justify-content: flex-end;
  margin-top: clamp(26px, 3.2vw, 40px);
}
/* A defined rim, only here. Everywhere else a brass button sits on flat paper
   and needs no help; on the translucent card its edge dissolves into whatever
   is showing through. Drawn as a shadow spread rather than a border so the pill
   keeps its exact size and nothing shifts on hover. */
.sheet__cta .btn--brass {
  box-shadow:
    0 0 0 1.5px rgba(108, 76, 20, .55),
    0 4px 14px rgba(184, 135, 60, .30);
}
.sheet__cta .btn--brass:hover {
  box-shadow:
    0 0 0 1.5px rgba(108, 76, 20, .75),
    0 8px 22px rgba(184, 135, 60, .36);
}
.sheet__note {
  margin: 12px 0 0;
  font-size: 12.5px;
  color: var(--text-soft);
  text-align: right;
}

.sheet__loading {
  padding: clamp(40px, 8vw, 90px) 0;
  text-align: center;
  color: var(--text-soft);
  font-size: 14px;
}

@media (max-width: 860px) {
  /* One column, art first — on a phone the cover and the video are what make
     someone decide to read the description at all. */
  .sheet__in { grid-template-columns: 1fr; }
  .sheet__plinth { width: min(250px, 62%); }
  .sheet__art .device { width: min(206px, 54%); }
  .sheet__art .device--land { width: min(420px, 92%); }
}

/* Stops the page behind scrolling while the sheet is open. Kept as a class so
   the scrollbar-width compensation and the lock are applied together — setting
   overflow alone reflows the whole page sideways as the bar disappears. */
html.sheet-open { overflow: hidden; }

/* The three floating widgets sit at z-index 999998 — far above this sheet, and
   above anything else this stylesheet could reasonably claim. Left alone they
   hover over the open card, which reads as a rendering fault rather than as
   three buttons belonging to the page underneath. They come back on close.
   Higher specificity than the widgets' own `#id` rules, so it wins wherever
   the injected <style> lands in the head. */
/* THE WASH DOWN THE RIGHT EDGE.

   Each widget's panel is a full-height sheet parked just outside the viewport
   and slid in when opened. All three carry `box-shadow: -4px 0 32px` — a
   shadow thrown LEFT — so while closed, three of them stacked bled a pale
   vertical band down the whole right side of every page. The panels darken the
   page behind them with their own scrim when open, so the shadow was doing no
   work that is missed. */
#mjgb-search-panel,
#mjgb-advisor-panel,
#mjgb-newsletter-panel { box-shadow: none !important; }

html.sheet-open #mjgb-search-fab,
html.sheet-open #mjgb-advisor-fab,
html.sheet-open #mjgb-newsletter-fab,
html.sheet-open .dock { display: none; }

/* ── THE PORTRAIT ─────────────────────────────────────────────────────────
   Hristo on stage, set behind glass: rounded, with a bright rim along the top
   edge and a diagonal sheen falling across the upper left, the way light
   catches a real pane. The sheen is kept faint — this is a photograph of a
   person playing, and a heavy overlay would wash out the very thing it is
   meant to frame. */
.portrait {
  position: relative;
  max-width: min(780px, 100%);
  margin: clamp(30px, 4vw, 52px) auto 0;
  border-radius: clamp(16px, 2vw, 26px);
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, .5);
  box-shadow:
    inset 0  1.5px 0 rgba(255, 255, 255, .9),
    inset 0 -1.5px 0 rgba(255, 255, 255, .35),
    0 26px 62px rgba(20, 17, 10, .26);
}
.portrait img { display: block; width: 100%; height: auto; }
.portrait::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(158deg,
              rgba(255, 255, 255, .20) 0%,
              rgba(255, 255, 255, .04) 34%,
              rgba(255, 255, 255, 0) 58%);
}
/* On the dark band the rim has to come from the glass, not from the page. */
.sec--dark .portrait { border-color: rgba(255, 255, 255, .28); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE DOCK — the three widget buttons on one glassy tray

   The widgets are third-party as far as this site is concerned: each pins its
   own button to the viewport corner and sets its own inline styles. The script
   adopts the three buttons into this tray and the tray owns their placement
   from then on, which is why every layout property here is `!important` — it
   has to beat the inline styles the widgets keep writing.

   That adoption also fixes, locally, a bug in the Lyra widget: it treats any
   path containing "/index.html" as the search site and drops its button into
   the Search button's slot. Fixed at source too, but this holds regardless of
   when the search tool is next deployed.

   Below the panels' own z-index (999999) so an open panel is never covered.
   ═════════════════════════════════════════════════════════════════════════ */

.dock {
  position: fixed;
  right: 0;
  bottom: clamp(16px, 4vh, 32px);
  z-index: 999997;
  display: flex;
  align-items: center;
  padding: 11px 12px 11px 0;
  border-radius: 26px 0 0 26px;
  background: rgba(255, 255, 255, .5);
  -webkit-backdrop-filter: blur(26px) saturate(1.8);
          backdrop-filter: blur(26px) saturate(1.8);
  border: 1px solid rgba(255, 255, 255, .62);
  border-right: 0;
  box-shadow:
    0 22px 54px rgba(20, 17, 10, .18),
    inset 0 1px 0 rgba(255, 255, 255, .85),
    inset 1px 0 0 rgba(255, 255, 255, .5);
  transition: transform .62s var(--ease), opacity .28s var(--ease);
}
/* Tucked away, leaving only the tab. The offset is its own width less the tab,
   so it works at any tray size without a magic number to keep in step. */
.dock.is-tucked { transform: translateX(calc(100% - 30px)); }

/* WHILE A WIDGET PANEL IS OPEN, THE TRAY STEPS ASIDE.

   Each widget turns its own button into a close X for as long as its panel is
   up. That was the right affordance when the three buttons floated loose in the
   corner — but in the tray it reads as a stray mark: the widget hides the other
   two buttons, so a lone orange X is left sitting in an otherwise empty pane of
   glass beside the arrow, duplicating the close button the panel already has in
   its own header. The panel covers this corner anyway.

   Driven off the widgets' own `.open` class rather than a flag in site.js, so
   it lands on the same frame the panel opens AND the same frame it closes —
   there is no polling interval to lag behind either edge. A browser without
   `:has()` keeps today's behaviour, which works; it is only untidy. */
.dock:has(.dock__body > .open) {
  opacity: 0;
  pointer-events: none;
}
/* Restoring a remembered state on a new page must not animate — sliding in
   from off-screen every time would read as the tray arriving fresh, which is
   precisely what remembering is meant to avoid. */
.dock.is-instant { transition: none; }

.dock__tab {
  flex: 0 0 auto;
  width: 30px;
  align-self: stretch;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  /* Brighter and more orange than the page's brass, because this one marker has
     to hold up over every band the tray floats past — paper, warm grey and the
     near-black plate sections. It clears 3:1 on white and 7:1 on black, so it
     reads on both rather than being tuned for one. */
  color: var(--signal);
  transition: color .25s var(--ease), transform .25s var(--ease);
}
.dock__tab:hover { color: var(--signal-lit); }
.dock__tab:active { transform: scale(.88); }
.dock__tab svg { width: 15px; height: 15px; display: block; transition: transform .5s var(--ease); }
/* The arrow points where the tray is going: right to push it away, and once it
   is tucked, left to pull it back. */
.dock.is-tucked .dock__tab svg { transform: rotate(180deg); }

.dock__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  /* Sized for three buttons before any of them arrive. The tray is drawn at
     once and the buttons drop in later; without a reserved height it would be
     a sliver that grows three times as they land. Width too, or an OPEN tray
     visibly widens the moment they arrive. 3 x 60 + 2 x 10 tall, 60 wide. */
  min-height: 200px;
  min-width: 60px;
}

/* NOTHING SHOWS UNTIL IT IS IN THE TRAY.

   Each widget pins its own button to the bottom-right corner the instant its
   script runs, and the dock adopts them a moment later. Left alone, every page
   load flashed three buttons into the corner and then yanked them away — the
   jitter you see when moving between pages. Hidden until seated, they simply
   appear where they belong. `visibility`, not `display`: the widgets drive
   `display` themselves to hide each other while a panel is open, and taking
   that property away from them would break it.

   Both halves are written with IDs. The hiding rule has to use them — that is
   what the widgets' own stylesheets use — and an ID beats any number of
   classes, so an attribute-selector override lost to it and left the buttons
   invisible even once they were seated. */
#mjgb-search-fab,
#mjgb-advisor-fab,
#mjgb-newsletter-fab { visibility: hidden; }
.dock__body.is-loaded #mjgb-search-fab,
.dock__body.is-loaded #mjgb-advisor-fab,
.dock__body.is-loaded #mjgb-newsletter-fab { visibility: visible; }
/* ONE fade for the set, once all three have landed. Fading each button in as
   it arrived made three separate flickers per page — the "reloading" look. */
.dock__body { opacity: 0; transition: opacity .3s var(--ease); }
.dock__body.is-loaded { opacity: 1; }

/* The buttons themselves — stripped of the fixed positioning they arrive with,
   and brought down from 76px so three of them do not make a tower. */
.dock__body [id^="mjgb-"][id$="-fab"] {
  position: static !important;
  inset: auto !important;
  margin: 0 !important;
  width: 60px !important;
  height: 60px !important;
  box-shadow: 0 2px 8px rgba(20, 17, 10, .10) !important;
}
.dock__body [id^="mjgb-"][id$="-fab"] img {
  width: 52px !important;
  height: 52px !important;
}

@media (max-width: 560px) {
  .dock { padding: 9px 10px 9px 0; }
  .dock__body { gap: 8px; min-height: 172px; min-width: 52px; }  /* 3 x 52 + 2 x 8 */
  .dock__body [id^="mjgb-"][id$="-fab"] { width: 52px !important; height: 52px !important; }
  .dock__body [id^="mjgb-"][id$="-fab"] img { width: 45px !important; height: 45px !important; }
}

@media (prefers-reduced-motion: reduce) {
  .dock, .dock__tab svg { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  .sheet__scrim, .sheet__card { animation: none; }
}

/* ── BOOK + DEVICE PAIRING ────────────────────────────────────────────────
   The WalkNessebar move: a framed device tucked behind and below the main object,
   overlapping it. Overlap comes from NEGATIVE MARGINS rather than transforms,
   so the row still reserves its true height and nothing below it gets overlaid. */

.pair {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
.pair__book { position: relative; z-index: 2; flex: 0 0 auto; width: min(300px, 62%); }
.pair__device {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  /* pulled in under the book, and dropped so the two read as depth not a row */
  margin-left: clamp(-72px, -7vw, -34px);
  margin-bottom: clamp(-26px, -3vw, -10px);
}
.pair__device .device { width: min(190px, 34vw); }
.pair__device .device--land { width: min(300px, 52vw); }

@media (max-width: 620px) {
  /* stacked, the negative overlap turns into a collision — reset it */
  .pair { flex-direction: column; align-items: center; }
  .pair__book { width: min(260px, 74%); }
  .pair__device { margin: 18px 0 0; }
}
