/* Habitat landing — design system lifted from the flask.do dissection.
   One ground, one ink at opacities, half-pixel bevels, light not fill.
   No webfonts by design: zero font bytes is part of the aesthetic. */

:root {
  --void: #09090B;            /* footer only — the single tonal break */
  /* M5: the room came up one step. At #191919 the page read as pure void on a
     phone and a panel had nothing to sit on. Note the lift alone separates
     nothing: panel fills are translucent, so they ride the ground up with it
     and the gap between them stays where it was. What buys the separation is
     the edge — the two hairlines below, and the .sh shadow that now has a
     lit ground to fall on. Move all three together or this reads flat again. */
  --ground: #1E1E1F;          /* the page. nothing else paints a background */
  --inbetween: #202020;       /* grid fill, used at ~20% alpha */
  --raised: #2C2C2C;          /* chips, pills, cards — mostly at low alpha */
  --line: rgb(78,78,78);      /* hairline sides + bottom */
  --line-top: rgb(102,102,102); /* hairline top edge: the faked overhead light */
  --ink: #FAFAFA;             /* the one ink; hierarchy is opacity, not hue */
  --ink-70: rgba(250,250,250,.7);
  --ink-50: rgba(250,250,250,.5);
  --ink-40: rgba(250,250,250,.4);
  --muted: #A1A1AA;           /* nav links, metadata — the one cool gray */
  /* Canopy: light through leaves. Light on top edges, moss and amber underneath. */
  --jade: #D6EFDC;            /* brand light. appears as glow and rim, never paint */
  --moss: #7ED08F;
  --amber: #E8B05C;
  --sand: #EFD7A4;
  --sans: ui-sans-serif, system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  --ease-out: cubic-bezier(.23, 1, .32, 1);
}

* { box-sizing: border-box; border-color: #2A2A2A; }
html { background: var(--ground); }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.6;
  letter-spacing: -0.01em;
  -webkit-font-smoothing: antialiased;
}

/* rails: desktop first. content lives at 1400, the demo canvas at 1500 */
.rail   { width: 100%; max-width: 1400px; margin: 0 auto; padding-left: 40px; padding-right: 40px; }
.rail-w { width: 100%; max-width: 1500px; margin: 0 auto; padding-left: 40px; padding-right: 40px; }

/* ---------- the pager: one idea at a time ----------
   The window never scrolls. One inner scroller, .pager, is the only scroll
   surface; every beat is one viewport tall and snaps to the top, and
   scroll-snap-stop: always makes one gesture worth at most one beat. Native
   snap is the never-halfway guarantee: there is no in-between state the page
   can be caught in, so nothing here scrubs. What stays on screen across a
   chapter (the review window, the console, the chat column) lives on a fixed
   layer under the pager and crossfades; the copy rides the pager over it.
   The three states the page script hands out: .on (owns screen centre, right
   now), .leaving (was settled here and is being scrolled away from), .gone
   (its fade is over; reset out of sight). Scenes key off those. */
html, body { height: 100%; overflow: hidden; }
.pager {
  position: relative; z-index: 20;
  height: 100vh; height: 100svh;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: contain;
  scroll-behavior: smooth;
  scrollbar-width: none;
  outline: none;
  transition: opacity .22s ease;
}
.pager::-webkit-scrollbar { display: none; }
.pager.cut { opacity: 0; }            /* a long jump is a cut, not a scroll: fade, move, fade back */
.beat {
  height: 100vh; height: 100svh;
  scroll-snap-align: start;
  scroll-snap-stop: always;           /* one gesture, at most one beat */
  position: relative; overflow: hidden;
  display: grid; align-items: center;
  padding: 88px 0 40px;               /* the nav pill floats over the top band */
}
/* a window too short for a beat: mandatory snap turns hostile, relax it */
@media (max-height: 560px) { .pager { scroll-snap-type: y proximity; } }

/* ---------- the stage: what stays ----------
   Fixed under the pager, same padding and the same grid as a two-column beat,
   so its right cell sits exactly where a beat's right cell would. pointer-
   events: none is load-bearing: without it the stage eats every wheel and
   the page stops scrolling. A beat that needs its visual touched (the demo's
   arrows, the voice pills, the pair tester) says data-lift; the stage then
   rises above the pager and only the controls marked .lift take the pointer. */
