/* LEA'S HOUSE — scene layer.
   Loaded only on house pages, on top of site.css.

   Text over imagery is the hard problem with an illustrated background, and it
   is solved structurally rather than with a text-shadow: no words are ever
   painted onto the picture. Every label sits in a solid pill of known colour,
   so contrast is a fixed pair we can verify once, not a gamble against 19
   different illustrations.

   Motion is CSS transforms only (translate/rotate/scale) - no layout
   properties - so it stays on the compositor and costs a mid-range phone
   almost nothing. All of it stops under prefers-reduced-motion. */

/* Every plate is portrait 7:10 and a desktop browser is landscape, so an
   unconstrained scene becomes a 1200px-tall wall on a wide screen. Capping the
   width keeps it a window you look through rather than a mural you scroll.
   On a phone it still fills the width, which is where it belongs. */
.scene {
  position: relative;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--paper-2);
  /* A board-book page: soft corners, a dark keyline, and a drop so the scene
     sits ON the room colour rather than in a hole cut out of it. */
  border-radius: 20px;
  box-shadow: 0 0 0 3px var(--ink), 0 18px 34px -18px rgba(0,0,0,.6);
}
/* 70% of the band once there is a band worth taking 70% of. The old cap was a
   flat 640px, which on a 1440px screen left the scene sitting in the middle of
   the wall as a picture hung on it rather than as the room you are standing
   in.

   The 88vh guard is doing real work and is not a hedge: the plates are square,
   so a scene 70% of a 1400px band is 950px TALL, and on a laptop that is more
   than the viewport - you would scroll to see the floor, and the hotspots
   would not be on screen together. On a tall display 70% wins outright and you
   get the full width; on a short one the guard keeps the whole room in one
   view, which is the thing the old 74vh cap existed to protect. Below 46rem
   the scene still fills the width, where 70% would make it smaller than it is
   today rather than larger. */
@media (min-width: 46rem) {
  .scene { max-width: min(70%, 88vh); }
}

.scene__bg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 60%;
}

/* ---- characters ----------------------------------------------------- */

.ch {
  position: absolute;
  transform-origin: 50% 100%;
  pointer-events: none;          /* the hotspots take the taps, not the art */
}
.ch img {
  width: 100%; height: auto; display: block; will-change: transform;
  filter: drop-shadow(0 3px 5px rgba(31, 27, 23, .18));
}

/* The figure moves, the shadow does not - the extraction stripped the drop
   shadow off the source sheets, so without this everyone floats. */
.ch::after, .bch::after {
  content: ""; position: absolute; left: 50%; bottom: 0; z-index: -1;
  width: 84%; height: 6%;
  transform: translate(-50%, 42%);
  background: radial-gradient(ellipse at center,
              rgba(31,27,23,.34) 0%, rgba(31,27,23,.17) 46%, rgba(31,27,23,0) 72%);
}
.ch img, .bch img { transform-origin: 50% 100%; }

/* ---- what the room says ----------------------------------------------- */
/* The line the scene head used to print as a caption, said by whoever is
   standing in the room. It hangs off the speaker's own box, so it goes where
   they go. Same badge as the hotspot labels - marigold, ink border, ink text -
   because a child should not have to learn two kinds of label in one picture,
   and because it keeps every word on flat colour rather than on the plate.
   It does NOT inherit the bob: a caption that bounces is hard to read, so only
   the figure animates. */
.say {
  position: absolute; bottom: 100%; left: 50%; z-index: 3;
  transform: translate(-50%, -12px);
  margin: 0; width: max-content; max-width: min(52vw, 250px);
  background: var(--marigold); color: var(--ink);
  border: 2px solid var(--ink); border-radius: 18px;
  padding: 10px 15px; font-size: 15px; font-weight: 700; line-height: 1.25;
  box-shadow: 0 6px 16px -6px rgba(31,27,23,.55);
}
/* A speaker near a wall gets the bubble anchored to their near edge, so it
   opens into the room instead of off the picture. Same rule the hotspot pills
   use, and the tail moves with it so it still points at whoever is talking. */
.say--start { left: 0; transform: translate(0, -12px); }
.say--start::after { left: 24px; }
.say--end { left: auto; right: 0; transform: translate(0, -12px); }
.say--end::after { left: auto; right: 24px; }

/* A 350px square already holds three hotspot labels; a full-width bubble on
   top of them buries one. Smaller on a phone so they can coexist. */
@media (max-width: 29.99rem) {
  .say { max-width: min(58vw, 185px); font-size: 13.5px; padding: 8px 12px; }
}
.say::after {
  content: ""; position: absolute; left: 50%; bottom: -7px;
  width: 12px; height: 12px; background: var(--marigold);
  border-right: 2px solid var(--ink); border-bottom: 2px solid var(--ink);
  transform: translateX(-50%) rotate(45deg); border-radius: 0 0 2px 0;
}
.ch--bob  img { animation: bob  4.2s ease-in-out infinite; animation-delay: inherit; }
.ch--tip  img { animation: tip  6.0s ease-in-out infinite; animation-delay: inherit; }
.ch--sway img { animation: sway 5.2s ease-in-out infinite; animation-delay: inherit; }

@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-3.6%) rotate(-1.4deg); }
}
@keyframes tip {
  0%, 100% { transform: rotate(-4deg); }
  50%      { transform: rotate(4deg); }
}
@keyframes sway {
  0%, 100% { transform: rotate(-1.2deg) translateX(0); }
  50%      { transform: rotate(1.2deg) translateX(1.2%); }
}