.stage {
  position: fixed; inset: 0; z-index: 10;
  pointer-events: none;
  display: grid; align-items: center;
  grid-template-columns: minmax(0, 1fr);   /* a frame's content can never widen the stage */
  overflow: hidden;
  padding: 88px 0 40px;
}
.stage.lift { z-index: 25; }
.frame {
  grid-area: 1 / 1; min-width: 0;
  opacity: 0; transform: translateY(48px);
  transition: opacity .5s ease, transform .6s var(--ease-out), filter .5s ease;
}
.frame.on { opacity: 1; transform: none; filter: none; }
/* leaving: the moment the beat starts moving (pager.js marks it and sets
   html[data-dir]) the artifact rides off with the copy — up into the distance
   when scrolling down, down when scrolling up — softening as it goes; the
   next one arrives from the other side, sharp */
.frame.leaving, .slide.leaving { opacity: 0; transform: translateY(-72px) scale(.96); filter: blur(5px); transition: opacity .4s ease, transform .45s ease, filter .4s ease; }
html[data-dir="-1"] .frame.leaving, html[data-dir="-1"] .slide.leaving { transform: translateY(72px) scale(.96); }
html[data-dir="-1"] .frame:not(.on):not(.leaving), html[data-dir="-1"] .slide:not(.on):not(.leaving) { transform: translateY(-48px); }
.frame.gone { visibility: hidden; }
.slides { position: relative; }
.slide {
  position: absolute; inset: 0;
  display: grid; align-items: center; justify-items: center;
  opacity: 0; transform: translateY(48px);
  transition: opacity .5s ease, transform .6s var(--ease-out), filter .5s ease;
}
.slide > * { min-width: 0; max-width: 100%; }   /* a slide's content fits the frame; it never sizes it */
.slide.on { opacity: 1; transform: none; filter: none; }
.slide.gone { visibility: hidden; }
.slide.gone, .slide.gone * { animation: none !important; }   /* loops stop, and restart from zero on return */
.stage.lift .frame.on .lift, .stage.lift .frame.on .slide.on .lift { pointer-events: auto; }
/* a frame's chrome says something different per slide: the lines stack in one
   cell and the page script lights the one whose data-for is the active beat */
.frame [data-for] { grid-area: 1 / 1; opacity: 0; transition: opacity .4s ease; }
.frame [data-for].on { opacity: 1; }

/* the two-column beat: copy left, visual right */
.beat-cols { display: grid; grid-template-columns: 5fr 7fr; gap: 64px; align-items: center; }
/* a cell's content never sizes the column: the 1140px demo canvas would otherwise
   take the whole rail before it is scaled to fit */
.beat-cols > * { min-width: 0; }
.beat-cols .beat-copy h2 { font-size: clamp(40px, 4vw, 64px); }
/* the copy column has room to spare next to a panel: the paragraph reads
   at 20px, not the page's 17 */
.beat-cols .beat-copy p { font-size: 20px; line-height: 1.55; max-width: 44ch; }
.beat-cols .beat-copy p.beat-kicker { font-size: 13px; line-height: 1.6; }
.beat-copy h2 + p { margin-top: 22px; }
/* the kicker under a heading: one quiet line of mono */
.beat-kicker {
  margin-top: 20px;
  font-family: var(--mono); font-size: 12px; letter-spacing: 0; line-height: 1.6;
  color: var(--ink-40);
  /* never a ch unit on a mono paragraph: Firefox on macOS resolves ch through a
     metrics path that builds the 12px mono font group wrong, and every 12px mono
     element on the page then shares it and renders proportional (found 2026-08-21
     by bisection; any stack, any element, only while a mono <p> kept p's 56ch). */
  max-width: none;
}

/* ---------- the hand-off ----------
   The snap carries position; the copy renders INTO it. Transitions, not
   keyframes, so an exit can pick the same properties up mid-flight. Reset is
   instant and happens under .gone, out of sight. */
@media (prefers-reduced-motion: no-preference) {
  .js .beat-copy > *, .js .beat-visual.rise {
    opacity: 0; transform: translateY(28px);
    transition: opacity .6s ease, transform .7s cubic-bezier(.22, 1, .36, 1);
  }
  .js .beat.on .beat-copy > *, .js .beat.on .beat-visual.rise { opacity: 1; transform: none; }
  .js .beat.on .beat-copy > :nth-child(2) { transition-delay: .09s; }
  .js .beat.on .beat-copy > :nth-child(3) { transition-delay: .18s; }
  .js .beat.on .beat-copy > :nth-child(4) { transition-delay: .27s; }
  .js .beat.on .beat-copy > :nth-child(5) { transition-delay: .36s; }
  .js .beat.on .beat-visual.rise { transition-delay: .18s; }
  .js .beat.gone .beat-copy > *, .js .beat.gone .beat-visual.rise { transition: none; }
  /* disperse: words and picture blur out where they stand — gauzy, no fling */
  .js .beat.leaving[data-exit="disperse"] .beat-copy > * {
    opacity: 0; filter: blur(10px); transform: none;
    transition: opacity .45s ease, filter .45s ease;
  }
  .js .beat.leaving[data-exit="disperse"] .beat-visual {
    opacity: 0; filter: blur(14px); transform: scale(1.05);
    transition: opacity .45s ease, filter .45s ease, transform .45s ease;
  }
}
/* a beat that is gone stops every loop in it, copy and visual alike, so they
   all start from zero together when it is back (the wiring captions ride the
   same 14s clock as the network; resetting one side alone desyncs them) */
.js .beat.gone, .js .beat.gone * { animation: none !important; }

/* ---------- the ambience: lamps in the distance ----------
   Three lamps on a fixed layer under everything, each drifting on its own
   slow clock, leaning toward whichever side of the screen the chapter keeps
   its visual on. Blur on blur, nothing ever ends in a straight line. */
.atmos { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.lamp {
  position: absolute;
  width: 44vw; height: 44vw;
  transition: transform 1.6s var(--ease-out), opacity 1.4s ease;
}
.lamp > i {
  display: block; width: 100%; height: 100%;
  border-radius: 50%;
  filter: blur(90px) saturate(1.2);
}
.lamp-1 { left: 10%; top: -12%; }
.lamp-1 > i { background: radial-gradient(circle, rgba(214,239,220,.2) 0%, rgba(214,239,220,.04) 52%, transparent 70%); animation: lamp-a 21s ease-in-out infinite; }
.lamp-2 { right: 2%; top: 0; width: 36vw; height: 36vw; }
.lamp-2 > i { background: radial-gradient(circle, rgba(126,208,143,.13) 0%, rgba(126,208,143,.03) 55%, transparent 72%); animation: lamp-b 27s ease-in-out infinite; }
.lamp-3 { left: 28%; bottom: -22%; width: 52vw; height: 38vw; }
.lamp-3 > i { background: radial-gradient(ellipse, rgba(232,176,92,.1) 0%, rgba(232,176,92,.025) 55%, transparent 74%); animation: lamp-c 31s ease-in-out infinite; }
@keyframes lamp-a { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(3vw, 2vh) scale(1.06); } }
@keyframes lamp-b { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-2vw, 3vh) scale(1.08); } }
@keyframes lamp-c { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-3vw, -2vh) scale(1.05); } }
/* the visual sits right in the card, console and chat chapters: light leans there */
html[data-chapter="1"] .lamp-1, html[data-chapter="2"] .lamp-1, html[data-chapter="3"] .lamp-1 { transform: translate(24vw, 8vh); opacity: .7; }
html[data-chapter="1"] .lamp-2, html[data-chapter="2"] .lamp-2, html[data-chapter="3"] .lamp-2 { transform: translate(-2vw, 16vh); opacity: .8; }
html[data-chapter="1"] .lamp-3, html[data-chapter="2"] .lamp-3, html[data-chapter="3"] .lamp-3 { transform: translate(12vw, 0); opacity: .7; }
/* the science leans left, the close settles low */
html[data-chapter="4"] .lamp-1 { transform: translate(-4vw, 12vh); opacity: .6; }
html[data-chapter="4"] .lamp-2 { transform: translate(-30vw, 10vh); opacity: .6; }
html[data-chapter="4"] .lamp-3 { transform: translate(-10vw, -4vh); opacity: .8; }
html[data-chapter="5"] .lamp-1 { transform: translate(12vw, 34vh); opacity: .55; }
html[data-chapter="5"] .lamp-2 { transform: translate(-12vw, 30vh); opacity: .5; }
html[data-chapter="5"] .lamp-3 { transform: translate(0, 6vh); opacity: .5; }

/* no script: the pager is still a scroller and every beat is simply present */
html:not(.js) .stage { display: none; }