/* ---- hotspots ------------------------------------------------------- */
/* A child who cannot read needs to see what is touchable, so the target
   breathes and carries a picture as well as a word. */

.spot {
  position: absolute;
  display: block;
  min-width: 44px; min-height: 44px;
  text-decoration: none;
  border-radius: 18px;
}
.spot__ring {
  position: absolute; inset: -5px;
  border: 4px solid var(--marigold);
  border-radius: 22px;
  box-shadow: 0 0 0 3px var(--ink), inset 0 0 0 2px var(--ink);
  animation: breathe 2.8s ease-in-out infinite;
  animation-delay: var(--ring-delay, 0ms);
}
.spot__pill {
  position: absolute; top: -14px;
  left: 50%; transform: translate(-50%, -100%);
  max-width: min(78vw, 300px);
  display: inline-flex; align-items: center; gap: 7px;
  white-space: nowrap;
  /* A speech bubble rather than a system label: marigold, with a 2px ink
     border. The border is not decoration - marigold alone is mid-luminance
     and has nothing to sit against on cream walls and pale wood, which is the
     same reason the hotspot ring is marigold sandwiched in ink. The ink
     outline is what makes the bubble hold its shape on any of the 19 plates;
     ink on marigold is 9.4:1 for the label itself. */
  background: var(--marigold); color: var(--ink);
  border: 2px solid var(--ink);
  font-weight: 700; font-size: 15px; line-height: 1;
  padding: 11px 16px; border-radius: 999px;
  box-shadow: 0 6px 16px -6px rgba(31, 27, 23, .55);
}
.spot__pill::after {          /* the little tail, so the pill points at the thing */
  content: ""; position: absolute; left: 50%; bottom: -7px;
  width: 12px; height: 12px; background: var(--marigold);
  /* rotate(45deg) puts the original bottom-right corner at the point, so those
     are the two edges that need the border to carry on round the tail. */
  border-right: 2px solid var(--ink); border-bottom: 2px solid var(--ink);
  transform: translateX(-50%) rotate(45deg); border-radius: 0 0 2px 0;
}

/* Vertical placement. Above by default; inside the top edge when the hotspot
   is tall but sits high; below when it is small and high. Without this, a
   label on anything near the top of the picture renders outside the frame -
   the bedroom dream bubble starts at 5%. */
.spot--inside .spot__pill { top: 12px; transform: translate(-50%, 0); }
.spot--below  .spot__pill { top: auto; bottom: -14px; transform: translate(-50%, 100%); }
/* Pointing up instead of down: the point is now the original top-left corner,
   so the border moves to the other two edges or the tail loses its outline. */
.spot--inside .spot__pill::after,
.spot--below  .spot__pill::after {
  bottom: auto; top: -7px;
  border-right: 0; border-bottom: 0;
  border-top: 2px solid var(--ink); border-left: 2px solid var(--ink);
  border-radius: 2px 0 0 0;
}
.spot--inside.spot--start .spot__pill,
.spot--below.spot--start  .spot__pill { left: 0; transform: translate(0, 0); }
.spot--below.spot--start  .spot__pill { transform: translate(0, 100%); }
.spot--inside.spot--end .spot__pill,
.spot--below.spot--end  .spot__pill { left: auto; right: 0; transform: translate(0, 0); }
.spot--below.spot--end  .spot__pill { transform: translate(0, 100%); }
/* Hotspots near an edge align their pill to that edge instead of centring it,
   or the label hangs off the picture. */
.spot--start .spot__pill { left: 0; transform: translate(0, -100%); }
.spot--start .spot__pill::after { left: 26px; }
.spot--end   .spot__pill { left: auto; right: 0; transform: translate(0, -100%); }
.spot--end   .spot__pill::after { left: auto; right: 20px; }
/* The icon was marigold because the pill was ink. The pill is marigold now, so
   the icon takes the ink the label takes. */
.spot__icon { width: 18px; height: 18px; flex: 0 0 auto; color: var(--ink); }

.spot:hover .spot__ring,
.spot:focus-visible .spot__ring { animation: none; inset: -6%; border-color: #fff; }
/* Straight inversion on hover - ink fill, paper label, 14.9:1. The old rule
   set the background to blue and left the colour alone, which worked while the
   label was paper and would be ink-on-blue at 1.4:1 now. */
.spot:hover .spot__pill,
.spot:focus-visible .spot__pill { background: var(--ink); color: var(--paper); }
.spot:hover .spot__pill::after,
.spot:focus-visible .spot__pill::after { background: var(--ink); }
.spot:focus-visible { outline: 4px solid var(--marigold); outline-offset: 8px; }

@keyframes breathe {
  0%, 100% { transform: scale(1);    opacity: .9; }
  50%      { transform: scale(1.09); opacity: 1; }
}

/* ---- the room switcher ---------------------------------------------- */
/* Persistent, pictures first, always in the same place - the pattern every
   pre-reader navigation uses, because a four-year-old cannot read the words. */

/* Five tiles have to fit one row on a 375px phone: 5x60 + 4x5 + 2x8 = 336.
   Wrapping pushed a room below the fold; at 70px wide the fifth item pushed
   the whole page into horizontal scroll. The rail can scroll as a last resort
   on anything narrower, with the scrollbar hidden. */
/* Doors, not thumbnails. At 46px these were the smallest thing on the site and
   they are the primary way around it; at 150px the plate reads as a room you
   could walk into rather than as a beige square. Five 150px doors need about
   790px, so the rail leans on the horizontal scroll it already had rather than
   wrapping - a wrapped rail put a room below the fold, which is the thing the
   original comment was written about. --door falls with the viewport so a
   phone still gets as many doors on screen as the width honestly allows. */
.rooms {
  /* Sized to FIT, not to a guessed floor. The rail holds five rooms and its
     scrollbar is hidden, so anything that does not fit is not just off-screen,
     it is off-screen with nothing to say so - a child has no way to know two
     rooms are over there. At 92px five doors came to 526px inside a 373px rail
     on a phone. (viewport - the 16px of padding - the four 5px gaps) / 5 is
     the width at which all five are visible; the 150px cap takes over at about
     800px, where they fit anyway.
     84px is everything the doors do not get: ~17 of scrollbar that 100vw
     counts and the rail never sees, 16 of rail padding, 20 of gaps, and 6 of
     padding on each of the five tiles. Sized against the viewport alone the
     row came out 47px too wide; against viewport-minus-scrollbar, 29px, which
     was the tile padding. Where the scrollbar is an overlay and costs nothing
     the doors land a few px under, which is the harmless direction. */
  --door: clamp(52px, calc((100vw - 84px) / 5), 150px);
  display: flex; gap: 5px; flex-wrap: nowrap;
  padding: 14px 8px 22px; margin: 0; list-style: none;
  background: var(--paper-2); border-top: var(--seam) solid var(--ink);
  overflow-x: auto; scrollbar-width: none;
  max-width: 100%;              /* the rail scrolls; the page must not */
  /* NOT justify-content: center. Centred flex content that overflows spills
     equally off both ends and scrollLeft cannot go below 0, so the first tile
     was clipped and unreachable at 360px. Auto margins on the end items centre
     the rail when it fits and collapse to zero when it does not, which leaves
     the whole rail scrollable. */
  justify-content: flex-start;
}
.rooms li:first-child { margin-inline-start: auto; }
.rooms li:last-child  { margin-inline-end: auto; }
.rooms::-webkit-scrollbar { display: none; }
.rooms li { flex: 0 0 auto; }
/* The label is what sets a tile's width, and "The living room" on one line
   made a 85px tile: five of those overflow a phone. Capped, the words wrap to
   two lines and five tiles come to 336px, inside a 360px viewport. */
.rooms a > span, .rooms span[aria-current] > span { max-width: var(--door); }
@media (min-width: 30rem) {
  .rooms { gap: 10px; padding-inline: var(--gut); }
  .rooms a, .rooms span[aria-current] { min-width: var(--door); padding-inline: 8px; }
}
.rooms a, .rooms span[aria-current] {
  display: flex; flex-direction: column; align-items: center; gap: 0;
  min-width: var(--door); min-height: 76px; padding: 0 3px;
  text-decoration: none; color: var(--ink);
  border-radius: 16px; border: 0;
  font-size: clamp(11px, calc(var(--door) / 12), 14px);
  font-weight: 700; text-align: center; line-height: 1.15;
  transition: transform 160ms cubic-bezier(.2,.7,.3,1);
}
/* The label rides the bottom edge of the door in a marigold pill, the way the
   hotspot labels do - one badge treatment across the whole house instead of
   two. The pill is what carries the word off the plate, so the rule about
   never setting type on artwork still holds. */
.rooms a > span, .rooms span[aria-current] > span {
  background: var(--marigold); color: var(--ink);
  border: 2px solid var(--ink); border-radius: 999px;
  padding: 5px 11px; margin-top: -14px; position: relative; z-index: 1;
  box-shadow: 0 2px 0 var(--ink);
}
/* The room you are in stands up, in its own colour. */
.rooms [aria-current] { transform: translateY(-4px); }
.rooms [aria-current] img { border-color: var(--room); }
.rooms a:hover { transform: translateY(-3px); }
/* Every tile wears its own room's colour, not only the one you are standing
   in. At 46px the four plates are the same warm neutral and the crops were
   near-identical beige squares; the colour is what makes them tellable apart,
   and it is the colour that room is dressed in when you get there. */
.rooms img {
  width: var(--door); height: var(--door);
  object-fit: cover; object-position: 50% 62%;
  border-radius: 14px;
  /* A paper mount and a drop, so the door reads as a thing standing on the
     floor strip rather than a picture printed on it. The keyline stays the
     room's own colour at FULL strength - it is a 2px accent on paper, where
     the washed wall colour would not clear 3:1. */
  border: 5px solid var(--paper);
  outline: 2px solid var(--room, rgba(31,27,23,.22));
  box-shadow: 0 10px 18px -8px rgba(31,27,23,.55);
}
.rooms [aria-current] img { outline-width: 3px; }

/* ---- scene furniture ------------------------------------------------ */

/* ---- the room ------------------------------------------------------- */
/* Each room carries one colour on --room, chosen for what the room is FOR
   rather than sampled from the plate: the plates are all the same warm
   wood-and-wall neutral and the kitchen has no distinctive hue at all, so
   sampling would have produced four identical browns. The colours reuse the
   book families, so the house and the catalogue read as one system. */

/* The wall is MIXED TOWARD PAPER, not flooded. At full strength the page
   carried the colour and the plates sat inside it as small bright squares; at
   58% the artwork is the most saturated thing on the screen, which is the
   whole point of the change.

   It inverts the text problem, and that was measured rather than assumed.
   Against the six washed walls plus the washed banner, #F6F1E7 lands at
   2.66-3.25:1 and fails every one of them; ink is 4.67:1 at its worst (bedroom
   indigo, #8981A3) and 5.2-11.4 everywhere else. So a pale wall takes DARK
   type, and --room-tx is ink. --wall carries the mix so --room stays the true
   colour for the things that still need it at full strength: the rail tiles'
   borders and the "you are here" marker, which are small accents on paper and
   have to clear 3:1. */
.room {
  --wall: color-mix(in srgb, var(--room) 58%, var(--paper));
  --room-tx: var(--ink);
}
/* the masthead keyline agrees with the room you are standing in */
body:has(.room) .bar { border-bottom-color: var(--ink); }

.scene-head {
  background: var(--wall, var(--room)); color: var(--room-tx);
  padding: clamp(20px,3vw,36px) var(--gut) clamp(22px,3vw,34px);
  border-top: var(--seam) solid var(--ink);
}
.scene-head h1 {
  margin: 0 0 .18em; color: var(--room-tx);
  font-variation-settings: "SOFT" 100, "WONK" 1;
}
/* Ink, not the old #E7DECB and not --ink-soft: on the washed walls the dim
   cream is 2.24-2.74:1 and ink-soft is 2.12-2.59:1, so both fail. The lede
   separates from the heading by size and weight instead of by colour. */
.scene-head p {
  margin: 0; color: var(--room-tx); font-weight: 500;
  font-size: clamp(16px,15px+.4vw,19px);
}

.scene-wrap {
  background: var(--wall, var(--room));
  padding: 0 var(--gut) clamp(22px,3vw,40px);
}

@media (prefers-reduced-motion: reduce) {
  .ch img, .bch img, .spot__ring { animation: none !important; }
}

/* ---- the banner ------------------------------------------------------ */
/* The one landscape element on a site otherwise made of portrait plates, so
   the front page has something that fills a wide screen. The cast stands on
   its bottom edge using the same cut-outs the rooms use. */

.banner {
  position: relative;
  /* Washed with the rest of the bands. The banner's type is already ink, so
     the paler ground costs it nothing - ink on the washed marigold is 11.4:1,
     up from 9.4:1 on the flood. */
  background: color-mix(in srgb, var(--marigold) 58%, var(--paper))
              url("/banner-pattern.svg") repeat;
  background-size: 140px;
  border-bottom: var(--seam) solid var(--ink);
  padding: clamp(22px,3.5vw,44px) var(--gut) 0;
  display: grid; gap: 0;
  overflow: hidden;
}
.banner__logo { width: clamp(42px,6vw,58px); height: auto; margin-bottom: 10px; }
.banner__kicker {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 11px; letter-spacing: .07em; color: var(--ink);
  margin: 0 0 8px; text-transform: uppercase;
}
.banner h1 {
  margin: 0 0 .3em; color: var(--ink);
  font-size: clamp(38px, 30px + 3.4vw, 76px);
  font-variation-settings: "SOFT" 100, "WONK" 1;
}
.banner__lede {
  margin: 0; color: var(--ink); font-weight: 500;
  font-size: clamp(16px, 15px + .5vw, 20px); max-width: 34ch;
}
.banner__text { position: relative; z-index: 2; }

/* The floor the cast stands on, and the thing that stops the pattern running
   straight into the room below. */
.banner__ground {
  position: absolute; left: 0; right: 0; bottom: 0; height: 34%;
  /* Still a shade lighter than the band it sits in, so the cast reads as
     standing on a floor - mixed from the same marigold so the two move
     together if the wash ever changes. */
  background: color-mix(in srgb, var(--marigold) 30%, var(--paper));
  border-top: var(--seam) solid var(--ink);
  z-index: 0;
}
.banner__cast { z-index: 1; }

/* One slow drift on the whole pattern - liveliness that costs nothing and
   never moves under the words, because the text sits on its own layer. */
@media (prefers-reduced-motion: no-preference) {
  .banner { animation: drift 90s linear infinite; }
}
@keyframes drift {
  from { background-position: 0 0; }
  to   { background-position: 480px 0; }
}

/* The cast is decoration: it sits in its own strip so it never overlaps the
   words, which keeps the text on flat colour rather than on artwork. */
.banner__cast {
  position: relative; height: clamp(150px, 36vw, 200px);
  margin-top: clamp(14px, 2vw, 22px);
}
/* Lea's greeting stands above her head, and below 46rem the banner is a single
   column - so without this the bubble comes down on the lede. Above 46rem the
   cast has its own column with empty ground over it and needs no reservation. */
.banner__cast--says { margin-top: calc(clamp(14px, 2vw, 22px) + 66px); }
/* The cast stands at fixed percentages of its strip, so a strip as wide as a
   tablet spread the three of them from edge to edge with a void between each.
   Capped, they stay a group standing together at the left, under the words. */
/* 460 held three; five need the room. At 300px tall the two adults are ~91px
   wide each and the children 88/68/66, so the figures come to about 400px -
   560 leaves them standing apart rather than shoulder to shoulder. */
@media (min-width: 30rem) {
  .banner__cast { max-width: 560px; }
}
/* Height-driven so a figure can never grow taller than its strip. */
.bch {
  position: absolute; bottom: 0; width: auto;
  transform-origin: 50% 100%; will-change: transform; pointer-events: none;
}
.bch img {
  height: 100%; width: auto; display: block; will-change: transform;
  filter: drop-shadow(0 3px 5px rgba(31,27,23,.18));
}

@media (min-width: 46rem) {
  .banner {
    grid-template-columns: 1fr minmax(280px, 42%);
    align-items: end; column-gap: clamp(20px,3vw,48px);
    padding-bottom: 0;
  }
  /* The ground is 34% of the banner, and the banner's height comes from
     whichever column is taller. At 1024 that is the text, so the seam was
     drawn straight through the descenders of "My Daddy". Tying the band to
     the cast strip it is the floor of, and then clearing it in the text
     column, keeps the line under the words at every width. */
  .banner { --cast-h: clamp(200px, 23vw, 300px); --band-h: calc(var(--cast-h) * .34); }
  .banner__ground { height: var(--band-h); }
  .banner__text { padding-bottom: calc(var(--band-h) + 16px); }
  .banner__cast, .banner__cast--says { height: var(--cast-h); margin-top: 0; }
}

/* ---- icon chips ------------------------------------------------------ */
/* Secondary hotspots draw the icon only. Three full labels in one scene
   overlapped each other and read as clutter. The ring is what tells a child
   something is touchable; the word is for the adult, so it appears on hover
   or focus and lives permanently in the link's accessible name. */

.spot--chip .spot__pill { padding: 11px; gap: 0; }
.spot--chip .spot__word {
  max-width: 0; overflow: hidden; opacity: 0; white-space: nowrap;
  transition: max-width 220ms cubic-bezier(.2,.7,.3,1), opacity 160ms ease,
              margin-left 220ms cubic-bezier(.2,.7,.3,1);
}
.spot--chip:hover .spot__word,
.spot--chip:focus-visible .spot__word { max-width: 15rem; opacity: 1; margin-left: 7px; }

/* ---- who lives here -------------------------------------------------- */

.who { background: var(--wall, var(--room)); padding: 0 var(--gut) clamp(26px,4vw,52px); }
.who__grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: clamp(16px,2.6vw,30px);
  grid-template-columns: repeat(auto-fit, minmax(min(220px,100%), 1fr));
}
.who__card {
  background: var(--paper); border: 3px solid var(--ink); border-radius: 20px;
  padding: 0 clamp(14px,2vw,22px) clamp(16px,2vw,24px);
  box-shadow: 0 14px 26px -16px rgba(0,0,0,.55);
}
/* A fixed stage per card so the figures share a floor line and their real
   height differences read as height, not as layout noise.
   The floor is 150px rather than 210 because the stage only earns its keep
   when cards sit side by side. Under 220px of card the grid falls to one
   column, and a shared floor line has nothing left to share: the old floor
   then bought nothing but empty card, seven times down a phone. The
   proportions still hold; they are just read one at a time. */