h1, h2 { margin: 0; font-weight: 500; letter-spacing: -0.025em; line-height: 1.1; text-wrap: balance; }
h1 { font-size: clamp(44px, 6.2vw, 92px); }
h2 { font-size: clamp(32px, 3.8vw, 58px); }
p  { margin: 0; font-size: 17px; line-height: 1.6; letter-spacing: -0.01em;
     color: var(--ink-70); text-wrap: pretty; max-width: 56ch; }
h2 + p { margin-top: 16px; }
.visual { margin-top: 40px; }
a { color: var(--muted); text-decoration: none; transition: color .2s; }
/* buttons carry their own label colors; the global hover must not repaint them */
a:not(.btn):hover { color: var(--ink); }
/* tabindex="0" and not -1: the box grid's cells are focusable so the keyboard
   can reach a hover-only reveal, and their only focus feedback was a 1.06:1
   background. #pager is tabindex="-1" and focused programmatically, so it must
   stay out of this or the whole scroller wears a ring on load. */
a:focus-visible, button:focus-visible, [tabindex="0"]:focus-visible { outline: 2px solid #D4D4D8; outline-offset: 2px; }
.mono { font-family: var(--mono); font-size: 12px; letter-spacing: 0; }

/* the strikethrough move: the line is quieter than the text it cancels */
.strike {
  color: rgba(250,250,250,.26);   /* what is being replaced: dim, not merely secondary */
  text-decoration: line-through;
  text-decoration-color: rgba(250,250,250,.22);
  text-decoration-thickness: .06em;
  text-underline-offset: 0;
}

/* the border that isn't a border */
.eb { border: .5px solid var(--line); border-top: .5px solid var(--line-top); }
.eb-grad {
  box-shadow: inset 0 .5px 0 0 rgba(214,239,220,.25),
              inset 0 -.5px 0 0 rgba(126,208,143,.32),
              inset 0 0 0 .5px rgba(119,119,113,.4);
}

/* the shadow ladder: one everyday shadow, one hero escalation.
   M5: on a #191919 page a 20% black shadow had nowhere to fall — it landed
   inside the ground's own darkness and vanished. The lifted ground gives it
   somewhere to land, so it is worth casting properly. */
.sh    { box-shadow: 0 12px 24px -6px rgba(0,0,0,.5), 0 4px 8px -4px rgba(0,0,0,.38); }
.sh-2xl { box-shadow: 0 25px 50px -12px rgba(0,0,0,.5); }

/* buttons lit from below */
.btn {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center;
  height: 44px; padding: 0 20px;
  border-radius: 9999px; border: none;
  font-family: var(--sans); font-size: 15px; font-weight: 500;
  cursor: pointer; text-decoration: none;
}
.btn > span { position: relative; z-index: 2; }
.btn::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  border-radius: inherit; opacity: 0; transition: opacity .3s ease;
}
.btn:hover::before { opacity: 1; }
.btn-primary {
  background: linear-gradient(to top, #D6EFDC 20%, #fafafa 80%);
  color: #09090b;
  box-shadow: inset 0 1px rgba(255,255,255,.85), inset 0 -1px rgba(9,9,11,.06),
              0 0 0 .5px rgba(9,9,11,.55), 0 1px 2px rgba(0,0,0,.35), 0 4px 12px rgba(0,0,0,.22);
}
.btn-primary::before {
  background:
    radial-gradient(50% 30% at 50% 112.3%, #7ED08F, rgba(126,208,143,0)),
    radial-gradient(60% 51.9% at 50% 110%, #E8B05C, rgba(232,176,92,0)),
    radial-gradient(89% 60% at 50% 95%, #efd7a4, rgba(239,215,164,0)),
    #fafafa;
  box-shadow: inset 0 1px rgba(255,255,255,.85), inset 0 -1px rgba(9,9,11,.06);
}
.btn-secondary {
  background: rgba(44,44,44,.6);
  color: rgba(255,255,255,.7);
  box-shadow: inset 0 .5px 0 0 rgba(214,239,220,.25), inset 0 0 0 .5px rgba(119,119,113,.4);
  transition: box-shadow .2s ease;
}
.btn-secondary:hover {
  box-shadow: inset 0 .5px 0 0 rgba(214,239,220,.55), inset 0 0 0 .5px rgba(119,119,113,.4);
}
.btn-secondary::before {
  background:
    radial-gradient(50% 30% at 50% 112.3%, rgba(126,208,143,.3), rgba(126,208,143,0)),
    radial-gradient(60% 51.9% at 50% 110%, rgba(232,176,92,.3), rgba(232,176,92,0)),
    radial-gradient(89% 60% at 50% 95%, rgba(239,215,164,.3), rgba(239,215,164,0));
}

/* Pure white lettering; the square remains the brand marker. */
.wordmark {
  font-weight: 500; letter-spacing: -0.02em;
  line-height: 1.3;   /* a blockified wordmark paints only its line box: 1.0 cuts the g and y */
  color: #FFFFFF;
}
/* the full stop as a card: a small rounded square in the gradient's own jade.
   An experiment (founder, 2026-08-22); the app's index.html draws the same.
   To revert, the two `<span class="sq">` in nav.html and immersion.html
   become a `.` again and this rule goes. */
.wordmark .sq {
  display: inline-block; width: .36em; height: .36em; border-radius: 24%;
  margin: 0 .14em; vertical-align: .02em; background: #D6EFDC;
}

/* the ambient wash: three lamps, blur on blur, edge always hidden */
.wash {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(ellipse at 10% 15%, rgba(214,239,220,.35) 0%, transparent 50%),
    radial-gradient(ellipse at 50% 60%, rgba(232,176,92,.12) 0%, transparent 50%),
    radial-gradient(ellipse at 85% 35%, rgba(126,208,143,.12) 0%, transparent 50%);
  filter: blur(120px) saturate(1.2);
}

/* the lit seam: a fading rule plus the light it reflects */
.seam { position: relative; height: 1px; }
.seam::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.55) 50%, transparent 100%);
}
.seam::after {
  content: ""; position: absolute; left: 50%; transform: translateX(-50%);
  top: -5px; height: 7px; width: 160px; border-radius: 9999px;
  background: rgba(214,239,220,.25); filter: blur(12px);
}

/* a still page: reduced motion, or the dev hook ?still. Everything lands in its settled state. */
html.still *, html.still *::before, html.still *::after { animation: none !important; transition: none !important; }
/* scroll-behavior is neither an animation nor a transition, so the two rules
   above never touched it and a still page still slid a whole viewport per key
   press. pager.js says the same thing to its own scrollTo(), whose argument
   outranks this. */
html.still .pager { scroll-behavior: auto; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  .pager { scroll-behavior: auto; }
}

/* ---------- the phone ----------
   One block, one breakpoint: above 760px nothing here applies and the page is
   exactly as it was. A two-column beat stacks — the copy in a fixed band at
   the top, the visual in whatever is left — and the fixed stage runs the SAME
   grid at the SAME padding, so a staged visual lands in the band its copy left
   for it. That only holds if both grids measure the same box, which is why the
   beat and the frame stretch to their padded height here instead of centring
   on their own content. Native snap on touch is still the mechanic; nothing
   below changes how the page moves. */
@media (max-width: 760px) {
  .rail, .rail-w { padding-left: 20px; padding-right: 20px; }
  /* identical on purpose: the pager and the stage share this box */
  .beat, .stage { padding: 72px 0 max(18px, env(safe-area-inset-bottom)); }
  /* fixed inset:0 measures the LARGE viewport while the pager is 100svh — on a
     phone with a toolbar showing that is a 60-80px disagreement and the stage
     would sit that much low. Over-constrained, so bottom is the one dropped. */
  .stage { height: 100svh; }
  .beat > .beat-cols, .stage > .frame { align-self: stretch; min-height: 0; }
  .frame > .beat-cols { height: 100%; }

  /* the artifact arrives settled, and the swap is an instant. On a phone the
     frame fills most of the screen, and a whole screenful sliding up under the
     copy reads as THE PAGE entering; what should look alive is the artifact's
     own scene, which still plays on beat:on. So no travel on the stage here —
     opacity carries the change from one slide to the next. The exit transform
     goes with it: a frame pulled back inside the 500ms grace would have animated
     home from where it left, which is an entrance under another name.
     M6 (founder, on his phone): "it feels glitchy and unsmooth between them...
     there is actually quite some benefit to having a static scroller". Measured
     before this: 540-780ms AFTER the arriving beat took the screen centre the
     swap was still running — the outgoing artifact sat at ~.87 and the incoming
     at ~.12, i.e. you were still looking at the last beat's artifact under the
     new beat's words. Two things went, both phone-only:
     - the leaving blur. filter on a 100svh composited layer is the one property
       the house rule forbids and the prime jank suspect on a mid-range phone
       GPU; it also left a ~300ms window where the whole screen was an
       unreadable smear (recall -> voices was the worst).
     - the half-second crossfade. 120ms linear instead: long enough that
       crossing the snap midpoint and coming back cannot flash, short enough
       that nothing is ever caught mid-fade once the beat lands.
     Desktop keeps the rise, the blur and the .5s crossfade. */
  .frame, .slide,
  .frame.leaving, .slide.leaving,
  html[data-dir="-1"] .frame.leaving, html[data-dir="-1"] .slide.leaving,
  html[data-dir="-1"] .frame:not(.on):not(.leaving),
  html[data-dir="-1"] .slide:not(.on):not(.leaving) {
    transform: none; filter: none;
    transition: opacity .12s linear;
  }
  /* .leaving lands on the artifact that is STILL the active one — pager.js marks
     it the moment the scroller moves, which is before the beat changes — so on a
     phone it must not start anything: the artifact holds its screen until the
     next beat takes the centre, and the swap happens there, once. The moment .on
     goes the plain .leaving rule above fades it out over the same 120ms. */
  .frame.on.leaving, .slide.on.leaving { opacity: 1; }
  /* and the frame's chrome line swaps with the artifact it labels rather than a
     third of a second behind it */
  .frame [data-for] { transition: opacity .12s linear; }

  /* the copy is simply in place. Same call as the artifact above: the rise put
     the words at opacity .2-.8 for the first half-second of every beat — the
     "content popping late" half of what he was seeing — and the disperse exit
     blurred them on the way out, filter again. A phone gets a static page: the
     snap carries position, and what is on the screen it lands on is already
     there. Desktop keeps the rise and the disperse. */
  .js .beat-copy > *, .js .beat.on .beat-copy > *,
  .js .beat-visual.rise, .js .beat.on .beat-visual.rise,
  .js .beat.leaving[data-exit="disperse"] .beat-copy > *,
  .js .beat.leaving[data-exit="disperse"] .beat-visual {
    opacity: 1; transform: none; filter: none; transition: none;
  }

  .beat-cols {
    /* the copy band. A beat whose copy genuinely needs more room raises it by
       id, and must move its frame with it:
       #x .beat-cols, .frame[data-slide="x"] .beat-cols { --m-copy: 46svh } */
    --m-copy: clamp(200px, 34svh, 300px);
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: var(--m-copy) minmax(0, 1fr);
    gap: 0;
    align-items: stretch;
  }
  /* recall.html widens its own two columns by id (3fr/9fr, beat and stage
     alike), and an id outranks .beat-cols; the stack has to name both halves of
     it or that one beat stays two columns on a phone */
  #recall .beat-cols,
  html[data-beat="recall"] .stage .rail.beat-cols {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: var(--m-copy) minmax(0, 1fr);
    gap: 0;
  }
  .beat-cols > * { min-height: 0; }
  .beat-cols .beat-copy { align-self: start; }
  .beat-cols .beat-visual { align-self: center; display: grid; place-items: center; min-height: 0; }

  /* type: the column IS the measure now, so the paragraph drops its own */
  h1 { font-size: clamp(36px, 10.5vw, 46px); }
  h2, .beat-cols .beat-copy h2 { font-size: clamp(27px, 7.4vw, 34px); }
  p { font-size: 15px; }
  .beat-cols .beat-copy p { font-size: 16px; line-height: 1.55; max-width: none; }
  .beat-kicker, .beat-cols .beat-copy p.beat-kicker { font-size: 11px; margin-top: 16px; }
  h2 + p, .beat-copy h2 + p { margin-top: 12px; }

  /* the lamps: 44vw is 160px on a phone, and 160px behind a 90px blur is not
     ambience, it is nothing. About a screen each, the blur pulled in so the
     core still reads. The chapter leans are vw/vh and carry over untouched. */
  .lamp { width: 100vw; height: 100vw; }
  .lamp > i { filter: blur(72px) saturate(1.2); }
  .lamp-1 { left: -16%; top: -8%; }
  .lamp-2 { right: -18%; top: 4%; width: 92vw; height: 92vw; }
  .lamp-3 { left: -12%; bottom: -16%; width: 110vw; height: 88vw; }
}