.who__stage {
  height: clamp(150px, 32vw, 280px);
  display: flex; align-items: flex-end; justify-content: center;
  margin-bottom: 12px;
}
.who__stage img { height: var(--fig-h, 100%); width: auto; }
.who__name { margin: 0 0 .25em; font-size: clamp(21px,19px+.9vw,28px); }
.who__age {
  display: block; font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 11px; letter-spacing: .07em; color: var(--ink-soft);
  text-transform: uppercase; font-weight: 400; margin-top: 4px;
}
.who__line { margin: 0; color: var(--ink-soft); font-size: 15px; }

/* One column means one card across the whole width, and a small figure
   centred over a paragraph leaves the card empty down both sides. Turned on
   its side it uses the width it has: the figure gets a proper column to stand
   in and the card comes down to about the height of the words. */
@media (max-width: 30rem) {
  .who__card {
    display: grid; grid-template-columns: 108px 1fr;
    grid-template-rows: auto auto; align-content: center;
    gap: 4px 16px;
    padding: clamp(14px,2vw,18px) clamp(14px,2vw,22px);
  }
  /* The figure takes the whole left column; the words stack in the right one.
     Without this the name lands beside the figure and the line drops under it. */
  .who__stage { grid-row: 1 / -1; grid-column: 1; height: 148px; margin-bottom: 0; }
  .who__name { grid-column: 2; align-self: end; font-size: 22px; }
  .who__line { grid-column: 2; align-self: start; }
}

/* ---- static pages, standing in a room -------------------------------- */
/* About, contact, the colouring sheets and the 404 were flat cream panels
   while every other page stood in a coloured, papered room, so they read as a
   different site. They stand in one now. Long prose still sits on paper - a
   card on the wall, the way the cast page does it - because a page of text
   reversed out of a saturated colour is not something to read twice. */

.page-wall {
  background: var(--wall, var(--room));
  padding: clamp(20px,3vw,36px) var(--gut) clamp(28px,4vw,54px);
}
.page-card {
  background: var(--paper); color: var(--ink);
  border: 3px solid var(--ink); border-radius: 20px;
  padding: clamp(18px,2.6vw,32px) clamp(16px,2.4vw,30px);
  box-shadow: 0 14px 26px -16px rgba(0,0,0,.55);
  max-width: 46rem;
}
.page-card + .page-card { margin-top: clamp(16px,2.4vw,28px); }
.page-stack { min-width: 0; max-width: 46rem; margin-inline: auto; }

/* Three columns on a wide screen: Lea, the words, then Max and Teddy. They all
   stand on the seam at the foot of the wall, which is why this wall has no
   bottom padding - the seam is the floor.

   Below 62rem there is no room for a figure either side of a readable measure,
   so the words take the full width and the two groups stand under them, still
   left and right of each other. */
.page-wall--split {
  padding-bottom: 0;
  display: grid;
  column-gap: clamp(12px, 2vw, 28px);
  align-items: end;
  grid-template-columns: 1fr 1fr;
  grid-template-areas:
    "text text"
    "lea  maxteddy";
}
.page-wall--split .page-stack {
  grid-area: text; padding-bottom: clamp(28px,4vw,54px);
}
.wall-cast { position: relative; height: clamp(170px, 40vw, 300px); }
.wall-cast--lea { grid-area: lea; }
.wall-cast--max { grid-area: maxteddy; }

/* Contact is the same wall with one figure instead of two.

   It stands in about's room by design - same colour, same wallpaper, the nav
   treats them as one section - so putting about's Lea AND Max on it too made
   the two pages read as one page shown twice. One figure keeps the wall
   inhabited without repeating the other page, and Max and Teddy are the right
   two for a page whose whole job is "come and say something".

   The words take a readable measure and the figure takes what is left, rather
   than the two sharing a wall the way about's three columns do. Below 54rem
   there is no room for both, so the figure stands under the words. */
.page-wall--aside {
  padding-bottom: 0;
  display: grid; align-items: end;
  column-gap: clamp(16px, 3vw, 48px);
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "text" "figure";
}
.page-wall--aside .page-stack {
  grid-area: text; margin-inline: 0;
  padding-bottom: clamp(28px, 4vw, 54px);
}
/* One slot, whoever is standing in it. The split wall needs two named areas
   because it holds Lea AND Max at once; this one holds either, so it beats
   both --lea and --max rather than making the page pick a matching modifier. */
.page-wall--aside .wall-cast { grid-area: figure; }
@media (min-width: 54rem) {
  .page-wall--aside {
    grid-template-columns: minmax(0, 46rem) minmax(190px, 1fr);
    grid-template-areas: "text figure";
  }
}

@media (min-width: 62rem) {
  .page-wall--split {
    grid-template-columns: minmax(150px, 1fr) minmax(0, 46rem) minmax(190px, 1fr);
    grid-template-areas: "lea text maxteddy";
    column-gap: clamp(20px, 3vw, 48px);
  }
}
/* A second topic inside one card needs to be further from the first than the
   first is from itself. Measured before this rule: 18px between the words and
   the address that belongs to them, 15px between that address and the next
   heading - so the button sat closer to the section it was not part of. */
.card-part {
  margin-top: clamp(22px, 2.6vw, 32px);
  padding-top: clamp(18px, 2.2vw, 26px);
  border-top: 1px solid var(--rule);
}
.page-card > :first-child { margin-top: 0; }
.page-card > :last-child { margin-bottom: 0; }
/* A colouring sheet is white line art, and white on brick with nothing round
   it read as a hole in the wall rather than a sheet of paper. Each one gets a
   frame - the same card the prose sits on - which also puts its caption and
   its download button back on paper, where the ordinary button treatment
   works. Bare on the wall they were a light button on a mid-red ground and a
   caption fighting the pattern. */
.page-wall .sheets { margin-top: 0; }
.page-wall .sheet {
  display: flex; flex-direction: column;
  background: var(--paper);
  border: 3px solid var(--ink); border-radius: 18px;
  padding: 14px 14px 16px;
  box-shadow: 0 14px 26px -16px rgba(0,0,0,.55);
}
/* The button sits on the bottom edge of every card, so a two-line caption
   next to a one-line caption does not stagger the row. */
.page-wall .sheet .btn { margin-top: auto; }

/* One book, its cover, and the two sheets that came out of it.

   The sheets used to be one flat grid of thirty, where a pair held together
   only because the column count was forced even - a rule that had already
   broken once - and the only thing naming the book was 12.5px of grey caption
   repeated thirty times. The group is a real object now, so the pairing
   survives any column count and the title is said once, at heading size, as
   the way back to the book.

   No box around the group: the sheets are already framed cards, and a card of
   cards reads as two nested objects rather than one. Proximity and the heading
   do the grouping, which is what the wall is for. */
.sheet-books {
  display: grid;
  /* Proximity is the only thing saying "these two came out of that book", so
     the space between groups has to beat the space inside one by enough to
     read at a glance. Measured at 36 vs 47px on the first cut - a ratio of
     1.3, which the eye does not resolve as grouping at all. */
  column-gap: clamp(30px, 4.4vw, 64px);
  row-gap: clamp(32px, 4vw, 54px);
  grid-template-columns: minmax(0, 1fr);
  margin: 0; padding: 0; list-style: none;
}
@media (min-width: 58rem) { .sheet-books { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 90rem) { .sheet-books { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.sheet-book { min-width: 0; }
/* A book's two sheets are one object, so they sit tight; the catalogue gap
   belongs between books, not down the middle of a pair. */
.sheet-book .sheets { gap: clamp(12px, 1.2vw, 18px); }
/* A real h2, so it takes the display face from the type scale rather than
   naming a font of its own, and so fifteen groups under one h1 come out as a
   document outline instead of fifteen styled spans. */
.sheet-book__t {
  font-size: clamp(17px, 1.5vw, 20px); line-height: 1.15;
  margin: 0 0 12px;
}
.sheet-book__head {
  display: flex; align-items: center; gap: 12px;
  text-decoration: none; color: var(--room-tx, var(--ink));
  /* The whole row is the target, and it has to clear 44px on a phone. */
  min-height: 44px;
}
.sheet-book__cover {
  flex: 0 0 auto; width: 56px; height: auto;
  border: 2px solid var(--ink); border-radius: 4px;
  box-shadow: 2px 2px 0 var(--ink);
}
.sheet-book__head:hover span { text-decoration: underline; }

/* ---- the series index ------------------------------------------------ */
/* One card per series: a cover you can buy, the name, what it is, and the way
   in. Horizontal on anything with room, stacked below that, because a 170px
   cover beside a 34ch paragraph needs about 30rem before it stops being two
   cramped columns. */

.series-card {
  display: grid; gap: clamp(14px, 2.4vw, 28px);
  align-items: center;
  max-width: 46rem; margin-inline: auto;
}
.series-card__cover img {
  display: block; width: 100%; max-width: 190px;
  border: 1px solid rgba(31,27,23,.3);
  box-shadow: 0 6px 14px -8px rgba(0,0,0,.7);
}
.series-card__text { min-width: 0; display: block; }
.series-card__text h2 { margin: 0 0 .3em; }
.series-card__text p { margin: 0; color: var(--ink-soft); }
.series-card .btns { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
@media (min-width: 30rem) {
  /* Centred, not top-aligned: a 243px cover beside three lines of text left
     the card with a hole under the words. */
  .series-card { grid-template-columns: auto 1fr; align-items: center; }
  .series-card__cover img { width: 170px; }
}

/* ---- room wallpaper --------------------------------------------------- */
/* A flat band under a picture-book scene reads bare. Each room carries its own
   motif - houses at the front door, books in the living room, crayons in the
   kitchen, stars at bedtime - so the pattern also says where you are.
   Painted through a mask, so one white overlay takes whatever colour the room
   is set to and no per-colour asset is needed. */

.scene-head, .scene-wrap, .who, .page-wall {
  position: relative; isolation: isolate;
}
/* INK, not white. A white overlay only reads on a flooded wall; against the
   washed grounds it all but disappears, and lifting its opacity to compensate
   just fogs the colour. Drawn in ink at 17% the motif becomes soft line-work
   on a pale ground - closer to wallpaper, and it no longer competes with the
   plates for the eye. */
.scene-head::before, .scene-wrap::before, .who::before,
.page-wall::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: var(--ink); opacity: .17;
  -webkit-mask-image: var(--pattern); mask-image: var(--pattern);
  -webkit-mask-repeat: repeat; mask-repeat: repeat;
  -webkit-mask-size: 140px; mask-size: 140px;
}
/* The head and the stage share one continuous sheet of wallpaper. */
.scene-wrap::before { -webkit-mask-position: 0 -18px; mask-position: 0 -18px; }

/* ---- the bookshelf ---------------------------------------------------- */
/* The living-room hotspot is a bookcase, so the books page should feel like
   standing at one. Ledges are drawn as a repeating background at a fixed row
   height, which is the only way to get a rule under every row of a wrapping
   grid. Books lean; the one you point at stands up. */

.shelf-room { --ledge: rgba(31,27,23,.55); }
/* On the night wall an ink ledge is invisible, so the shelf takes a light one
   there. The books are the same books on the same shelf; only the wall
   behind them changes. */
.panel--night.shelf-room { --ledge: rgba(246,241,231,.26); }
.panel--night .bookcase { margin-top: 18px; }
.shelf-wall {
  position: relative; isolation: isolate;
  background: var(--wall, var(--room));
  padding: clamp(14px,2.5vw,26px) var(--gut) clamp(24px,3vw,40px);
  border-top: var(--seam) solid var(--ink);
}
/* The walls used to be dark, so buttons standing on them took the same
   inversion the dark panels use - paper fill, paper text on the outlined
   variant. Washed to 58% the walls are pale, and that inversion is now the
   failure it was written to prevent: paper text lands at 2.7-3.3:1 on them.
   The ordinary treatment is already right for a light ground (ink fill, paper
   label, ink border), so the override is gone rather than rewritten. Only the
   spacing rule survives. .panel--night keeps its own inversion; those panels
   are still dark. */
.shelf-wall .btns,
.page-wall > .btns { margin: clamp(20px,2.6vw,32px) 0 0; }

.shelf-wall::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: var(--ink); opacity: .17;
  -webkit-mask: var(--pattern) repeat; mask: var(--pattern) repeat;
  -webkit-mask-size: 140px; mask-size: 140px;
}

.bookcase {
  /* The row sets the cover's height (row - 70 for the label) and its 7:10
     aspect then sets the width, so the row height decides how much of a
     column a book fills. At 228px that was 111px of a 157px column and the
     books looked lost on a mostly bare shelf.

     Flat, not a vw clamp. Tracks are minmax(150px, 1fr), so a column is 150px
     at its narrowest and stretches from there; a viewport-scaled row made the
     cover wider than the column at some widths, where max-width shrank the
     image, which shortened it, which lifted it off the ledge. 270 gives a
     140px cover that fits the narrowest column everywhere. */
  /* The row is the cover plus the label, and the two used to be tangled: the
     label's height was written in one place and the ledge's position derived
     from it by hand in another, as "row - 64" and "row - 58". Naming the label
     makes the relationship explicit - the cover ends at row minus label minus
     its 6px of padding, and the ledge is the 6px band it stands on.
     300 and 74 give the same 154px cover 284 and 58 did; the extra 16px is
     the label growing from 11.5px to 13.5px, not the cover shrinking. */
  --bookcase-label: 74px;
  --bookcase-row: 300px;
  display: grid; gap: 0 clamp(6px, 1.6vw, 18px);
  /* A bookcase, not a warehouse shelf. auto-fill against a 116px minimum put
     fourteen columns on a 1568px screen and shrank every cover to a postage
     stamp, which is the opposite of standing in front of a shelf. Capped at
     1080px the row holds six or seven, and a phone still gets two. */
  /* One shelf everywhere: the ledge runs the width of the wall and the books
     stand at its left, the way books stand on a shelf. Centring the grid on
     /books/ while the front page left-aligned its three made the same
     component read as two different things on two pages. */
  max-width: var(--bookcase-max, none);
  margin-inline: var(--bookcase-mi, 0);
  /* a percentage min inside repeat(auto-fill, ...) makes the repetition
     count indeterminate and collapses the grid to one column */
  /* 1fr, and it has to be. A column stretches to 173px at 600 and 1024 while
     the cover stays 140, so a book sits in more air on a tablet than on a
     phone - but minmax(150px, 160px) is not the fix. auto-fill counts tracks
     against the MAX when the max is definite, so a 160px cap made 2*160+6
     exceed a 375px phone's 320px of shelf and dropped it to one column, and
     cost the desktop its sixth. The air is cosmetic; the column count is not. */
  /* auto-FIT, not auto-fill. They are identical once the shelf is full, which
     is why /books/ does not move - but the front page carries only the titles
     that are actually on sale, and auto-fill kept the empty tracks, so three
     covers huddled at the left of a full-width wall with two thirds of it bare.
     auto-fit collapses the empty tracks and the covers spread across the whole
     shelf. */
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  grid-auto-rows: var(--bookcase-row);
  background: repeating-linear-gradient(to bottom,
    transparent 0,
    transparent calc(var(--bookcase-row) - var(--bookcase-label) - 6px),
    var(--ledge)  calc(var(--bookcase-row) - var(--bookcase-label) - 6px),
    var(--ledge)  calc(var(--bookcase-row) - var(--bookcase-label)),
    transparent   calc(var(--bookcase-row) - var(--bookcase-label)),
    transparent   var(--bookcase-row));
}

/* A 320px phone has about 265px of shelf, and two 150px tracks need 306, so
   the smallest screens fell to one book per row with the ledge running empty
   beside it. Below 22.5rem the tracks and the row shrink together - the row
   has to come down too, or the cover would be wider than its column, and
   max-width would shrink it off the ledge. */
@media (max-width: 22.5rem) {
  .bookcase {
    grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
    /* +16 with the label, so the cover keeps the 128px it had here. */
    --bookcase-row: 263px;
  }
}

.bookcase__item {
  display: flex; flex-direction: column; text-decoration: none;
  height: var(--bookcase-row); padding: 0 2px;
}
.bookcase__book {
  flex: 1; display: flex; align-items: flex-end; justify-content: center;
  padding-bottom: 6px;   /* no overflow clip: a leaning cover needs its corners */
}
.bookcase__book picture { display: block; height: 100%; }
/* An explicit height, not a percentage chain. Through <picture> the percentage
   did not resolve, so covers overflowed their row and sat on the labels.
   It has to be the SAME arithmetic the ledge uses, or the cover stops standing
   on it. This was a third independent number - row minus 70, against a 58px
   label and a ledge at row minus 64 - and enlarging the label proved why that
   was fragile: the cover kept its old height, squeezed the label from 74 back
   to 64, and overhung the ledge by 13px. One variable now decides all three. */
.bookcase__book img {
  display: block; width: auto; max-width: 100%;
  height: calc(var(--bookcase-row) - var(--bookcase-label) - 6px);
}
.bookcase__book img {
  transform: rotate(var(--lean, 0deg)); transform-origin: bottom center;
  transition: transform 200ms cubic-bezier(.2,.7,.3,1);
  box-shadow: 0 5px 10px -5px rgba(0,0,0,.65);
  border: 1px solid rgba(31,27,23,.3);
}
.bookcase__item:nth-child(4n+1) { --lean: -2.4deg; }
.bookcase__item:nth-child(4n+2) { --lean:  1.6deg; }
.bookcase__item:nth-child(4n+3) { --lean: -0.8deg; }
.bookcase__item:nth-child(4n)   { --lean:  2.2deg; }
.bookcase__item:hover img, .bookcase__item:focus-visible img {
  transform: rotate(0deg) translateY(-8px);
}

.bookcase__label {
  height: var(--bookcase-label); padding-top: 9px; display: block;
  /* 13.5, not 11.5. On a shelf of fifteen the covers are deliberately one
     family - same cast, same palette, same lettering - so the title is what
     tells them apart, and it was set smaller than the status line's own
     caption size deserves. Measured at both ends before changing it: at
     13.5px in a 74px box nothing overflows, at the 150px desktop column or
     the ~157px a 375px phone actually gives. */
  font-size: 13.5px; line-height: 1.2; font-weight: 700;
  /* Ink, because the shelf wall is washed now. Cream was right when the wall
     was flooded and measures 2.87:1 on the washed teal - every book title on
     every shelf was below AA until this line changed. The grown-ups shelf is
     still a dark .panel--night, so it keeps the cream. */
  color: var(--ink);
}
.panel--night .bookcase__label { color: var(--night-tx); }
/* The status is metadata under a title now, not a second line of the same
   size, so it steps down rather than relying on opacity alone. */
.bookcase__status { display: block; font-weight: 400; font-size: 12px;
                    opacity: .85; margin-top: 3px; }
.bookcase__item--soon .bookcase__book img { opacity: .82; }

@media (prefers-reduced-motion: reduce) {
  .bookcase__book img { transition: none; }
}
