/* LETTER FRIENDS
 * ============================================================================
 * Its own design language, and deliberately not the arcade's.
 *
 * The other three pages of this site share one palette: a near-black indigo
 * stage, cyan as the single interactive accent, amber for currency, magenta for
 * threats. That language is built for a dark room and a fast game, and every
 * single thing about it is wrong here. This page is for a four year old sitting
 * next to a grown-up, probably in daylight, and it wants to look like a picture
 * book: warm paper, soft pastels, big rounded shapes, nothing sharp and nothing
 * dark. So this stylesheet starts from its own :root and shares nothing with
 * css/app.css but the discipline of keeping every colour in one block.
 *
 * THE FOUR RULES THIS FILE IS WRITTEN TO
 *
 *  1. TOUCH TARGETS ARE ENORMOUS. A small child's aim is poor and their patience
 *     for missing is nil. Nothing interactive here is under about 56px and the
 *     main doors are several times that.
 *  2. THE PICTURE IS THE BIGGEST THING ON SCREEN. It is the question. Everything
 *     else is smaller than it, always.
 *  3. NOTHING FLASHES FASTER THAN THREE TIMES A SECOND, anywhere, ever. That is
 *     the photosensitive seizure threshold, and every animation below is either
 *     a single transition or a slow pulse well under it.
 *  4. COLOUR IS NEVER THE ONLY SIGNAL. A hinted key also lifts and scales, a
 *     wrong key also wobbles, a collected slot also gains a picture. Roughly one
 *     boy in twelve cannot rely on the red/green channel and this game is for
 *     children too young to compensate.
 *
 * Sections
 *   1  tokens          8  slots and pips
 *   2  base            9  celebration layer
 *   3  screens        10  the book
 *   4  home           11  the grown-up sheet
 *   5  the stage      12  responsive
 *   6  the map        13  reduced motion
 *   7  map hints
 * ========================================================================== */

/* ---------------------------------------------------------------- 1 tokens */
:root{
  /* Paper and ink. The background is warm rather than white: a full-white field
     at this size is glare, and glare is the first thing a tired child notices. */
  --paper:      #FFF6EE;
  --paper-2:    #FFEFE2;
  --card:       #FFFFFF;
  --ink:        #4A3B4E;
  --ink-soft:   #7C6B80;
  --ink-faint:  #A99BAC;

  /* The sticker palette, so the interface and the art are visibly one thing. */
  --pink:       #FF9FC4;
  --pink-deep:  #E4739F;
  --lilac:      #B8B0FF;
  --lilac-deep: #8C82E8;
  --mint:       #A6E3C8;
  --mint-deep:  #5FBF98;
  --sun:        #FFD98A;
  --sun-deep:   #E9AE3C;
  --sky:        #8FD4FF;
  --peach:      #FFC7A8;

  /* The three rows of the map are three places, and each place has a colour.
     This is the spatial mnemonic made visible: "up on the ridge" is a thing you
     can see as well as a thing you are told. */
  --row-0:      #CFE3FF;   /* Rainbow Ridge, sky */
  --row-1:      #CDEEDA;   /* Sunny Meadow, grass */
  --row-2:      #F7DCC8;   /* Cosy Burrow, earth */
  --row-0-ink:  #4E6E9E;
  --row-1-ink:  #3F7A5C;
  --row-2-ink:  #9A6647;

  /* Right and wrong. Never the only signal, and the wrong one is deliberately
     soft: a child is going to see it a great deal and it must not read as anger. */
  --yes:        #5FBF98;
  --nope:       #C9A6C4;

  /* THE ALBUM. Its own small family, because a printed page is not the same
     warm white as the interface behind it, a board cover is not a card, and gold
     foil is not any of the pastels above. Every one of these exists to make the
     book read as a manufactured object rather than as a panel. */
  --page:       #FFFCF4;   /* the cream of a page, away from the gutter */
  --page-gut:   #F6E7CE;   /* the same page, shaded into the spine */
  --page-trim:  #E7D3B4;   /* the printed trim line and the scallop */
  --board:      #FBBBD1;   /* the cover boards */
  --board-deep: #E4789F;
  --board-dark: #C95C86;   /* the spine, which is always in its own shadow */
  --ribbon:     #FF93AE;
  --foil:       #E7B23F;
  --foil-lite:  #FFEFC0;
  --foil-deep:  #A97417;

  --r-sm: 12px;
  --r-md: 20px;
  --r-lg: 30px;
  --r-xl: 44px;

  --shadow-1: 0 2px 0 rgba(74,59,78,.10), 0 6px 16px rgba(74,59,78,.10);
  --shadow-2: 0 4px 0 rgba(74,59,78,.12), 0 12px 30px rgba(74,59,78,.14);
  --shadow-in: inset 0 3px 8px rgba(74,59,78,.10);

  --display: 'Baloo 2', 'Nunito', 'Trebuchet MS', system-ui, sans-serif;
  --ui: 'Nunito', system-ui, -apple-system, 'Segoe UI', sans-serif;
}

/* ------------------------------------------------------------------ 2 base */
*, *::before, *::after{ box-sizing: border-box; }

html, body{ height: 100%; }

body{
  margin: 0;
  font-family: var(--ui);
  color: var(--ink);
  background:
    radial-gradient(1200px 700px at 50% -10%, var(--paper-2), transparent 70%),
    var(--paper);
  -webkit-text-size-adjust: 100%;
  /* The game is a keyboard game. Selecting text with a stray drag is never
     wanted and on a touch screen it produces a magnifier over the picture. */
  -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent;
  overflow-x: hidden;
}

h1, h2, h3{ font-family: var(--display); font-weight: 800; margin: 0; letter-spacing: .01em; }

button{ font: inherit; color: inherit; cursor: pointer; }

img{ max-width: 100%; display: block; }

/* `display:block` above outranks the user agent's [hidden]{display:none}, so a
   hidden <img> kept its box and drew an empty frame exactly where the picture
   would have been. Stated once, for everything. */
[hidden]{ display: none !important; }

/* Focus is visible and generous. A parent driving this by keyboard, or a child
   with a switch device, must never lose where they are. */
:focus-visible{
  outline: 4px solid var(--lilac-deep);
  outline-offset: 3px;
  border-radius: 8px;
}

.btn{
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.05rem;
  border: 0;
  border-radius: var(--r-lg);
  padding: .7em 1.3em;
  min-height: 52px;
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-1);
  transition: transform .12s ease, box-shadow .12s ease;
}
.btn:hover{ transform: translateY(-2px); }
.btn:active{ transform: translateY(1px); box-shadow: var(--shadow-in); }
.btn.primary{ background: var(--mint); color: #22513F; }
.btn.ghost{ background: transparent; box-shadow: none; color: var(--ink-soft); }
.btn.ghost:hover{ background: rgba(255,255,255,.7); }
.btn.danger{ background: #FFE0E6; color: #8E3A55; }
.btn.tiny{ min-height: 36px; padding: .3em .8em; font-size: .85rem; }
.btn.big{ font-size: 1.35rem; padding: .8em 2em; min-height: 68px; }

.lede{ color: var(--ink-soft); margin: .4em 0 0; }
.hint{ color: var(--ink-soft); font-size: .92rem; line-height: 1.5; }

.noscript{ padding: 3rem 1.5rem; text-align: center; }

.tiny-foot{
  position: fixed; right: 10px; bottom: 6px;
  font-size: .68rem; color: var(--ink-faint);
  pointer-events: none; z-index: 1;
}
.tiny-foot code{ font-family: ui-monospace, monospace; }

/* --------------------------------------------------------------- 3 screens */
.app{ min-height: 100%; }
.screen{ min-height: 100vh; min-height: 100dvh; }
.screen[hidden]{ display: none; }

/* ------------------------------------------------------------------ 4 home */
.screen-home{ display: grid; place-items: center; padding: 4vh 1rem; }

.home{ width: min(680px, 100%); text-align: center; display: grid; gap: 1.1rem; }
.home-hi{ font-size: clamp(2.2rem, 7vw, 3.4rem); color: var(--lilac-deep); }
.home-stats{ margin: 0; color: var(--ink-soft); font-weight: 600; }

/* THE DOORS. Three, and each is a different size, colour and shape so that a
   child who cannot read a single word on this page can still tell them apart
   and remember which is which. */
.door{
  border: 0;
  border-radius: var(--r-xl);
  display: grid;
  justify-items: center;
  gap: .3rem;
  padding: 1.1rem;
  box-shadow: var(--shadow-2);
  transition: transform .15s cubic-bezier(.2,.9,.3,1.3), box-shadow .15s ease;
}
.door:hover{ transform: translateY(-4px) scale(1.02); }
.door:active{ transform: translateY(2px) scale(.99); }
.door-label{ font-family: var(--display); font-weight: 800; }

.door-play{ background: linear-gradient(180deg, #FFE9F1, var(--pink)); padding-bottom: 1.4rem; }
.door-play .door-label{ font-size: clamp(2rem, 6vw, 2.8rem); color: #7E2E4F; }
.door-play .door-pic img{
  width: clamp(140px, 34vw, 230px); height: auto;
  /* A painted scene rather than a die-cut sticker, so it is framed on purpose:
     an unrounded rectangle inside a rounded card reads as a mistake. */
  border-radius: var(--r-lg);
  box-shadow: 0 6px 18px rgba(74,59,78,.18);
}

.door-book{
  background: linear-gradient(180deg, #F1EEFF, var(--lilac));
  grid-template-columns: auto 1fr;
  justify-items: start; align-items: center; gap: .9rem;
  padding: .9rem 1.4rem;
}
.door-book .door-label{ font-size: clamp(1.3rem, 4vw, 1.8rem); color: #3E357F; }
/* A painted scene, framed like the play door's, rather than a die-cut sticker.
   It is wide (an open album is a landscape object) so it gets more width than
   the square sticker it replaced. */
.door-book .door-pic img{
  width: clamp(104px, 24vw, 150px); height: auto;
  border-radius: var(--r-md);
  box-shadow: 0 4px 12px rgba(74,59,78,.18);
}

.door-grown{
  background: transparent;
  box-shadow: none;
  color: var(--ink-faint);
  font-size: .95rem;
  padding: .5rem;
  display: inline-flex; gap: .5rem; align-items: baseline;
  justify-self: center;
}
.door-grown .door-hint{ font-size: .75rem; opacity: .7; }
.door-grown.is-holding{
  color: var(--lilac-deep);
  /* The hold is visible as a fill, so the adult knows it is working rather than
     wondering whether the button is broken. */
  background: linear-gradient(90deg, var(--lilac) 0%, transparent 0%);
  animation: hold .9s linear forwards;
  border-radius: var(--r-md);
}
@keyframes hold{
  from{ background: linear-gradient(90deg, var(--lilac) 0%, transparent 0%); }
  to  { background: linear-gradient(90deg, var(--lilac) 100%, transparent 100%); }
}

.home-foot{ color: var(--ink-faint); font-size: .82rem; margin-top: .5rem; }
.home-foot a{ color: var(--ink-soft); }

/* ------------------------------------------------------------- 5 the stage */
.screen-play{ position: relative; }

/* The whole turn has to fit on screen at once. A child mid-turn who has to
   scroll to see the map has lost the thread, so the stage is exactly the height
   of the viewport, the map is sized so it always fits, and the picture takes
   whatever is left. minmax(0,1fr) rather than 1fr because a grid row defaults to
   min-content and would otherwise refuse to shrink below the picture. */
.stage{
  height: 100vh; height: 100dvh;
  display: grid;
  /* The explicit single column matters. Without it the grid has one AUTO column,
     which sizes to the widest item and then sits at the start of the line, so the
     map (the widest thing) pinned the whole stage to the left edge. */
  grid-template-columns: minmax(0, 1fr);
  /* card, the help button, the map. The middle row is content sized, so the
     button never takes space the picture or the map needed. */
  grid-template-rows: minmax(0, 1fr) auto auto;
  gap: clamp(.4rem, 1.5vh, 1.2rem);
  padding: clamp(.5rem, 1.6vh, 1.2rem);
  overflow: hidden;
}

.stage-card{
  position: relative;
  min-height: 0;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: .5rem;
  text-align: center;
}

/* THE QUESTION ROW: the letter, then who lives with it. Read left to right, so
   the card says "Z is for zucchini" as a picture before the voice says it as a
   sentence. The two never stack: the pairing IS the sentence, and a letter above
   a picture is two facts rather than one. */
.qrow{
  display: flex;
  align-items: center; justify-content: center;
  gap: clamp(.4rem, 2.4vw, 1.6rem);
  width: 100%;
  min-height: 0;
}
.qrow[hidden]{ display: none; }

/* THE LETTER'S PLATE. RULE 2 says the question is the biggest thing on screen,
   and on a letter turn the question is the LETTER, so this is a shade larger
   than the picture beside it. Its size never changes with what is in it: when
   the letter is withdrawn what is left is an empty doorplate exactly where the
   letter was, which is the question rather than a hole. */
.plate-wrap{
  display: grid; justify-items: center; gap: .5rem;
  min-width: 0;
}
.plate-wrap[hidden]{ display: none; }

.letter-plate{
  position: relative;
  width: min(34vh, 36vw);
  aspect-ratio: 1;
  /* THE PLATE IS THE RULER FOR WHAT IS ON IT. The letter used to be sized off
     the VIEWPORT while the plate was sized off the viewport separately, so the
     two only agreed by arithmetic nobody was checking, and they disagreed
     outright on the bare rung, where the plate grows into the missing picture's
     room and the letter stayed exactly as it was. In `cqw` the glyph is a
     fraction OF THE PLATE, whatever the plate turns out to be. */
  container-type: size;
  display: grid; place-items: center;
  padding: 0; margin: 0;
  border: 0;
  border-radius: var(--r-xl);
  background: var(--card);
  box-shadow: var(--shadow-2);
  color: var(--lilac-deep);
  overflow: hidden;
  cursor: pointer;
  transition: box-shadow .3s ease, background .3s ease;
}

/* Empty: a dashed doorplate with a question mark on it. Dashed rather than
   blank, and colour is not the only signal, because "nothing here yet" has to
   read as a thing being asked and never as a picture that failed to load. */
.letter-plate[data-show="off"]{
  background: transparent;
  box-shadow: inset 0 0 0 4px rgba(140,130,232,.28), var(--shadow-1);
  outline: 3px dashed rgba(140,130,232,.35);
  outline-offset: -14px;
}
.letter-plate:active{ transform: scale(.98); }

/* THE PLATE ALONE. The bare rung has no picture at all (see `single` in
   js/letters/play.js), and on that turn the letter is not one of two things on
   the card, it is the whole card. So it grows into the room the missing picture
   left rather than sitting at the size it was cut to fit beside one.
   Written with :has() rather than a class set from script because the picture is
   hidden from four different places in play.js and a mirrored attribute is a
   fifth thing to keep in step. It fails in the safe direction: with no :has()
   the plate stays exactly the size it has always been. */
.qrow:has(.pic-wrap[hidden]) .letter-plate{ width: min(42vh, 52vw); }

/* ------------------------------------------------------------- the answer --
 *
 * WHAT ARRIVES ON THE PLATE THE MOMENT THE CHILD IS RIGHT. It fills the plate
 * edge to edge, because it is a picture OF the letter and the letter was already
 * the biggest thing on the card. */
.lp-duet{
  position: absolute; inset: 4%;
  width: 92%; height: 92%;
  object-fit: contain;
  pointer-events: none;
}
.lp-duet[hidden]{ display: none; }
/* The plate stops being a question: the dashes go, the card fills in, and it
   takes the warm ring the map's own right-answer key wears, so the two halves of
   the screen agree about what just happened. */
.letter-plate[data-show="won"]{
  background: var(--card);
  outline: 0;
  box-shadow: 0 0 0 4px var(--yes), 0 10px 26px rgba(95,191,152,.34);
}
.letter-plate[data-show="won"] .lp-glyph,
.letter-plate[data-show="won"] .lp-ask{ opacity: 0; }
/* One bounce, once, on arrival. Nowhere near 3 Hz because it does not repeat at
   all: it happens and it is over, which is the honest shape for a thing that is
   reporting an event rather than waiting for one. */
.letter-plate.is-won{ animation: wonpop .5s cubic-bezier(.2,.9,.3,1.5); }
@keyframes wonpop{
  0%  { transform: scale(.9); }
  55% { transform: scale(1.06); }
  100%{ transform: scale(1); }
}
.letter-plate.is-won .lp-duet{ animation: wonin .42s cubic-bezier(.2,.9,.3,1.4) backwards; }
@keyframes wonin{
  from{ transform: scale(.6) rotate(-7deg); opacity: 0; }
  to  { transform: none; opacity: 1; }
}

/* THE LETTER IS THE QUESTION, SO IT FILLS THE PLATE IT IS ASKED ON.
 *
 * It was set at min(15vh, 20vw) against a plate of min(34vh, 36vw), which is
 * about two fifths of the plate's width in font-size and, once the cap height of
 * the face is taken off that, about three tenths of the plate in actual ink. The
 * rest was white card. That is a bad trade on the one element rule 2 exists for,
 * and it is worse than it sounds for the audience: a four year old is still
 * learning to tell a b from a d, and the difference between those two shapes is
 * a few millimetres of stroke at that size.
 *
 * WHAT SETS THE CEILING IS THE PAIR, not the letter. The plate prints "Aa" by
 * default, and two glyphs of a wide rounded display face run about 1.26em; the
 * widest pairs in the alphabet (Mm, Ww) run nearer 1.8em, which is why they get
 * their own step rather than dragging every other letter down to their size. One
 * glyph on its own (the upper or lower setting) has no pair to fit, so it takes
 * the whole plate. Sized to leave a margin at every step, and the plate clips
 * anyway, so the failure direction is a slightly small letter and never a broken
 * card. The fallback faces in `--display` are all narrower than Baloo 2, so a
 * blocked webfont errs the same safe way.
 */
.lp-glyph{
  font-family: var(--display);
  font-weight: 800;
  font-size: 68cqw;
  line-height: 1;
  white-space: nowrap;
  letter-spacing: -.02em;
  transition: opacity .42s ease, transform .42s cubic-bezier(.4,0,.6,1);
}
/* One glyph, because the grown-up has asked for capitals only or lower case
   only. Nothing to fit beside it. */
.letter-plate[data-glyphs="1"] .lp-glyph{ font-size: 78cqw; }
/* The two widest pairs there are. Mm and Ww are half again as wide as an
   ordinary pair, so a size that fits every other letter comfortably would send
   these over the edge of the card. */
.letter-plate[data-glyphs="2"][data-wide="1"] .lp-glyph{ font-size: 50cqw; }
/* The letter LEAVING is the whole of the middle rung, so it is a departure the
   child can watch rather than a frame in which it is suddenly not there. */
.letter-plate[data-show="off"] .lp-glyph{
  opacity: 0;
  transform: scale(.55) translateY(-8%);
}

/* The question mark under the letter's place. It is behind the glyph at all
   times and simply invisible while the glyph is there, which is why the
   breathing animates TRANSFORM ONLY: an animation that also touched opacity
   would outrank the rule holding it at zero and the mark would show through the
   letter, which it did. Opacity belongs to the state, motion belongs to the
   animation, and the two never fight. */
.lp-ask{
  position: absolute;
  font-family: var(--display); font-weight: 800;
  font-size: clamp(2rem, 9vh, 4.4rem);
  line-height: 1;
  color: var(--ink-faint);
  opacity: 0;
  transition: opacity .42s ease .12s;
  /* A slow breath, nowhere near the 3 Hz line, so the plate reads as waiting for
     an answer rather than as an empty box. */
  animation: ask-breathe 3.4s ease-in-out infinite;
}
.letter-plate[data-show="off"] .lp-ask{ opacity: .8; }
@keyframes ask-breathe{
  0%,100%{ transform: scale(1); }
  50%    { transform: scale(1.09); }
}

/* RULE 2, the other half: the picture is the biggest thing on screen after the
   question itself. Sized off the space that is actually left rather than off the
   viewport, so it shrinks instead of pushing the map off the bottom. */
/* Sized from WIDTH with an aspect ratio, not from height: a percentage height
   inside an auto-height parent has nothing to resolve against and collapses. */
.pic-wrap{
  display: grid; place-items: center;
  width: min(30vh, 32vw);
  aspect-ratio: 1;
  cursor: pointer;
  /* It FADES rather than collapsing between the beat that shows the letter and
     the beat that introduces the friend. A card that changes shape under a four
     year old mid-turn is a card they have to read again from scratch. */
  opacity: 1;
  transition: opacity .34s ease;
}
.pic-wrap[data-in="0"]{ opacity: 0; }
.pic-wrap[hidden]{ display: none; }
.pic{
  width: 100%; height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 10px 18px rgba(74,59,78,.16));
  animation: breathe 5.2s ease-in-out infinite;
}
/* A slow float, well under 3 Hz. It makes the sticker feel alive without ever
   competing with the thing the child is meant to be doing. */
@keyframes breathe{
  0%,100%{ transform: translateY(0) rotate(-.6deg); }
  50%    { transform: translateY(-9px) rotate(.6deg); }
}

/* Block, NOT grid. As a grid every inline child became its own row, so
   "M is for Mama" came out as three stacked lines with a full stop on a line of
   its own. The only thing that has to break is the story, and it says so itself. */
.prompt{
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(1.3rem, 4.4vw, 2.2rem);
  line-height: 1.25;
  margin: 0;
  color: var(--ink);
  max-width: 22ch;
  /* Held open through the first beat, when there is nothing to say yet. The line
     filling in must not move the picture and the plate up the screen. */
  min-height: 1.25em;
}
.prompt b{ color: var(--lilac-deep); }
.story{
  display: block;
  font-family: var(--ui);
  font-weight: 700;
  font-size: clamp(.95rem, 2.6vw, 1.15rem);
  color: var(--ink-soft);
}

/* THE WAY OUT. Mirrors "say it again" into the opposite corner, and is
   deliberately the quieter of the two: the child's control is lilac and full
   size, this one is soft ink and a little smaller, because a sitting should end
   when somebody means it to and not because a hand landed here on the way to the
   picture. A house rather than an arrow, since the audience cannot read a label
   and a house is the one symbol for "the start" that needs no teaching. */
.gohome{
  position: absolute; top: 0; left: 0;
  width: 52px; height: 52px;
  border: 0; border-radius: 50%;
  background: var(--card);
  color: var(--ink-soft);
  box-shadow: var(--shadow-1);
  display: grid; place-items: center;
  opacity: .85;
}
.gohome svg{ width: 24px; height: 24px; fill: currentColor; }
.gohome:hover{ opacity: 1; }
.gohome:active{ transform: scale(.94); }

/* The child's own two controls, in the corner they have always been in. Stacked
   rather than placed by hand so neither has to know how big the other is. */
.card-tools{
  position: absolute; top: 0; right: 0;
  display: grid; gap: .5rem; justify-items: end;
}

/* Say it again. Always in the same place, always the same shape: it is the one
   control a pre-reader will learn to rely on. */
.again{
  width: 60px; height: 60px;
  border: 0; border-radius: 50%;
  background: var(--card);
  color: var(--lilac-deep);
  box-shadow: var(--shadow-1);
  display: grid; place-items: center;
}
.again svg{ width: 30px; height: 30px; fill: currentColor; }
.again:active{ transform: scale(.94); }

/* ASK FOR HELP. Not in a corner: a control a four year old has to hunt for is a
   control they will not use, and this is the one they are meant to reach for.
   It sits in its own band between the card and the map, on the line their eyes
   already travel down on every turn, and it is a PICTURE of somebody rather than
   a symbol, because the audience cannot read the word underneath it. The label
   is there for the grown-up. */
.helpbar{
  display: flex; justify-content: center; align-items: center;
  gap: clamp(.5rem, 2vw, 1.2rem);
  flex-wrap: wrap;
}
.helpme{
  display: flex; align-items: center; gap: .55rem;
  border: 0;
  border-radius: 999px;
  padding: .3rem 1.1rem .3rem .3rem;
  background: var(--card);
  box-shadow: var(--shadow-1);
  color: var(--lilac-deep);
  font-family: var(--display); font-weight: 800;
  font-size: clamp(.9rem, 2.2vw, 1.1rem);
  transition: transform .14s cubic-bezier(.2,.9,.3,1.3), box-shadow .14s ease;
}
.helpme-pic{
  width: clamp(38px, 7vh, 54px); height: clamp(38px, 7vh, 54px);
  border-radius: 50%;
  object-fit: cover;
  background: var(--lilac);
}
.helpme:hover{ transform: translateY(-2px); box-shadow: var(--shadow-2); }
.helpme:active{ transform: scale(.96); }

/* THE SPACE BAR PRESSES THIS BUTTON, said on the button. Under the label rather
   than beside it, small, and in the soft ink: "Help me" is what the control is
   and this is a footnote about another way to reach it, aimed at the grown-up,
   since neither line is readable by the player. The keycap is drawn wide and
   flat because that is the shape of the key it is naming, and a child who cannot
   read the word can still match the picture to the biggest key in front of
   them. */
.helpme-say{ display: grid; justify-items: start; gap: .05rem; line-height: 1.15; }
.helpme-key{
  font-size: .62em; font-weight: 700;
  color: var(--ink-soft);
  display: flex; align-items: center; gap: .3em;
}
.helpme-key b{
  font-weight: 800;
  padding: .1em .7em .15em;
  border-radius: .45em;
  background: var(--paper-2);
  box-shadow: 0 1px 0 var(--ink-faint);
  color: var(--ink);
}
/* A TABLET HAS NO SPACE BAR, so it is not told about one. The remark is the only
   part of this button that can be untrue, and on the device this game is most
   often played on it would be. The button itself is untouched: the shortcut is
   an extra door, never the door. */
@media (hover: none) and (pointer: coarse){
  .helpme-key{ display: none; }
}

/* THE DOOR TO A NEW FRIEND, present only when it will open. It arrives rather
   than lights up, because its arrival is the reward: a child who has just
   answered every letter they own twice running has earned a moment, and an
   element that fades in where there was nothing is a moment. */
.newfriend{
  display: flex; align-items: center; gap: .5rem;
  border: 0; border-radius: 999px;
  padding: .45rem 1.1rem;
  background: var(--sun);
  color: #7A5312;
  box-shadow: var(--shadow-1);
  font-family: var(--display); font-weight: 800;
  font-size: clamp(.9rem, 2.2vw, 1.1rem);
  transition: transform .14s cubic-bezier(.2,.9,.3,1.3), box-shadow .14s ease;
}
.newfriend[hidden]{ display: none; }
.newfriend-plot{ display: grid; place-items: center; width: 26px; height: 26px; }
.newfriend-plot svg{ width: 26px; height: 26px; }
.newfriend:hover{ transform: translateY(-2px); box-shadow: var(--shadow-2); }
.newfriend:active{ transform: scale(.96); }
.newfriend.is-new{ animation: doorin .55s cubic-bezier(.2,.9,.3,1.4); }
/* THE FIRST ONE EVER. Bigger than an ordinary arrival and it keeps moving for a
   few seconds afterwards, because the child has just been told what it is and
   now has to find it: a button that announces itself and then sits still is a
   button somebody has to be pointed at twice. It stops on its own. */
.newfriend.is-first{
  animation: doorin-first .7s cubic-bezier(.2,.9,.3,1.5),
             door-beat 1.15s .7s ease-in-out 2;
  box-shadow: 0 0 0 4px rgba(255,199,92,.5), var(--shadow-2);
}
/* AND IT KEEPS BEATING WHILE THE GAME WAITS FOR IT. The first door parks the
   screen: no question, no picture, one button. A pulse that stops after two
   beats would leave a child looking at a still screen with nothing on it that
   says "press this", which is the state the whole moment exists to avoid. */
.newfriend.is-waiting{
  animation: doorin-first .7s cubic-bezier(.2,.9,.3,1.5),
             door-beat 1.5s .7s ease-in-out infinite;
}
@keyframes doorin-first{
  0%{ transform: scale(.4) rotate(-10deg); opacity: 0; }
  60%{ transform: scale(1.14) rotate(2deg); opacity: 1; }
  100%{ transform: scale(1) rotate(0); opacity: 1; }
}
@keyframes door-beat{
  0%, 100%{ transform: scale(1); box-shadow: 0 0 0 4px rgba(255,199,92,.5), var(--shadow-2); }
  50%{ transform: scale(1.07); box-shadow: 0 0 0 10px rgba(255,199,92,0), var(--shadow-2); }
}
@keyframes doorin{
  0%{ transform: scale(.7); opacity: 0; }
  60%{ transform: scale(1.08); opacity: 1; }
  100%{ transform: scale(1); opacity: 1; }
}

/* THE BUFFET: six pictures, one per letter that could come next. Wider and a
   little smaller than the three-card anchor choice, because six of those would
   not fit a phone and a picture too small to recognise is not an option. */
/* THREE ACROSS, TWO DOWN, and it is a grid rather than a wrapping row because a
   wrap put five on one line and one lonely picture underneath: six equal things
   have to look like six equal things, or the odd one out reads as the answer. */
.choose[data-wide]{
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  justify-items: center;
  max-width: min(760px, 94vw);
  margin-inline: auto;
}
.choose[data-wide] .pick img{ width: clamp(66px, 13vw, 104px); }
.choose[data-wide] .pick span{ font-size: .95rem; }
@media (max-width: 560px){
  .choose[data-wide]{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* THE ANCHOR CHOICE. The one moment the child is asked what they want, so the
   options are large, equal and unhurried. */
.choose{
  display: flex; flex-wrap: wrap; gap: clamp(.6rem, 2vw, 1.4rem);
  justify-content: center;
}
.pick{
  border: 0; background: var(--card);
  border-radius: var(--r-lg);
  padding: .8rem;
  box-shadow: var(--shadow-1);
  display: grid; justify-items: center; gap: .3rem;
  transition: transform .15s cubic-bezier(.2,.9,.3,1.3);
}
.pick img{ width: clamp(96px, 22vw, 150px); height: auto; }
.pick span{ font-family: var(--display); font-weight: 700; font-size: 1.05rem; }
.pick:hover{ transform: translateY(-5px) scale(1.04); }
/* WHICH ONE IS CHOSEN RIGHT NOW, drawn for a child steering with the arrow keys.
   The browser's own focus ring is a hairline meant for an adult reading a form,
   and this is the one control in the game that is driven without being touched,
   so it gets what a hinted key on the map gets: a fat ring AND a lift, because
   colour is never the only signal here. Written as :focus rather than
   :focus-visible on purpose. The first picture is focused from script as the
   screen arrives, and whether a browser calls that "visible" depends on how the
   child got here (a mouse click on the door would hide it), which would make the
   one promise this screen makes ("something is always chosen") come and go. */
.pick:focus{
  outline: none;
  transform: translateY(-5px) scale(1.04);
  box-shadow: var(--shadow-2), 0 0 0 .34rem var(--lilac-deep);
}
.pick:active{ transform: scale(.97); }

/* --------------------------------------------------------------- 6 the map */
/* NOTE: there is no .map-wrap at runtime. keyboard.mount() takes the host
   element and renames it to .map, so the map IS the wrapper and centres itself. */

/* Width is the smallest of three limits: a sensible maximum, the space across,
   and the width that keeps the map inside a fixed share of the viewport HEIGHT.
   That last one is what stops a short window pushing the bottom row off screen,
   and it is expressed as a width because aspect-ratio then derives the height. */
.map{
  position: relative;
  margin-inline: auto;
  width: min(1000px, 100%, calc(31vh * 10 / 3.4));
  aspect-ratio: 10 / 3.4;
  --keyw: 10%;
}

.map-bands{ position: absolute; inset: 0; }
.map-band{
  /* left and width are set per row from the row's own key count and stagger. */
  position: absolute;
  height: 33.34%;
  border-radius: var(--r-md);
  transition: filter .25s ease, transform .25s ease;
}
/* THE BAND IS A COLOUR AND NOTHING ELSE. It used to carry its place name as a
   watermark across the whole width; that is gone (owner's call, 2026-08) and
   nothing should put it back. The name is a handle for the grown-ups and for the
   voice, and printing it on the map spends the one surface the child is
   searching on a word they cannot read yet. */
.map-band[data-row="0"]{ background: var(--row-0); }
.map-band[data-row="1"]{ background: var(--row-1); }
.map-band[data-row="2"]{ background: var(--row-2); }

.map-path{ position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.map-arc{
  fill: none;
  stroke: var(--lilac-deep);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-dasharray: 4 3;
  vector-effect: non-scaling-stroke;
  animation: crawl 1.1s linear infinite;
}
@keyframes crawl{ to{ stroke-dashoffset: -14; } }
.map-arc-end{ fill: var(--lilac-deep); opacity: .85; }

/* THE ROUTE THROUGH A WORD: the homes already visited, joined in order. A solid
   line rather than the hint's crawling dashes, because it is not asking for
   anything: it is the record of where the child has been. Quiet enough that it
   never competes with the key being asked for next, and it is never drawn ahead
   of them (see trail() in js/letters/keyboard.js).

   THE NEWEST SEGMENT DRAWS ITSELF, and that is the arcade's backdrop rule
   applied here: a thing behind what the child is reading has to be small enough
   to read as texture or fast enough to read as motion, and a line that simply
   appears is neither. Drawn, it reads as the journey it is. At rest it is a thin
   faint stroke and not a patch. */
.map-walk{
  fill: none;
  stroke: var(--mint-deep);
  stroke-width: 2.2;
  stroke-linecap: round;
  opacity: .5;
  vector-effect: non-scaling-stroke;
}
.map-walk.is-new{
  stroke-dasharray: 1;
  animation: walkdraw .34s ease-out;
}
@keyframes walkdraw{ from{ stroke-dashoffset: 1; } to{ stroke-dashoffset: 0; } }
/* The word finished: the whole route lights at once. This is the map's half of
   the word celebration, and the reason it is on the map at all is that the map is
   where the journey happened. */
.map-walk.is-done{ stroke: var(--sun-deep); opacity: .95; stroke-width: 3; }
.mkey.is-walked .mkey-face{
  box-shadow: 0 0 0 3px var(--sun-deep), 0 6px 16px rgba(233,174,60,.35);
}

.map-keys{ position: absolute; inset: 0; }

.mkey{
  position: absolute;
  width: var(--keyw);
  height: 33.34%;
  padding: 2px;
  border: 0; background: none;
  transition: transform .18s cubic-bezier(.2,.9,.3,1.3), filter .25s ease;
}
.mkey-face{
  position: relative;
  display: grid; place-items: center;
  width: 100%; height: 100%;
  border-radius: 26%;
  background: var(--card);
  box-shadow: 0 2px 0 rgba(74,59,78,.14), 0 4px 10px rgba(74,59,78,.10);
  overflow: hidden;
  transition: box-shadow .18s ease, background .18s ease, opacity .18s ease;
}
/* An empty plot: visible, quiet, and clearly a place something could live. */
.mkey[data-stage="new"] .mkey-face{
  background: rgba(255,255,255,.42);
  box-shadow: inset 0 0 0 2px rgba(74,59,78,.07);
}
.mkey[data-stage="new"] .mkey-letter{ opacity: .38; }
.mkey[data-stage="strong"] .mkey-face{ box-shadow: 0 2px 0 rgba(95,191,152,.5), 0 4px 10px rgba(74,59,78,.10); }

.mkey-pic{
  position: absolute; inset: 6%;
  /* The three faces are a change of size and place, so they are worth animating:
     a picture that shrinks into the corner as a letter grows says what happened,
     and one that teleports says nothing. */
  transition: inset .22s ease, width .22s ease, height .22s ease, opacity .22s ease;
  background-size: contain; background-repeat: no-repeat; background-position: center;
  display: grid; place-items: center;
  font-family: var(--display); font-weight: 800;
  font-size: clamp(.9rem, 2.4vw, 1.5rem);
  color: var(--pink-deep);
}
.mkey[data-kind="family"] .mkey-pic{
  background-color: #FFE9F1;
  border-radius: 50%;
  inset: 14%;
  background-size: cover;
}
/* THE DUET: the letter and its friend as one picture, drawn for the middle face.
   It fills the key rather than sitting inside a margin, because the letter is
   the subject here and a letter drawn at 60% of a key is a letter nobody can
   read across a room. Hidden by default and shown only where the face asks for
   it (below), so both pictures can be loaded and a hint can swap between them by
   toggling a class. */
.mkey-duet{
  position: absolute; inset: 2%;
  display: none;
  background-size: contain; background-repeat: no-repeat; background-position: center;
}
.mkey-letter{
  position: absolute; left: 6%; bottom: 2%;
  font-family: var(--display); font-weight: 800;
  font-size: clamp(.55rem, 1.5vw, .95rem);
  color: var(--ink-soft);
  letter-spacing: .04em;
  text-shadow: 0 1px 0 #fff, 0 0 4px #fff;
  pointer-events: none;
  transition: transform .18s ease;
}
/* ONE CASE SITS WHERE TWO WOULD HAVE SAT. The label is anchored by its left
   edge, which is right for the pair (Aa) and wrong for either half of it on its
   own: a lone letter left against that edge reads as shoved into the corner of
   a space plainly sized for two. So it is nudged right by half of the glyph that
   is no longer there, which lands it on the pair's centre. Measured in em, so it
   follows the label down to the small size a phone prints it at, and the two
   values differ because a capital is wider than a lower case letter.

   Only on the first face, where the letter really is a label in a corner. On the
   other two it is centred type and centring is what puts it where it belongs. */
.map[data-case="upper"] .mkey[data-face="1"]:not(.is-showface) .mkey-letter{ transform: translateX(.28em); }
.map[data-case="lower"] .mkey[data-face="1"]:not(.is-showface) .mkey-letter{ transform: translateX(.35em); }

/* ------------------------------------------- the key's three levels, 1 2 3 --
 *
 * `data-face` carries the LEVEL, numbered from one, because that is the language
 * this game is talked about in (faceLevel in js/letters/letters.js).
 *
 *   1  the big picture of the friend, letter small in the corner
 *   2  the DUET: the letter and the friend as one picture, the letter leading
 *   3  the letter alone: a real keyboard
 *
 * Level 1 belongs to the NEWEST FRIEND and to nobody else: a picture the child
 * chose stays whole while it is still news, and settles into the duet the moment
 * somebody new moves in. So exactly one key on the map is ever at level 1.
 *
 * LEVEL 3 IS NOT REACHED AT THE MOMENT, and its rules are kept rather than
 * deleted. `LEVEL_TOP` in js/letters/letters.js switches it off (owner's call,
 * 2026-08), so nothing on the map paints `data-face="3"` today; raising that one
 * number is meant to be the whole of putting it back, which it can only be if
 * the styles it needs are still here.
 *
 * `.is-showface` puts a key back to level 1 for as long as a hint is pointing at
 * its row, which is the one moment the pictures are worth more than the letters
 * (see setHint in js/letters/keyboard.js). It is written as :not() on the two
 * advanced levels rather than as a louder rule of its own, so there is one place
 * each property is set and no specificity race between them. */

/* THE LETTER ALONE, AND THE ONE THING THIS RULE HAS TO DO IS FILL THE KEY.
 *
 * It used to say `inset: 0` and then `left: auto; bottom: auto` on the next
 * line, meaning to cancel the corner anchoring in the base rule above. `inset`
 * had already set all four sides, so what those two declarations actually did
 * was un-set two of them again: the label came out pinned to `top: 0; right: 0`,
 * shrink-wrapped around its own glyph, and `place-items: center` then centred
 * the letter inside a box exactly the size of the letter, which is no centring
 * at all. Every advanced key on the map drew its letter jammed into the top
 * right corner and the wide ones (Aa, Ww) overhung the key entirely. The base
 * rule's `left` and `bottom` are already overridden by `inset`, so the fix is to
 * let it do its job and say nothing else.
 */
.mkey[data-face="3"]:not(.is-showface) .mkey-letter,
.mkey[data-face="2"][data-duet="0"]:not(.is-showface) .mkey-letter{
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: clamp(1rem, 2.9vw, 1.85rem);
  color: var(--ink);
  letter-spacing: 0;
  text-shadow: none;
}
/* WITH A DUET, THE PICTURE IS THE LETTER. What is left for type to do is the
   half the painted capital cannot say, which is the lower case, and it says it
   the way the first face says it: small, in the corner, a label on a picture. */
.mkey[data-face="2"][data-duet="1"]:not(.is-showface) .mkey-duet{ display: block; }
.mkey[data-face="2"][data-duet="1"]:not(.is-showface) .mkey-pic{ display: none; }
.mkey[data-face="2"][data-duet="1"]:not(.is-showface) .mkey-letter{
  left: auto; right: 7%; bottom: 3%;
  font-size: clamp(.5rem, 1.35vw, .85rem);
  opacity: .75;
}
/* No duet drawn for this one (a family anchor, or an item the pack has not
   reached yet). The composite it replaced, which is still a correct way to say
   the same thing: the letter leads and the friend stays in the corner, no longer
   the answer but still who lives there. */
.mkey[data-face="2"][data-duet="0"]:not(.is-showface) .mkey-pic{
  inset: auto 5% 5% auto;
  width: 38%; height: 38%;
  opacity: .92;
}
/* Face 2: the picture is gone. Not faded, gone, because a ghost of it is still
   something to match against and the whole point of this face is that there is
   nothing left to match. */
.mkey[data-face="3"]:not(.is-showface) .mkey-pic{ display: none; }
/* The two keycaps with real nubs on them, drawn as a nub. The one landmark a
   child can find without looking, so the map says so. */
.mkey[data-bump="1"] .mkey-face::after{
  content: '';
  position: absolute; left: 50%; bottom: 7%;
  width: 22%; height: 3px; border-radius: 3px;
  transform: translateX(-50%);
  background: var(--ink-faint);
  opacity: .55;
}

/* ------------------------------------------------------------- 7 map hints */

/* WHAT IS NOT BEING POINTED AT GOES GREY, AND THE HOUSES GO WITH THE GROUND
   THEY STAND ON. A hint that recolours the band but leaves ten full-colour
   pictures sitting on top of it is not narrowing anything: the pictures are the
   loudest thing on the map, so as long as they all shout equally the child is
   still searching twenty six places. Colour is what separates the place being
   asked for from the rest, so it is taken away everywhere else. */
.map{
  --map-quiet: grayscale(.92) opacity(.42);
  /* THE SECOND WASH, FOR THE ONE KEY THAT IS NEITHER (see THE HOME, below). A
     step back from the search and a long way in front of the rest of the map:
     still a picture the child can name, never a thing that competes with the
     row the hint is pointing at. */
  --map-aside: grayscale(.45) opacity(.72);
}

/* THE POINTERS. Hint 1 used to be entirely subtractive: the other two rows go
   quiet and the right one lifts. That says where not to look, which is the
   quieter half of a hint, and a child who has gone quiet enough to have earned a
   hint needs something that says HERE. So a pair of drawn arrows stands either
   side of the row and nudges toward it.

   They sit outside the band, in a gutter the map wrap reserves for them, so they
   never cover a key: the map is the surface being searched and putting a pointer
   on top of the houses would be the same mistake as printing the place names
   across it. */
.map-wrap{ position: relative; padding-inline: clamp(22px, 6.5vw, 78px); }
.map-point{
  position: absolute; top: 50%; z-index: 4; pointer-events: none;
  width: clamp(20px, 5.6vw, 68px); height: auto;
  opacity: 0; transition: opacity .22s ease;
  filter: drop-shadow(0 3px 5px rgba(74,59,78,.22));
}
.map-point.is-l{ right: 100%; margin-right: .35rem; transform: translateY(-50%); }
.map-point.is-r{ left: 100%; margin-left: .35rem; transform: translateY(-50%) scaleX(-1); }
.map-band.is-hinted .map-point{ opacity: 1; animation: point-nudge 1.05s ease-in-out infinite; }
.map-band.is-hinted .map-point.is-r{ animation-name: point-nudge-r; }
/* Toward the row, both of them: the mirrored one moves left on screen because
   its own axis is flipped, which is the point of mirroring it. */
@keyframes point-nudge{
  0%, 100%{ transform: translateY(-50%) translateX(0); }
  50%{ transform: translateY(-50%) translateX(26%); }
}
@keyframes point-nudge-r{
  0%, 100%{ transform: translateY(-50%) scaleX(-1) translateX(0); }
  50%{ transform: translateY(-50%) scaleX(-1) translateX(26%); }
}

/* Hint 1: the row. The other two rows recede, keys and pictures with them, and
   the row being asked for lifts. Twenty six places become ten. */
.map[data-rowhint] .map-band{ filter: var(--map-quiet); }
.map[data-rowhint="0"] .map-band[data-row="0"],
.map[data-rowhint="1"] .map-band[data-row="1"],
.map[data-rowhint="2"] .map-band[data-row="2"]{
  filter: none;
  transform: scale(1.015);
  box-shadow: 0 0 0 3px rgba(140,130,232,.35);
}
.map[data-rowhint="0"] .mkey:not([data-row="0"]),
.map[data-rowhint="1"] .mkey:not([data-row="1"]),
.map[data-rowhint="2"] .mkey:not([data-row="2"]){ filter: var(--map-quiet); }

/* Hint 2: the path is drawn (SVG above), and the home it starts from is ringed. */
.mkey.is-from .mkey-face{
  box-shadow: 0 0 0 3px var(--mint-deep), 0 4px 10px rgba(74,59,78,.12);
}

/* Hint 3: the answer. Ten houses become one, so the rest of the hinted row goes
   grey as well and the map holds exactly one thing in colour. A slow pulse,
   floor well above zero so the key is never invisible on the dark half of the
   cycle, and comfortably under 3 Hz. */
.map[data-keyhint] .mkey{ filter: var(--map-quiet); }
.mkey.is-target{ z-index: 2; }
.mkey.is-target .mkey-face{
  background: #FFF9E6;
  box-shadow: 0 0 0 4px var(--sun-deep), 0 8px 22px rgba(233,174,60,.45);
  animation: glow 1.4s ease-in-out infinite;
}
@keyframes glow{
  0%,100%{ transform: scale(1); filter: brightness(1); }
  50%    { transform: scale(1.07); filter: brightness(1.06); }
}

/* Right: a bounce and a green ring. Colour is never alone. */
.mkey.is-right .mkey-face{
  box-shadow: 0 0 0 4px var(--yes), 0 8px 20px rgba(95,191,152,.4);
  animation: pop .55s cubic-bezier(.2,.9,.3,1.4);
}
@keyframes pop{
  0%{ transform: scale(1); } 35%{ transform: scale(1.22); }
  70%{ transform: scale(.96); } 100%{ transform: scale(1); }
}

/* Wrong: a small wobble and a soft mauve ring. Not red, not a shake that reads
   as anger, and over in under half a second. */
.mkey.is-wrong .mkey-face{
  box-shadow: 0 0 0 3px var(--nope), 0 4px 10px rgba(74,59,78,.12);
  animation: wobble .38s ease;
}
@keyframes wobble{
  0%,100%{ transform: translateX(0) rotate(0); }
  25%{ transform: translateX(-4px) rotate(-2.5deg); }
  75%{ transform: translateX(4px) rotate(2.5deg); }
}

/* THE HOME A PATH IS DRAWN FROM IS NEITHER LIT NOR QUIET, and it is the one key
   on this map that has to be both (owner's call, 2026-08).
 *
 * It used to be simply exempt, at full colour, and about half the time it is in a
 * DIFFERENT ROW from the answer, because the paths worth drawing run up and down
 * the finger columns as often as along a row. So the map said "look in the
 * middle row", greyed the sixteen houses in the other two, and left one of them
 * shouting: two full-colour keys, one of them outside the very band the hint had
 * just drawn a box around. Read as a rendering fault rather than as a hint, which
 * it is: the whole point of the wash is that colour means "here", and a second
 * thing in colour spends that meaning.
 *
 * A ring alone would not do instead. The child is being told "it is next to Dog",
 * out loud, and a grey Dog is a picture they have to work to recognise at the
 * moment they are already stuck. So the home keeps its picture and its ring and
 * takes one step back: quieter than the row being pointed at, louder than the map
 * behind it, and never the loudest thing on screen. At the answer rung it is a
 * step behind the one key still in colour, which is what the rung means.
 *
 * It applies wherever the home is drawn, in its own row or another one. A ringed
 * key at full strength INSIDE the hinted row was the same problem with a shorter
 * fuse: it is the most eye-catching house in the ten and it is the one place the
 * answer is guaranteed not to be. */
.map[data-rowhint] .mkey.is-from{ filter: var(--map-aside); }

/* THE EXEMPTIONS, and they are the half that keeps the greying honest. Two keys
   are never quiet whatever rung the hint is on: the answer itself, and the key
   under the child's finger right this second, right or wrong, because a press
   that answers in grey reads as a press the game did not take. Written last and
   at the same weight as the rule above, so source order settles it: this is also
   what puts a press on the home key back to full strength for as long as the
   flash lasts. */
.map[data-rowhint] .mkey.is-target,
.map[data-rowhint] .mkey.is-right,
.map[data-rowhint] .mkey.is-wrong{ filter: none; }

/* --------------------------------------------------------- 8 slots and pips */
.slots{ display: flex; gap: .4rem; justify-content: center; flex-wrap: wrap; }
.slot{
  width: clamp(38px, 8vw, 56px);
  height: clamp(46px, 9.5vw, 66px);
  border-radius: var(--r-sm);
  background: var(--card);
  box-shadow: var(--shadow-in);
  display: grid; place-items: center;
  font-family: var(--display); font-weight: 800;
  font-size: clamp(1.3rem, 4vw, 2rem);
  color: var(--ink-faint);
}
.slot.is-done{ background: var(--mint); color: #22513F; box-shadow: var(--shadow-1); }
/* THE LETTER THAT IS PRINTED BUT NOT YET TYPED, which is the whole of the
   scaffold on the `build` rung: the word is in view, so the only question left is
   where each letter lives. Faint rather than absent, and faint rather than as
   solid as a typed one, so the row reads at a glance as "these are done, this one
   is now, these are still to come". */
.slot.is-shown{ color: var(--ink-faint); opacity: .55; }
/* A SLOT THE GAME IS GOING TO TYPE, WHICH IS EVERY ONE BUT THE FIRST ON THE
   `first` RUNG. Drawn as an outline with no fill: an empty frame is a promise
   that something goes there, and a filled box is a thing that is already
   finished. Next to it the one slot that IS the child's keeps its solid card and
   stands up under `.is-now`, so the row says "press this one, watch the rest"
   with no words in it, which matters because the audience cannot read the words.
   As the demonstration runs each outline takes its letter and goes solid, so the
   promise is kept in front of them one letter at a time. */
.slot.is-watch{
  background: none;
  box-shadow: inset 0 0 0 2px var(--ink-faint);
  opacity: .5;
}
/* The slot being asked for right now, marked by shape as well as colour, and
   lifted so the child's eye lands on it without being told. This is the attention
   shift: the word does not move, the ONE letter being asked for stands up. */
.slot.is-now{
  box-shadow: 0 0 0 3px var(--lilac-deep), var(--shadow-in);
  opacity: 1;
  transform: translateY(-6px) scale(1.06);
  animation: slotnow 1.8s ease-in-out infinite;
}
.slot{ transition: transform .18s cubic-bezier(.2,.9,.3,1.4), opacity .18s ease; }
/* A slow breath, well under 3 Hz, with a floor: a marker caught on its dark half
   is a marker that is not there. */
@keyframes slotnow{
  0%,100%{ transform: translateY(-6px) scale(1.06); }
  50%    { transform: translateY(-9px) scale(1.10); }
}
/* A space in a two-word label. Drawn as a gap rather than a box, because it is
   a gap, and the space bar it asks for is the widest key on the map. */
.slot.is-gap{ background: none; box-shadow: none; width: clamp(16px, 3vw, 26px); }

/* The pips counting down to the next collected picture. A four year old cannot
   read a progress bar and can absolutely count five dots. */
.pips{ display: flex; gap: .4rem; }
.pip{
  width: 14px; height: 14px; border-radius: 50%;
  background: rgba(74,59,78,.13);
  transition: transform .25s cubic-bezier(.2,.9,.3,1.5), background .25s ease;
}
.pip.is-on{ background: var(--sun-deep); transform: scale(1.18); }

/* --------------------------------------------------- 9 celebration layer */
.fx{ position: fixed; inset: 0; pointer-events: none; z-index: 50; }
.fx-canvas{ position: absolute; inset: 0; }

.fx-card{
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  display: grid; justify-items: center; gap: .3rem;
  padding: 1.2rem 2rem;
  border-radius: var(--r-xl);
  background: rgba(255,255,255,.94);
  box-shadow: var(--shadow-2);
  animation: cardin .34s cubic-bezier(.2,.9,.3,1.4);
  text-align: center;
}
/* A card that is waiting to be tapped says so and behaves like it. The others
   are tappable too (any tap has always skipped) and simply do not advertise it. */
.fx-layer.is-waiting .fx-card{ cursor: pointer; }
.fx-card.is-out{ animation: cardout .2s ease forwards; }
@keyframes cardin{
  from{ transform: translate(-50%,-50%) scale(.7); opacity: 0; }
  to  { transform: translate(-50%,-50%) scale(1); opacity: 1; }
}
@keyframes cardout{ to{ transform: translate(-50%,-50%) scale(1.06); opacity: 0; } }

.fx-card.is-big{ padding: 1.6rem 2.4rem; }
.fx-card.is-master{ background: linear-gradient(180deg, #FFFDF5, #FFF1CE); }
.fx-pic{ width: clamp(150px, 34vw, 260px); height: auto; }
.fx-pic.is-small{ width: clamp(96px, 22vw, 150px); }
.fx-line{ font-family: var(--display); font-weight: 800; font-size: clamp(1.1rem, 3.4vw, 1.6rem); margin: 0; }
.fx-name{ font-family: var(--display); font-weight: 700; color: var(--ink-soft); margin: 0; font-size: 1.05rem; }
.fx-name.is-small{ font-size: .92rem; color: var(--ink-faint); font-weight: 600; }
/* The card for the first door shows the door's OWN empty plot, the same dashed
   square the button carries, so the thing being introduced and the thing that
   turns up underneath afterwards are visibly one object. */
.fx-plot{
  display: grid; place-items: center;
  width: clamp(96px, 24vw, 160px); aspect-ratio: 1;
  color: var(--sun-deep);
  border-radius: var(--r-lg);
  background: rgba(255,255,255,.5);
  box-shadow: inset 0 0 0 3px rgba(255,199,92,.4);
}
.fx-plot svg{ width: 62%; height: 62%; }
.fx-card.is-door .fx-plot{ animation: plot-pop .6s cubic-bezier(.2,.9,.3,1.5) both; }
@keyframes plot-pop{
  0%{ transform: scale(.5) rotate(-8deg); opacity: 0; }
  100%{ transform: scale(1) rotate(0); opacity: 1; }
}
.fx-letter{ font-family: var(--display); font-weight: 800; font-size: clamp(3rem, 12vw, 5.5rem); color: var(--sun-deep); line-height: 1; }
.fx-crown{ font-size: clamp(1.6rem, 5vw, 2.4rem); color: var(--sun-deep); }
.fx-shine{
  position: absolute; inset: -30%;
  background: conic-gradient(from 0deg, transparent, rgba(255,217,138,.35), transparent 40%);
  animation: spin 5s linear infinite;
  border-radius: 50%;
  z-index: -1;
}
@keyframes spin{ to{ transform: rotate(360deg); } }

/* THE FINISHED WORD, assembled. The letters land left to right in the order they
   were typed, a beat apart, which is what makes the card a consequence of what
   the child just did rather than a caption printed over it. The stagger is one
   custom property, so a nine letter name costs exactly what a three letter one
   costs. */
.fx-word{ display: flex; gap: .35rem; justify-content: center; flex-wrap: wrap; }
.fx-word b{
  font-family: var(--display); font-weight: 800;
  font-size: clamp(1.8rem, 7vw, 3rem);
  line-height: 1;
  min-width: 1.1em;
  padding: .18em .1em;
  border-radius: var(--r-sm);
  background: var(--mint);
  color: #22513F;
  box-shadow: var(--shadow-1);
  animation: tiledrop .42s cubic-bezier(.2,.9,.3,1.5) backwards;
  animation-delay: calc(var(--i) * .09s);
}
.fx-word .fx-gap{ width: .5em; }
@keyframes tiledrop{
  from{ transform: translateY(-18px) scale(.7); opacity: 0; }
  to  { transform: none; opacity: 1; }
}
/* The first whole word ever. The one card that is allowed to look different from
   every other card, because it happens once. */
.fx-card.is-first{ background: linear-gradient(180deg, #FFFDF5, #F3E9FF); }
.fx-card.is-first .fx-word b{ background: var(--sun); color: #5A4212; }
/* A picture earned on the same turn rides along inside the word card rather than
   queueing behind it: one celebration, two facts, and no child left waiting
   through a second animation for a thing they can already see. */
.fx-also{
  display: flex; align-items: center; gap: .5rem;
  font-family: var(--display); font-weight: 700; color: var(--ink-soft);
}
.fx-pic.is-tiny{ width: clamp(44px, 10vw, 64px); }

/* ------------------------------------------------------------- 10 the book */
/* THE STICKER BOOK, and it is trying to be an OBJECT rather than a screen.
 *
 * What is being prototyped here is a physical album: board covers, a stitched
 * spine, a ribbon, cream pages with a printed trim, and slots that were clearly
 * waiting for the sticker that goes in them. So the whole section is built from
 * the paper thing outward, and every effect below is imitating something a
 * printer or a bindery actually does:
 *
 *   the gutter shading      pages curve into a spine and catch less light there
 *   the stack edges         there are more pages behind this one
 *   the scallop and trim    a die-cut border and a printed keyline
 *   the grain               uncoated stock has tooth
 *   the tilt on a sticker   a child pressed it on by hand, slightly crooked
 *   the die-cut white edge  real stickers are cut with a margin around the art
 *   the gloss               vinyl catches the light and paper does not
 *   the foil                the one expensive thing on the page, spent on the
 *                           two marks that mean you finished something
 *
 * None of it is decoration for its own sake: a collection that looks printed
 * feels finishable, and a page you can see the empty spaces in is an invitation.
 */
.screen-book{ padding: 2vh 1rem 4rem; display: grid; }
/* THE STAGE PACKS TO THE TOP, and that one line is a bug fix rather than a
   preference. A grid stretches its auto rows to fill whatever height it is
   given, and this stage is given the whole screen, so the album's height was
   really "the leftover screen divided by however many children the stage
   happens to have": the contents spread had one child and swallowed all of it
   (767px), a letter spread had two (the album and the edge nav) and split it
   (645px), and the same book came out a fifth taller on pages one and two.
   Nothing about a page's own content was involved, which is why it could not be
   fixed by editing either page. */
/* THE SIZE RULE, STATED ONCE AND ENFORCED RATHER THAN PROMISED.
 *
 *   A SPREAD IS ONE SHAPE AND ONE SIZE ON EVERY PAGE OF THE BOOK. The shape is a
 *   fixed aspect ratio. The size is as much of the screen as there is room for.
 *   Neither depends on what is printed inside.
 *
 * Everything before this was a FLOOR (`min-height`), which can only promise that
 * a spread is at least so tall, so pages with more on them were taller and the
 * book changed shape as you turned through it. A floor cannot express this rule;
 * an aspect ratio can, and `container-type: size` on the page makes it
 * structural: a sized container's children cannot feed their height back into
 * it, so no amount of content can move the book. That is the difference between
 * a rule and a habit.
 *
 * IT MATTERS BECAUSE OF WHO IS HOLDING IT. The turn arrows, the thumb tabs and
 * the back button all sit around the book, so a book that changes height moves
 * every control on the screen under a hand that is already reaching for one. At
 * four, aiming at a target that moved is not a small annoyance, it is the
 * difference between driving this thing yourself and asking for help.
 *
 * AND IT IS AS BIG AS THE ROOM ALLOWS (owner's call, 2026-08). The stage was
 * capped at 1060px and the album at 620px tall, which on a large monitor is a
 * postcard in an empty field. This is the payoff screen of the whole game; it
 * gets the screen. The cap is now high enough to stop mattering on any ordinary
 * display, and the height budget is what really bounds it.
 */
.album-stage{
  /* The height the book may have, once the furniture around it is paid for: the
     back button above, the fore edge below, and a little air. Everything else
     here is derived from it, so there is one number to change. */
  --book-h: min(78vh, 1180px);
  --spread-ar: 1.58;                  /* two pages side by side, a real album */
  width: min(1760px, 100%); margin: 0 auto;
  display: grid; justify-items: center; align-content: start; gap: 1rem;
  position: relative; padding-top: 3.2rem;
}
.bk-back{ position: absolute; left: 0; top: 0; z-index: 6; }

.sr-only{
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---------------------------------------------------------------- the cover */
/* The one screen in this game that is a single object rather than a layout, and
   the moment the whole section is built around: a closed book with the child's
   name on it that visibly OPENS. */
/* THE CLOSED BOOK IS ONE PAGE OF THE OPEN ONE (owner's call, 2026-08). It was a
   400px card with a 3:4 shape of its own, so tapping it swapped one object for a
   differently sized one and the swing that is supposed to say "this is a book"
   said "this is a screen transition". A closed album is exactly its own right
   hand page: same height as the spread, half its width. Both come from the same
   two variables the album is built from, so the cover cannot drift from the book
   again. */
.cover-wrap{
  perspective: 1800px; padding: 1rem 0 2rem;
  width: min(100%, calc(var(--book-h) * var(--spread-ar)));
  display: grid; justify-items: center;
}
.cover{
  position: relative;
  width: 50%; aspect-ratio: calc(var(--spread-ar) / 2);
  border: 0; padding: 0;
  /* Spine on the left, so the board is cut like a real cover: a tight radius on
     the bound edge and a generous one on the three that a hand touches. */
  border-radius: 10px 26px 26px 10px;
  background:
    linear-gradient(115deg, rgba(255,255,255,.42), transparent 42%),
    linear-gradient(180deg, var(--board), var(--board-deep));
  box-shadow:
    /* the page block peeking out past the board */
    4px 5px 0 -2px #FFF8EA, 7px 8px 0 -3px #F4E4C8,
    0 18px 40px rgba(74,59,78,.28);
  display: grid;
  grid-template-rows: auto 1fr auto auto;
  justify-items: center;
  gap: .5rem;
  padding: clamp(1.4rem, 4vw, 2.2rem) clamp(1rem, 3.5vw, 1.8rem) clamp(1rem, 3vw, 1.6rem);
  transform-origin: left center;
  transform-style: preserve-3d;
  transition: transform .18s cubic-bezier(.2,.9,.3,1.3), box-shadow .18s ease;
}
.cover:hover{ transform: translateY(-4px) rotateY(-4deg); }
.cover:active{ transform: translateY(1px) rotateY(-2deg); }
/* THE SWING, and it is the single most valuable animation in the section: it is
   the one that tells a child this is a book rather than a page of a website.
   Lifted out over the contents that are already painted underneath it, and
   stopping just short of side-on, because the back of a cover is its own front
   seen mirrored and there is no reading of that other than "broken". */
.cover.is-lifted{
  position: absolute; z-index: 8;
  aspect-ratio: auto;                /* the lift sets a real width and height */
  pointer-events: none;
  backface-visibility: hidden;
}
.cover.is-opening{
  transform: rotateY(-86deg);
  opacity: 0;
  transition: transform .58s cubic-bezier(.5,0,.6,1), opacity .34s ease .26s;
}
/* Its own perspective, because once it is lifted onto the stage it is no longer
   inside the wrapper that was providing one. */
.album-stage{ perspective: 1900px; }
/* The bound edge: a darker strip with the stitching printed on it. */
.cover-board{
  position: absolute; left: 0; top: 0; bottom: 0; width: 26px;
  border-radius: 10px 0 0 10px;
  background:
    repeating-linear-gradient(180deg,
      transparent 0 9px, rgba(255,255,255,.5) 9px 15px, transparent 15px 24px),
    linear-gradient(90deg, var(--board-dark), rgba(0,0,0,0) 92%);
  box-shadow: inset -6px 0 12px rgba(160,60,100,.18);
}
.cover-plate{
  display: grid; justify-items: center; gap: .1rem;
  padding: .7rem 1.4rem .9rem;
  border-radius: var(--r-md);
  background: linear-gradient(180deg, #FFFDF6, #FFF3DF);
  /* A foil-stamped label stuck on the board. */
  box-shadow: 0 0 0 3px var(--foil), 0 0 0 4px rgba(255,255,255,.6),
              0 6px 14px rgba(120,60,80,.22);
  margin-left: 12px;
}
.cover-eyebrow{
  font-family: var(--ui); font-weight: 800; font-size: clamp(.85rem, 2.6vw, 1.05rem);
  color: var(--board-dark); letter-spacing: .06em; text-transform: uppercase;
}
/* The foil-stamped title. Painted as a real ink colour FIRST and only cut out of
   a gradient where the browser says it can: a `background-clip: text` that is
   not honoured leaves a transparent fill over a transparent background, which
   is a cover with no title on it. */
.cover-title{
  font-family: var(--display); font-weight: 800;
  font-size: clamp(1.7rem, 6.2vw, 2.5rem); line-height: .98; text-align: center;
  color: var(--foil-deep);
}
@supports ((background-clip: text) or (-webkit-background-clip: text)){
  .cover-title{
    background: linear-gradient(180deg, var(--foil-lite), var(--foil) 45%, var(--foil-deep));
    -webkit-background-clip: text; background-clip: text;
    color: transparent; -webkit-text-fill-color: transparent;
    filter: drop-shadow(0 1px 0 rgba(255,255,255,.8));
  }
}
/* The die cut window, showing the child's own stickers through the cover. */
.cover-window{
  position: relative; align-self: center;
  width: 78%; aspect-ratio: 1 / .82;
  border-radius: var(--r-lg);
  background: radial-gradient(120% 100% at 50% 0%, #FFFDF7, #FFF1DC);
  box-shadow: inset 0 0 0 3px rgba(255,255,255,.85),
              inset 0 6px 16px rgba(150,90,110,.22),
              0 2px 0 rgba(255,255,255,.5);
  display: grid; place-items: center;
  margin-left: 12px;
}
.cover-peek{
  position: absolute; width: 48%; height: auto;
  filter: drop-shadow(0 4px 8px rgba(120,70,90,.28));
}
.cover-peek.p0{ transform: rotate(-8deg) translate(-26%, 6%); z-index: 2; }
.cover-peek.p1{ transform: rotate(5deg)  translate(24%, -2%); z-index: 1; width: 44%; }
.cover-peek.p2{ transform: rotate(-2deg) translate(2%, 26%);  z-index: 3; width: 40%; }
.cover-empty{
  font-weight: 700; color: var(--ink-faint); text-align: center;
  font-size: clamp(.8rem, 2.6vw, .95rem); line-height: 1.35; padding: 1rem;
}
.cover-count{
  font-weight: 700; color: #7E2E4F; margin-left: 12px;
  background: rgba(255,255,255,.72); padding: .3em 1em; border-radius: 999px;
}
.cover-count b{ font-family: var(--display); font-size: 1.15em; }
/* The badge that says opening this is worth doing right now. */
.cover-new{
  position: absolute; top: -12px; right: -10px;
  font-family: var(--display); font-weight: 800; font-size: 1rem;
  color: #6A4A0E; background: linear-gradient(180deg, var(--sun), var(--sun-deep));
  padding: .35em .9em; border-radius: 999px;
  box-shadow: 0 4px 10px rgba(233,174,60,.5), inset 0 0 0 2px rgba(255,255,255,.6);
  transform: rotate(8deg);
}
.cover-open{
  font-family: var(--display); font-weight: 800;
  font-size: 1.05rem; color: #fff; margin-left: 12px;
  letter-spacing: .1em; text-transform: uppercase; opacity: .9;
}

/* ---------------------------------------------------------------- the album */
/* The size rule above, applied. The ratio decides the shape, the height budget
   and the available width decide the size, and `min()` picks whichever of the
   two the screen actually has: a tall narrow window makes a width-bound book, a
   wide short one makes a height-bound book, and both are the same shape. */
.album{
  position: relative;
  aspect-ratio: var(--spread-ar);
  width: min(100%, calc(var(--book-h) * var(--spread-ar)));
  display: grid; grid-template-columns: 1fr 1fr;
  perspective: 2200px;
  filter: drop-shadow(0 20px 34px rgba(74,59,78,.20));
}
.page{
  position: relative;
  padding: clamp(1rem, 2.2cqw, 2rem);
  background: var(--page);
  overflow: hidden;
  display: grid;
  align-content: center;
  backface-visibility: hidden;
  /* THE MEASURING STICK, AND THE LOCK. Everything printed on a page is sized
     against the page rather than against the viewport, which is the only
     question with a right answer (the album is capped, the page has padding,
     and the phone layout is a different shape entirely) and is also what lets
     the whole book scale as ONE object when it gets more room.

     `size` rather than `inline-size` is the enforcement half of the size rule:
     a sized container is laid out independently of its contents, so a page can
     never be pushed taller by what is on it. It is safe here and would not have
     been before, because the album now has a definite height to give it. */
  container-type: size;
}
/* THE GUTTER. Pages curve into the spine and catch less light there, and this
   one gradient is most of what makes two rectangles read as an open book. */
.page-left{
  border-radius: 18px 4px 4px 18px;
  background-image: linear-gradient(90deg,
    rgba(0,0,0,0) 62%, rgba(150,110,60,.05) 84%, rgba(150,110,60,.16));
  transform-origin: right center;
  box-shadow: -3px 0 0 -1px #FBF2E2, -6px 0 0 -2px #F3E6CE, -9px 0 0 -3px #EBDCBE;
}
.page-right{
  border-radius: 4px 18px 18px 4px;
  background-image: linear-gradient(270deg,
    rgba(0,0,0,0) 62%, rgba(150,110,60,.05) 84%, rgba(150,110,60,.16));
  transform-origin: left center;
  box-shadow: 3px 0 0 -1px #FBF2E2, 6px 0 0 -2px #F3E6CE, 9px 0 0 -3px #EBDCBE;
}
/* THE PAGE TURN. The incoming page swings down onto the spine, which is what a
   page does. It animates the real element rather than a cloned ghost, so there
   is nothing to clean up and nothing that can be left stuck over the book. */
.album[data-turn="fwd"] .page-right{ animation: leaf-fwd .46s cubic-bezier(.3,.7,.4,1) both; }
.album[data-turn="fwd"] .page-left{ animation: leaf-fade .34s ease both; }
.album[data-turn="back"] .page-left{ animation: leaf-back .46s cubic-bezier(.3,.7,.4,1) both; }
.album[data-turn="back"] .page-right{ animation: leaf-fade .34s ease both; }
@keyframes leaf-fwd{
  from{ transform: rotateY(-88deg); filter: brightness(.86); }
  to  { transform: rotateY(0);      filter: brightness(1); }
}
@keyframes leaf-back{
  from{ transform: rotateY(88deg);  filter: brightness(.86); }
  to  { transform: rotateY(0);      filter: brightness(1); }
}
@keyframes leaf-fade{ from{ opacity: .35; } to{ opacity: 1; } }

/* The paper itself: tooth, a printed trim line, and the scalloped die cut top
   and bottom. Every one of them is in the ::before/::after of one throwaway
   element so the page's own layout never has to know they exist. */
.page-grain{
  position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  /* Uncoated stock. Generated noise rather than a texture file: it costs nothing
     to ship and it tiles perfectly. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
  opacity: .055;
  mix-blend-mode: multiply;
}
/* The printed keyline, inset like a trim mark. */
.page::before{
  content: ''; position: absolute; inset: 10px;
  border: 1px solid var(--page-trim); border-radius: 12px;
  pointer-events: none; opacity: .55;
}
/* The scallop, top and bottom, exactly the border the pack's own album art has. */
.page::after{
  content: ''; position: absolute; left: 0; right: 0; height: 11px;
  top: 0; pointer-events: none;
  background-image: radial-gradient(circle at 11px 0, transparent 9.5px, var(--page-trim) 10px);
  background-size: 22px 11px;
  opacity: .5;
}
.page.is-shelf::after, .page.is-index::after{ transform: scaleY(-1); top: auto; bottom: 0; }
.page-no{
  position: absolute; bottom: 14px; margin: 0;
  font-family: var(--display); font-weight: 700;
  font-size: .8rem; color: var(--ink-faint); opacity: .8;
}
.page-left .page-no{ left: 24px; }
.page-right .page-no{ right: 24px; }

/* The spine: a shadow, a stitch line and the ribbon. */
.spine{
  position: absolute; left: 50%; top: 0; bottom: 0; width: 26px;
  transform: translateX(-50%); pointer-events: none; z-index: 3;
  background:
    repeating-linear-gradient(180deg,
      transparent 0 12px, rgba(160,110,70,.16) 12px 20px, transparent 20px 32px),
    linear-gradient(90deg, rgba(120,80,40,.14), rgba(120,80,40,.22) 50%, rgba(120,80,40,.14));
}
.ribbon{
  position: absolute; left: 50%; top: 0; width: 11px; height: calc(100% + 26px);
  transform: translateX(-50%);
  background: linear-gradient(90deg, var(--ribbon), #FFC0CE 45%, var(--ribbon));
  box-shadow: 0 2px 6px rgba(120,60,80,.25);
  /* The notched tail, hanging out of the bottom of the book. */
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% calc(100% - 14px), 0 100%);
}

/* ------------------------------------------------------- the contents pages */
/* THE TITLE PAGE FILLS ITS PAGE. It was a 30ch column of rem-sized type sitting
   in the middle of a page that is now nearly a thousand pixels wide, which is
   the same complaint the whole book had, one page further in: capped lengths
   cannot grow with a container that grows. Everything here is measured against
   the page, so the title page is one drawing that scales with the album. */
/* STRETCH, or the block's `height: 100%` is a no-op: .page centres its rows, so
   the row is content-height and 100% of it is the content. This is the same one
   line the index page opposite needs, and it is the difference between a title
   page laid out down the leaf and one floating in the middle of it. */
.is-title{ align-content: stretch; justify-items: center; text-align: center; }
/* Laid out down the whole leaf rather than as a short block floating in the
   middle of it, which is the same call the index wall opposite makes. A title
   page is allowed generous margins; it is not allowed to use half the paper. */
.title-block{
  display: grid; justify-items: center; align-content: space-evenly;
  width: 100%; max-width: 84cqw; height: 100%; padding: 4cqh 0;
}
.title-eyebrow{
  margin: 0; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  font-size: clamp(.7rem, 2.6cqw, 1.15rem); color: var(--ink-faint);
}
.title-name{
  font-size: min(13cqw, 11cqh); color: var(--lilac-deep);
  line-height: 1.05;
}
.title-rule{
  width: 64%; height: max(6px, .8cqh); border-radius: 3px;
  background:
    radial-gradient(circle at 50% 50%, var(--foil) 0 5px, transparent 5.5px) 50% 0 / 24px 6px repeat-x,
    linear-gradient(90deg, transparent, var(--page-trim) 20%, var(--page-trim) 80%, transparent);
  margin: .2rem 0 .3rem;
}
.title-count{
  margin: 0; font-weight: 700; color: var(--ink-soft); line-height: 1.35;
  font-size: clamp(.85rem, 3cqw, 1.5rem);
}
.title-count b{ font-family: var(--display); font-size: 2.2em; color: var(--pink-deep); }
.title-count span{ font-size: .72em; color: var(--ink-faint); }
.title-meter{
  width: 78%; height: max(14px, 2.2cqh); border-radius: 999px;
  background: rgba(74,59,78,.08); box-shadow: var(--shadow-in);
  overflow: hidden; margin: .2rem 0 .4rem;
}
.title-meter i{
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--mint), var(--sun) 60%, var(--pink));
  transition: width .5s cubic-bezier(.2,.9,.3,1);
}
/* The legend. A collection with marks on it needs to say what they mean once,
   in the one place a grown-up will read it out loud. */
.title-key{
  list-style: none; margin: 1cqh 0 0; padding: 0;
  display: grid; gap: 1.4cqh; color: var(--ink-soft);
  font-size: clamp(.78rem, 2.5cqw, 1.25rem);
  text-align: left; font-weight: 600;
}
.title-key li{ display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 2cqw; }
.title-key b{ font-family: var(--display); color: var(--ink); font-size: 1.15em; }
/* THE LEGEND SHOWS THE REAL MARKS. It used to draw its own two badges out of
   gradients and glyphs, which meant the key to the book and the book disagreed
   about what a prize looks like. Same pictures, printed small. */
.mark{
  width: min(9cqw, 7cqh); height: auto; object-fit: contain;
}

/* THE INDEX FILLS ITS PAGE TOO, as three bands: a heading, the wall, one line
   under it. Stated as rows so the wall gets everything the other two do not
   want, which is the same shape the sticker page is built in. */
.is-index{
  align-content: stretch; justify-items: center;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.index-head{
  font-size: min(6cqw, 5cqh); color: var(--lilac-deep);
  margin: 0; text-align: center; align-self: center;
}
/* THE INDEX IS THE ALPHABET, A to Z in reading order (owner's call, 2026-08).
   It used to be the keyboard, staggered, on the reasoning that a second
   arrangement of the same twenty six things would undo the first. The MAP still
   is the keyboard and is still the only thing teaching where a letter lives, and
   every letter's own page still prints its address in words; what a contents is
   for is FINDING a page, and a child looking for G looks for it after F.
   Because the order is the alphabet, a line break is not a rearrangement (the
   letters still run A to Z however many fit a row), which is the one liberty the
   map itself can never take. */
/* Seven keys and six gaps have to fit the page's own width, which is what the
   container query unit buys: 7 x 12.5 + 6 x 1.6 = 97cqw, inside the page
   whatever the album is scaled to. A viewport unit here shipped a clipped P and
   a clipped M. Seven rather than six because 26 letters land in four rows
   instead of five.

   THE 56px CAP IS GONE, and it was the whole reason this page looked empty
   inside a book that had just been made twice as big: a key that may not exceed
   56px stays 56px however much room it is given, so the wall sat in the middle
   of a page nearly a thousand pixels wide looking like a widget. The height
   bound replaces it and is the honest one, because what the wall must not do is
   overflow its band, and `cqh` is the unit that knows how tall the band is. */
.index-wall{
  display: flex; flex-wrap: wrap; gap: var(--bgap); justify-content: center;
  /* SPACE-EVENLY, not stretch and not centre. Centred, the wall floated in a
     tall page with empty paper above and below it. Stretched, the tabs became
     tall thin columns with the same small drawing marooned in the middle of
     each, which fills the page without giving the child anything bigger to look
     at. The picture is bounded by the tab's WIDTH (seven of them across a page),
     so the honest use of the leftover height is air BETWEEN the rows. Flex also
     keeps the last row of five centred under the seven, which a grid would not. */
  align-content: space-evenly;
  width: calc(var(--bkey) * 7 + var(--bgap) * 6); max-width: 100%;
  height: 100%;
  --bkey: min(12.6cqw, 17.5cqh);
  --bgap: min(1.5cqw, 2cqh);
}
/* A thumb tab, the way an address book has them. */
.tab{
  position: relative;
  border: 0; background: linear-gradient(180deg, #FFFFFF, #FFF7EC);
  border-radius: max(9px, 1.2cqw);
  box-shadow: 0 1px 0 rgba(74,59,78,.10), 0 3px 8px rgba(74,59,78,.10);
  flex: 0 0 auto; width: var(--bkey);
  padding: .8cqh .18rem .6cqh;
  display: grid; justify-items: center; gap: .05rem;
  align-self: start;
  transition: transform .15s cubic-bezier(.2,.9,.3,1.3), box-shadow .15s ease;
}
.tab:hover{ transform: translateY(-3px); box-shadow: 0 2px 0 rgba(74,59,78,.10), 0 8px 16px rgba(74,59,78,.16); }
.tab:active{ transform: translateY(1px); }
.tab-key{
  font-family: var(--display); font-weight: 800; color: var(--lilac-deep);
  font-size: min(4.6cqw, 6.4cqh); line-height: 1.05;
}
.tab-pic{ width: 100%; aspect-ratio: 1; display: grid; place-items: center; }
.tab-pic img{ width: 100%; height: 100%; object-fit: contain; }
.tab-fam{ font-family: var(--display); font-size: min(6cqw, 8cqh); color: var(--pink-deep); }
.tab-count{
  font-size: min(2.6cqw, 3.6cqh); color: var(--ink-faint);
  font-weight: 800; letter-spacing: .02em;
}
/* THE ROW COMES WITH THE LETTER EVEN THOUGH THE ORDER NO LONGER DOES. A tab is
   washed with the colour of the band its letter really lives on, so the index
   reads alphabetically and still says, quietly and in the game's own three
   colours, that G is down in the meadow. That is the half of the keyboard index
   worth keeping: the shape of the map was never what taught the place, the place
   did. */
.tab[data-row="0"]{ background: linear-gradient(180deg, #FFFFFF 58%, var(--row-0)); }
.tab[data-row="1"]{ background: linear-gradient(180deg, #FFFFFF 58%, var(--row-1)); }
.tab[data-row="2"]{ background: linear-gradient(180deg, #FFFFFF 58%, var(--row-2)); }
/* A letter nobody has met yet is quieter, and that is ALL it is. Nothing here is
   locked: every page in this book can be turned to from minute one. */
/* Both of these are written after the row tints and match with the same
   specificity, so source order is what makes them win. Keep them below. */
.tab.is-new-letter{ background: rgba(255,255,255,.5); box-shadow: none; }
.tab.is-new-letter .tab-pic{ opacity: .3; }
.tab.is-full{
  background: linear-gradient(180deg, #FFFDF2, #FFF0CE);
  box-shadow: 0 0 0 2px var(--foil), 0 3px 9px rgba(169,116,23,.28);
}
.tab.is-medal{ outline: 3px solid var(--lilac); outline-offset: -3px; }
.tab-star{
  position: absolute; top: -9px; right: -8px;
  width: clamp(20px, 4.4cqw, 30px); height: auto;
  filter: drop-shadow(0 2px 3px rgba(169,116,23,.45));
}
/* A page with something new in it. A dot, not a number: the point is to send a
   child to the page, not to tell them what is on it. */
.tab-new{
  position: absolute; top: -5px; left: -5px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--pink-deep); box-shadow: 0 0 0 3px var(--page);
  animation: newdot 2.4s ease-in-out infinite;
}
@keyframes newdot{ 0%,100%{ transform: scale(1); } 50%{ transform: scale(1.28); } }

/* TAP TO KEEP PLAYING. A big moment now ends when the CHILD ends it, so the card
   has to say so, and it has to say so only once the moment is over: an
   instruction to tap while the voice is still talking is an instruction to
   interrupt. Hidden until the layer is put into its waiting state, then it fades
   up and breathes, which is as much as a line of type should do next to a
   picture somebody just won. */
.fx-go{
  margin: .5rem 0 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(.78rem, 2.4vw, .95rem); color: var(--ink-faint);
  opacity: 0; transform: translateY(6px);
  transition: opacity .35s ease, transform .35s ease;
  pointer-events: none;
}
.fx-layer.is-waiting .fx-go{
  opacity: 1; transform: none;
  animation: go-breathe 1.8s 1s ease-in-out infinite;
}
@keyframes go-breathe{ 0%,100%{ opacity: .75; } 50%{ opacity: 1; } }
.index-hint{
  margin: 0; font-size: clamp(.72rem, 2.4cqw, 1.1rem); color: var(--ink-faint);
  text-align: center; font-weight: 600; align-self: center;
}

/* ------------------------------------------------------- a letter's spread */
/* LEFT: who this letter is. RIGHT: what you have collected for it. One kind of
   completion on each page, so neither has to explain the other. */
/* MEASURED AGAINST THE PAGE, not against the viewport. Everything on this page
   used to be sized in vw and rem, which is the one thing that cannot work now
   that the book has a fixed shape: the page grows and shrinks with the album,
   and type that does not grow with it either rattles around on a big screen or
   overflows a small one. In cq units the left page is one drawing that scales.
   Both axes are bounded, because this page is tall and narrow on a spread and
   short and wide on a phone. */
.is-letter .lp{
  display: grid; justify-items: center; align-content: center;
  gap: 2cqh; text-align: center; height: 100%;
}
.lp-letter{
  display: flex; align-items: baseline; gap: .1em;
  font-family: var(--display); font-weight: 800;
  font-size: min(26cqw, 20cqh); line-height: .9;
  color: var(--lilac-deep);
  text-shadow: 0 3px 0 rgba(184,176,255,.35);
}
.lp-letter .is-lower{ font-size: .72em; color: var(--pink-deep); text-shadow: 0 3px 0 rgba(255,159,196,.35); }
/* The resident, framed like a photograph in an album, because that is what it
   is: the one picture on this page the child chose themselves. */
.portrait{
  margin: 0; display: grid; justify-items: center; gap: .35rem;
  padding: .7rem .7rem .5rem;
  background: #fff; border-radius: 10px;
  box-shadow: 0 1px 0 rgba(74,59,78,.1), 0 8px 18px rgba(74,59,78,.16);
  transform: rotate(-1.6deg);
  width: min(62cqw, 38cqh);
}
.portrait img{ width: 100%; aspect-ratio: 1; object-fit: contain; }
.portrait figcaption{
  font-family: var(--display); font-weight: 700;
  font-size: clamp(.7rem, 3.4cqw, 1.15rem); color: var(--ink);
}
.portrait.is-family{ background: linear-gradient(180deg, #FFF1F6, #FFE3EC); }
.portrait-fam{
  font-family: var(--display); font-size: 3.4rem; color: var(--pink-deep);
  line-height: 1.1;
}
.portrait.is-vacant{
  background: rgba(255,255,255,.45);
  box-shadow: inset 0 0 0 2px var(--page-trim);
  transform: none;
}
.portrait-vacant{
  font-family: var(--display); font-size: 3rem; color: var(--ink-faint);
  opacity: .5; line-height: 1.5;
}
.portrait.is-vacant figcaption{ color: var(--ink-faint); font-weight: 600; font-size: .82rem; }
/* The address line and the bump line are gone from this page (owner's call,
   2026-08): see the note in js/letters/book.js. ROW_NAMES and the row colours
   are untouched, because the MAP still uses them and the hint still says a row
   out loud, which is the version of that fact a pre-reader can actually use. */
/* THE MEDAL. Not a currency and not a gate: a record that this letter is known,
   sitting on the page about the letter. Empty, it is a printed space like any
   other empty slot in the book, which is the same invitation. */
/* IT IS A DRAWN MEDAL NOW, not a gold disc with the letter in it (owner's call,
   2026-08: "so boring, what a letdown"). The verdict was right and it was right
   about the whole idea rather than about the gradient: a typographic badge is a
   certificate, and a four year old who has learned a letter has earned a PRIZE.
   Unwon it is the same picture printed as an empty space, which is the same
   invitation every other empty slot in this book makes. */
.medal{ display: grid; justify-items: center; gap: .35rem; margin-top: .2rem; }
.medal-art{
  width: clamp(64px, 22cqw, 150px); height: auto;
  /* Not yet won: the shape is there, the prize is not. Printed rather than
     hidden, exactly like an empty slot. */
  filter: grayscale(1) opacity(.26);
  transition: filter .3s ease, transform .3s cubic-bezier(.2,.9,.3,1.3);
}
.medal-cap{
  font-size: clamp(.72rem, 2.3cqw, 1rem); font-weight: 700; color: var(--ink-faint);
  text-align: center;
}
.medal.is-won .medal-art{
  filter: drop-shadow(0 4px 10px rgba(169,116,23,.4));
}
.medal.is-won .medal-cap{ color: var(--foil-deep); }

/* ------------------------------------------------------- the sticker field */
/* THE STICKER PAGE IS THREE BANDS: a header, the grid, and one line under it.
   Stated as rows so the grid gets everything the other two do not want, which
   is what keeps the slots the same size on a page with a rosette on it and a
   page without one. */
.page.is-shelf{
  align-content: stretch;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.sp-head{ display: grid; justify-items: center; gap: .1rem; text-align: center; }
.sp-head h2{ font-size: clamp(1.1rem, 4.4cqw, 2rem); color: var(--lilac-deep); }
.sp-count{ margin: 0; font-weight: 700; color: var(--ink-soft); font-size: clamp(.8rem, 2.6cqw, 1.1rem); }
.sp-count b{ font-family: var(--display); font-size: 1.3em; color: var(--pink-deep); }
/* Which sheet of a two sheet letter this is. Quiet: it is an answer to "why does
   B have two pages", not something a child has to read. */
.sp-part{
  display: inline-block; margin-left: .4em; padding: .05em .5em;
  border-radius: 999px; background: rgba(140,130,232,.14);
  color: var(--lilac-deep); font-size: .82em;
}
/* The one line under the grid, and the rosette when the page is finished. Sized
   as a band so a page that gains a rosette does not move its own stickers. */
.sp-foot{ display: grid; place-items: center; min-height: 3.4cqh; }

/* THREE BY THREE, ON EVERY PAGE (owner's call, 2026-08).
 *
 * Nine slots to a page and three to a row, and the grid is that shape whether
 * the letter holds three stickers or nine: what changes is how many cells are
 * PRINTED. The columns used to swing between two, three and four to make each
 * count look composed, and the honest description of that is that every page was
 * a different layout, which is the other half of why the book never looked like
 * one object.
 *
 * A fixed grid usually risks the thing this album must never do, which is print
 * a slot that can never be filled. It does not here, because the empty CELLS are
 * not slots: a letter with five stickers prints five and the other four cells
 * are simply unoccupied grid area. The page reserves the room; it does not
 * pretend there is something to find in it.
 *
 * THE SLOT SIZE IS A DEFINITE LENGTH, and that is not a style choice. Measured
 * against the page the field is printed on (`cqw` for the width it may take,
 * `cqh` for the height it may take, both available because .page is a size
 * container), the square is a real number before layout starts and the rows
 * cannot collapse into each other, which is what a percentage over an aspect
 * ratio used to do here. Whichever of the two bounds is smaller wins, so the
 * grid fits its page on a wide monitor and on a tall phone without a media
 * query deciding anything. */
.field{
  --fgap: 2cqw;
  --slot: min(29cqw, 21cqh);
  display: grid;
  grid-template-columns: repeat(3, var(--slot));
  grid-template-rows: repeat(3, auto);
  justify-content: center; align-content: center;
  gap: var(--fgap);
  min-height: 0;
}
/* The square is the art box inside the slot and the caption sits under it, which
   is how a printed album is set. The slot itself is sized by the grid. */
.pocket{
  position: relative;
  width: var(--slot);
  border: 0; padding: 0; background: transparent;
  display: grid; grid-template-rows: auto auto; gap: .2rem;
  justify-items: center;
}
/* On a held slot this is a BUTTON (the one that says the name), so it carries
   the reset a button needs; on an empty one it is a span and the reset costs
   nothing. */
.pocket-art{
  position: relative; width: var(--slot); height: var(--slot);
  border: 0; padding: 0; background: transparent; font: inherit; color: inherit;
  border-radius: 16px;
  display: grid; place-items: center;
}
/* Both states reserve the same caption line, so a page half full does not have
   two different row heights in it. */
.pocket-cap{
  min-height: 1.15em; display: grid; place-items: center;
  font-family: var(--display); font-weight: 700;
  font-size: clamp(.7rem, 1.7vw, .9rem);
  color: var(--ink); text-align: center; line-height: 1.1;
  width: 100%; word-break: break-word;
}
/* THE EMPTY SLOT is the more carefully designed of the two, because it is the
   one doing the work: a dashed die line, a printed number, the silhouette of
   what goes here, and a ruled line where the name will be written. It shows the
   SHAPE and never the WORD: the word is the answer to a question the game has
   not asked yet. */
.pocket.is-empty .pocket-art{
  background: rgba(255,255,255,.34);
  box-shadow: inset 0 2px 7px rgba(120,90,60,.10);
  border: 2.5px dashed var(--pocket-hue, var(--lilac));
}
.pocket.is-empty img{
  width: 82%; height: 82%; object-fit: contain;
  filter: grayscale(1) brightness(.55);
  opacity: .13;
}
.pocket-no{
  position: absolute; top: 4px; left: 7px; z-index: 2;
  font-family: var(--display); font-weight: 800; font-size: .72rem;
  color: var(--pocket-hue, var(--lilac)); opacity: .9;
}
.pocket-line{
  width: 74%; height: 2px; border-radius: 2px;
  background: repeating-linear-gradient(90deg,
    var(--page-trim) 0 5px, transparent 5px 9px);
  opacity: .85;
}
/* The five pastels the die lines cycle through, so a page of empty slots reads
   as printed decoration and not as a grid of holes. */
.h-pink { --pocket-hue: var(--pink); }
.h-lilac{ --pocket-hue: var(--lilac); }
.h-mint { --pocket-hue: var(--mint-deep); }
.h-sun  { --pocket-hue: var(--sun-deep); }
.h-sky  { --pocket-hue: var(--sky); }

/* A COLLECTED STICKER. Die cut with a white margin the way a real sticker is,
   pressed on slightly crooked the way a child does it, and glossy, because vinyl
   catches the light and the page it is stuck to does not. */
.pocket.is-held .pocket-art{ cursor: pointer; }

/* WHO LIVES ON THE KEY, SAID WITH A CROWN. The corner keycap badge is gone
 * (owner's call, 2026-08: it polluted the picture, and thirty pixels is not a
 * target for the most interesting decision in the book). Choosing moved into the
 * close-up, where the picture is the size of a hand; all the SLOT has to do now
 * is say which one is chosen, and a little gold crown says it without setting a
 * letter on top of a drawing.
 *
 * It sits half off the corner of the sticker, like something pinned on, and the
 * foil ring stays: "which of my pictures is on my keyboard" should be answerable
 * from across the page. */
.pocket-crown{
  position: absolute; top: -6%; left: -7%; z-index: 3;
  width: 40%; height: auto;
  transform: rotate(-14deg);
  filter: drop-shadow(0 2px 4px rgba(169,116,23,.45));
  pointer-events: none;
}
.pocket.is-onkey .sticker{
  box-shadow: 0 0 0 3px var(--foil), 0 1px 0 rgba(74,59,78,.12),
              0 6px 12px rgba(74,59,78,.20);
}
/* Said once, for the grown-up to read out. The child is shown a shape. */
.sp-pick{
  margin: .9rem 0 0; text-align: center; line-height: 1.35;
  font-size: .76rem; font-weight: 700; color: var(--ink-faint);
}
.sp-pick b{ font-family: var(--display); color: var(--lilac-deep); font-size: 1.15em; }
.sticker{
  position: relative; display: grid; place-items: center;
  width: 100%; height: 100%;
  border-radius: 22%;
  background: #fff;
  transform: rotate(var(--tilt, 0deg));
  box-shadow: 0 1px 0 rgba(74,59,78,.12), 0 6px 12px rgba(74,59,78,.20);
  transition: transform .18s cubic-bezier(.2,.9,.3,1.3), box-shadow .18s ease;
  padding: 8%;
}
.sticker img{ width: 100%; height: 100%; object-fit: contain; }
.gloss{
  position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: linear-gradient(128deg,
    rgba(255,255,255,.72) 0 18%, rgba(255,255,255,.14) 30%,
    rgba(255,255,255,0) 46%, rgba(255,255,255,0) 100%);
  mix-blend-mode: screen;
}
/* Hung off the art button rather than off the whole slot, so hovering the keycap
   in the corner does not lift the sticker it is sitting on. */
.pocket.is-held .pocket-art:hover .sticker{
  transform: rotate(var(--tilt, 0deg)) scale(1.06) translateY(-3px);
  box-shadow: 0 2px 0 rgba(74,59,78,.12), 0 12px 22px rgba(74,59,78,.26);
}
.pocket.is-held .pocket-art:active .sticker{ transform: rotate(var(--tilt, 0deg)) scale(.98); }
/* Tapping a sticker says its name and peels a corner up while it speaks, so the
   thing that answers is visibly the thing that was touched. */
.pocket.is-peel .sticker{ animation: peel .5s cubic-bezier(.3,.8,.4,1); }
@keyframes peel{
  0%  { transform: rotate(var(--tilt,0deg)) scale(1); }
  35% { transform: rotate(calc(var(--tilt,0deg) - 5deg)) scale(1.09) translateY(-5px); }
  100%{ transform: rotate(var(--tilt,0deg)) scale(1); }
}

/* PRESSING A NEW ONE IN. A sticker earned since the book was last opened lands
   on the page in front of the child rather than being found already stuck down.
   One transition, no strobe, and skipped outright under reduced motion. */
.pocket.is-pressing .sticker{
  animation: press-in .62s cubic-bezier(.2,.85,.35,1.2) both;
  animation-delay: var(--press-delay, 0ms);
}
@keyframes press-in{
  0%  { transform: rotate(calc(var(--tilt,0deg) * 4)) scale(2.4); opacity: 0; }
  30% { opacity: 1; }
  72% { transform: rotate(calc(var(--tilt,0deg) * .5)) scale(.93); opacity: 1; }
  100%{ transform: rotate(var(--tilt,0deg)) scale(1); opacity: 1; }
}
.pocket.is-pressing .pocket-art::after{
  content: ''; position: absolute; inset: 0; border-radius: 24%;
  border: 3px solid var(--sun); pointer-events: none;
  animation: press-ring .62s ease-out both;
  animation-delay: var(--press-delay, 0ms);
}
@keyframes press-ring{
  0%  { transform: scale(.5); opacity: 0; }
  40% { opacity: .9; }
  100%{ transform: scale(1.5); opacity: 0; }
}

/* THE PAGE FILLED UP. The one piece of foil on the sticker page, spent on the
   only thing worth spending it on. */
.rosette{
  justify-self: center;
  display: inline-grid; grid-auto-flow: column; align-items: center; gap: .5rem;
  padding: .3em 1.1em .35em .5em; border-radius: 999px;
  background: linear-gradient(180deg, #FFFDF2, #FFEFC6);
  box-shadow: 0 0 0 2.5px var(--foil), 0 4px 12px rgba(169,116,23,.32);
  color: #6A4A0E;
}
.rosette img{
  width: clamp(26px, 6cqw, 46px); height: auto;
  filter: drop-shadow(0 1px 2px rgba(169,116,23,.4));
}
.rosette b{ font-family: var(--display); font-size: clamp(.85rem, 2.6cqw, 1.1rem); }

/* -------------------------------------------------------------- page turns */
/* Big, obvious and outside the paper, so they never compete with a sticker. */
.leaf{
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 5;
  width: 54px; height: 54px; border-radius: 50%;
  border: 0; background: #fff;
  box-shadow: 0 2px 0 rgba(74,59,78,.12), 0 8px 18px rgba(74,59,78,.20);
  display: grid; place-items: center;
  font-size: 2rem; line-height: 1; color: var(--lilac-deep);
  transition: transform .15s cubic-bezier(.2,.9,.3,1.3);
}
.leaf span{ margin-top: -4px; }
.leaf:hover{ transform: translateY(-50%) scale(1.1); }
.leaf:active{ transform: translateY(-50%) scale(.95); }
.leaf-prev{ left: -27px; }
.leaf-next{ right: -27px; }

/* --------------------------------------------------------------- the close-up */
/* A STICKER, THE SIZE OF A HAND. The card is one transformed element and the
 * scrim is another, because the flight is done on the compositor: a FLIP from
 * the slot that was pressed to here, which is the cheapest smooth animation
 * there is and the only one that stays smooth on a two hundred pound tablet.
 *
 * The card is deliberately not a "dialog": no header bar, no chrome, no panel.
 * It is the sticker, its name, and one thing to do with it, on a piece of the
 * same paper the book is printed on. */
.closeup{
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center; padding: 4vmin;
}
.cu-scrim{
  position: absolute; inset: 0; border: 0; padding: 0; cursor: pointer;
  background: rgba(74,59,78,.42);
  backdrop-filter: blur(2px);
  opacity: 0; transition: opacity .28s ease;
}
.closeup.is-in .cu-scrim{ opacity: 1; }
.cu-card{
  position: relative; z-index: 1;
  display: grid; justify-items: center; gap: .6rem;
  padding: clamp(1rem, 3vmin, 2rem) clamp(1.2rem, 4vmin, 2.6rem) clamp(1.1rem, 3vmin, 2rem);
  border-radius: 28px;
  background:
    radial-gradient(120% 90% at 50% 0%, #FFFFFF, var(--page) 70%);
  box-shadow: 0 0 0 3px var(--page-trim), 0 30px 60px rgba(74,59,78,.34);
  /* The flight. The overshoot is small on purpose: a sticker that bounces like a
     toy reads as a toy, and this one is a possession. */
  transition: transform .42s cubic-bezier(.18,.9,.24,1.06), opacity .3s ease;
  will-change: transform;
  max-width: min(92vw, 560px);
}
.cu-art{
  position: relative;
  width: min(58vmin, 400px); aspect-ratio: 1;
  display: grid; place-items: center;
}
.cu-art img{ width: 100%; height: 100%; object-fit: contain; }
/* The vinyl catches the light once, as it arrives. */
.cu-gloss{
  position: absolute; inset: 0; border-radius: 24px; pointer-events: none;
  background: linear-gradient(112deg, transparent 34%, rgba(255,255,255,.55) 46%,
                                      transparent 58%);
  transform: translateX(-120%);
}
.closeup.is-in .cu-gloss{ animation: cu-shine .9s .22s cubic-bezier(.3,.7,.4,1) both; }
@keyframes cu-shine{ to{ transform: translateX(120%); } }
/* NO CROWN OVER THE PICTURE HERE (owner's call, 2026-08). There was one, and it
   covered the sticker completely: `.cu-art img` is a more specific selector than
   `.cu-crown`, so the 100% width meant for the sticker won over the 26% meant for
   the crown, and the close-up of a friend was a close-up of a crown. It is not
   coming back at the right size either, because it was redundant the whole time:
   the button underneath already says "Lives on your N key" and already carries a
   little crown. The slot on the page is where that mark earns its place, over a
   picture the size of a thumbnail with no room for a sentence. */
.cu-name{
  margin: 0; font-family: var(--display); font-weight: 800;
  font-size: clamp(1.6rem, 6vmin, 2.8rem); line-height: 1; color: var(--ink);
}
/* The choice, with room to say the whole sentence. This is the control the
   corner badge could never be. */
/* The most inviting control in the book, so it looks like one: the same mint
   the game uses for "yes, do this" everywhere else, at a size a small hand can
   hit without aiming. */
.cu-pick{
  font-size: clamp(1rem, 2.6vmin, 1.25rem);
  padding: .7em 1.5em; min-height: 60px;
  box-shadow: 0 4px 0 rgba(34,81,63,.18), 0 8px 18px rgba(34,81,63,.18);
}
.cu-pick:active{ box-shadow: 0 1px 0 rgba(34,81,63,.18); }
.cu-on{
  margin: 0; display: inline-grid; grid-auto-flow: column; align-items: center; gap: .45rem;
  padding: .35em 1em .4em .5em; border-radius: 999px;
  background: linear-gradient(180deg, #FFFDF2, #FFEFC6);
  box-shadow: 0 0 0 2.5px var(--foil);
  color: #6A4A0E; font-weight: 800; font-size: clamp(.85rem, 2.2vmin, 1rem);
}
.cu-on img{ width: 26px; height: auto; }
.cu-x{
  position: absolute; top: -14px; right: -14px;
  width: 44px; height: 44px; border-radius: 50%; border: 0; cursor: pointer;
  background: #fff; color: var(--ink-soft); font-size: 1.5rem; line-height: 1;
  box-shadow: 0 0 0 2px var(--page-trim), 0 6px 14px rgba(74,59,78,.28);
  display: grid; place-items: center;
}
.cu-x:hover{ color: var(--ink); transform: scale(1.06); }

/* ------------------------------------------------------ the fore edge index */
/* THE PAGE EDGES ARE THE INDEX, AND THE INDEX IS A CONTROL. Twenty seven thumb
   tabs, each printed with its own page's letter and pressable, standing in for
   the block of page edges a real album shows you the whole time it is open. It
   used to be twenty seven blank marks that only said which one you were
   standing on, which is a progress bar wearing a book's clothes: it could say
   "you are on the fourth page" and offered no way to ask for the ninth.

   The tab you are on STICKS UP out of the block, which is the same thing the
   old marks did with height and is the one part of it worth keeping: a page
   edge that stands proud of the ones beside it is how a book shows a place. */
/* IT IS THE WIDTH OF THE BOOK, because it is the edge of the book. It used to be
   capped at 920px under an album capped at 1060, which on a big screen left a
   short strip of tiny tabs floating under a small book. Both caps are gone: the
   strip belongs to the album and grows with it. */
.edge{
  display: flex; flex-wrap: wrap; gap: 4px;
  justify-content: center; align-items: flex-end;
  width: min(100%, calc(var(--book-h) * var(--spread-ar))); margin-top: 1rem;
}
/* Sized in three numbers rather than one so the strip is a single row of page
   edges on a spread and wraps into a block of them when the book is one page
   wide, without a scroll gesture buried under the one it already uses. The
   letters were too small to lean on (owner's call, 2026-08), and a tab printed
   with a letter nobody can read is the blank mark this replaced. */
.edge-tab{
  flex: 1 1 34px; min-width: 30px; max-width: 62px;
  height: 38px; padding: 0; border: 0;
  border-radius: 3px 3px 7px 7px;
  background: linear-gradient(180deg, #FFFDF6, #F7E9D1);
  box-shadow: inset 0 0 0 1px rgba(150,110,60,.22), 0 2px 4px rgba(74,59,78,.12);
  position: relative; display: grid; place-items: center;
  /* Soft rather than faint: this is furniture, but it is furniture printed with
     the one thing the child is matching, and a letter they have to lean in for
     is a letter they will not use. */
  color: var(--ink-soft);
  transition: transform .15s cubic-bezier(.2,.9,.3,1.3), height .15s ease,
              background .15s ease, box-shadow .15s ease;
}
.edge-face{
  font-family: var(--display); font-weight: 800;
  font-size: 1.05rem; line-height: 1; color: currentColor;
}
/* The second sheet of a letter that has two. Same letter, because it is the same
   letter's page; the dot is what says it is the one after. */
.edge-tab.is-more .edge-face::after{
  content: '\00B7'; margin-left: .12em; opacity: .75;
}
/* The contents is the one spread that is not a letter, so it is marked as a
   list. Drawn from `currentColor` rather than a token, so it comes along when
   the tab it sits on is the one you are standing on and turns white. */
.edge-tab.is-contents .edge-face{
  width: 11px; height: 9px;
  background:
    linear-gradient(currentColor 0 0) 0 0    / 100% 2px no-repeat,
    linear-gradient(currentColor 0 0) 0 50%  / 100% 2px no-repeat,
    linear-gradient(currentColor 0 0) 0 100% / 100% 2px no-repeat;
}
/* THE ROW COMES WITH THE LETTER, exactly as it does on the contents wall: a
   hairline of the band the letter really lives on, printed along the bottom of
   its tab. The strip is bound alphabetically and this is what stops that
   costing anything. */
.edge-tab::after{
  content: ''; position: absolute; left: 4px; right: 4px; bottom: 3px;
  height: 2px; border-radius: 2px; background: transparent;
}
.edge-tab[data-row="0"]::after{ background: var(--row-0); }
.edge-tab[data-row="1"]::after{ background: var(--row-1); }
.edge-tab[data-row="2"]::after{ background: var(--row-2); }
/* A page with something new on it, sending a child to the page without saying
   what is on it. Same dot and same job as the one on the contents wall. */
.edge-tab.is-new::before{
  content: ''; position: absolute; top: -3px; right: -2px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--pink-deep); box-shadow: 0 0 0 2px var(--page);
}
.edge-tab:hover{ transform: translateY(-3px); }
.edge-tab:active{ transform: translateY(1px); }
/* Both of the states below match at one class, so source order is what makes
   them win. `is-here` is last on purpose: where you are outranks how a page is
   doing, and a finished page you are standing on has to read as both. */
.edge-tab.is-full{
  background: linear-gradient(180deg, #FFFCEE, var(--foil-lite));
  box-shadow: inset 0 0 0 1px var(--foil), 0 2px 5px rgba(169,116,23,.26);
  color: var(--foil-deep);
}
.edge-tab.is-here{
  height: 50px;
  background: linear-gradient(180deg, var(--lilac), var(--lilac-deep));
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.45), 0 4px 10px rgba(140,130,232,.42);
  color: #fff;
}
.edge-tab.is-here .edge-face{ font-size: 1.25rem; }
.edge-tab.is-here::after{ opacity: .55; }
.edge-tab.is-here.is-full{ box-shadow: inset 0 0 0 2px var(--foil), 0 4px 10px rgba(169,116,23,.4); }

/* -------------------------------------------------------------- responsive */
/* A phone cannot hold a spread, so the book becomes one page at a time and the
   fold runs across instead of down. It is still a book: same paper, same trim,
   same scallop, same order. */
@media (max-width: 860px){
  /* ONE COLUMN, AND STILL ONE FIXED SHAPE. The stacked layout used to drop the
     size rule entirely ("two stacked pages are sized by what is printed on
     them"), which is the phone getting the bug the desktop just had fixed. The
     spread turns portrait instead, and the sticker page is given the larger
     share of it, because on a phone the collection is what the child came for
     and the letter page is a caption. */
  /* A PHONE IS THE SHAPE THE BOOK SHOULD TAKE THERE. The ratio is tall because
     the screen is, and the height budget is nearly all of it: a fixed shape is
     only worth having if the shape fits the screen it is on, and a portrait
     album floating in the top half of a phone is the same wasted room the
     desktop had. The letter page is given the smaller share on purpose. What a
     child opened the book for is the stickers; the letter page is the caption. */
  .album-stage{ --book-h: min(82vh, 1000px); --spread-ar: .56; padding-top: 2.6rem; }
  /* A phone spread is two pages STACKED, so one page there is a wide short box
     and reads as a card rather than as a closed book. The whole album box is the
     honest closed shape on this layout, and it is still the size the book opens
     to, which is the thing that was wrong. */
  .cover{ width: 100%; aspect-ratio: var(--spread-ar); }
  .album{
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, .5fr) minmax(0, 1fr);
  }
  /* The letter page lies down: the letterform, the resident and the medal in a
     row across a short band, rather than a column that has to be clipped. */
  .is-letter .lp{
    grid-auto-flow: column; grid-auto-columns: auto;
    align-items: center; justify-content: center; gap: 4cqw;
  }
  .lp-letter{ font-size: min(22cqw, 46cqh); }
  .portrait{ width: min(34cqw, 74cqh); }
  .medal-art{ width: min(20cqw, 46cqh); }
  .medal-cap{ display: none; }
  .field{ --slot: min(27cqw, 26cqh); }
  .page-left{
    border-radius: 18px 18px 4px 4px;
    background-image: linear-gradient(180deg,
      rgba(0,0,0,0) 70%, rgba(150,110,60,.06) 88%, rgba(150,110,60,.14));
    box-shadow: none;
  }
  .page-right{
    border-radius: 4px 4px 18px 18px;
    background-image: linear-gradient(0deg,
      rgba(0,0,0,0) 78%, rgba(150,110,60,.05) 92%, rgba(150,110,60,.12));
    box-shadow: 0 4px 0 -1px #FBF2E2, 0 8px 0 -2px #F3E6CE;
    padding-bottom: 2.4rem;
  }
  .spine{
    left: 0; right: 0; width: auto; top: 50%; bottom: auto; height: 22px;
    transform: translateY(-50%);
    background: linear-gradient(180deg, rgba(120,80,40,.10), rgba(120,80,40,.18) 50%, rgba(120,80,40,.10));
  }
  .ribbon{
    left: auto; right: 26px; top: -18px; width: 11px; height: 58px; transform: none;
  }
  /* Page numbers are furniture, and on one column they would sit under the turn
     arrows, which are not. */
  .page-no{ display: none; }
  .is-letter .lp{ gap: .45rem; }
  .lp-letter{ font-size: clamp(3rem, 16vw, 4.6rem); }
  .portrait{ width: min(180px, 52%); }
  /* The turn arrows come off the paper's edge and sit under it, where a thumb
     already is. */
  .leaf{ top: auto; bottom: -22px; transform: none; width: 48px; height: 48px; }
  .leaf:hover, .leaf:active{ transform: scale(1); }
  .leaf-prev{ left: 12%; }
  .leaf-next{ right: 12%; }
  .album-stage{ padding-bottom: 2.2rem; }
  /* THE EDGE STAYS ON A PHONE, and it did not used to. Hiding it was right when
     it was decoration saying which page you were on: a strip of twenty seven
     three pixel marks is unreadable at that width and told a child nothing. It
     is a way of getting to a page now, and it is worth MORE on the layout that
     shows one page at a time, so instead of being hidden it wraps into a block
     of tabs big enough for a thumb. The margin clears the turn arrows, which
     hang below the paper here. */
  .edge{ width: 100%; margin-top: 1.7rem; gap: 4px; }
  .edge-tab{ flex: 1 1 34px; min-width: 32px; max-width: 52px; height: 36px; }
  .edge-tab.is-here{ height: 46px; }
  .edge-face{ font-size: 1.05rem; }
  .edge-tab.is-here .edge-face{ font-size: 1.2rem; }
}
@media (max-width: 520px){
  /* No key override here: `cqw` already measures the page the index is printed
     on, and a viewport-based size clipped P and M on a phone exactly as it did
     on a desktop half-spread. */
  /* No tab-count override any more: it is measured in cq against the page the
     wall is printed on, which is the question that has a right answer at every
     width. A rem cap here was the phone half of the 56px cap above. */
}

/* ------------------------------------------------- 11 the grown-up sheet */
.screen-grown{ padding: 3vh 1rem 5rem; }
.sheet{ width: min(760px, 100%); margin: 0 auto; display: grid; gap: 1.4rem; }
.sheet-head h1{ font-size: clamp(1.6rem, 5vw, 2.2rem); }
.panel-block{
  background: var(--card);
  border-radius: var(--r-lg);
  padding: 1.2rem 1.3rem;
  box-shadow: var(--shadow-1);
  display: grid; gap: .7rem;
}
.panel-block h2{ font-size: 1.15rem; }
.sheet-foot{ display: grid; justify-items: center; padding-bottom: 2rem; }

/* The two maintenance desks, at the bottom of the sheet. A link rather than a
   button because it really is a different page, and the touch target is the
   whole first line so it clears the 48px floor the rest of this game holds. */
.deskrow{ display: grid; gap: .2rem; margin: 0; }
.deskrow a{
  font-family: var(--display); font-weight: 800; font-size: 1.02rem;
  color: var(--lilac-deep); text-decoration-thickness: 2px; text-underline-offset: 3px;
  padding: .35rem 0; min-height: 44px; display: flex; align-items: center;
}
.deskrow a:hover, .deskrow a:focus-visible{ color: var(--ink); }

.fld{ display: grid; gap: .3rem; font-weight: 700; font-size: .95rem; }
.fld input[type=text], .fld select{
  font: inherit; padding: .65em .8em;
  border: 2px solid rgba(74,59,78,.14);
  border-radius: var(--r-sm);
  background: var(--paper);
  color: var(--ink);
  min-height: 48px;
}
/* The MEADOW ink rather than `--mint-deep`, which is the obvious choice and the
   wrong one: a browser derives a range's unfilled track from its accent, and
   against a light accent it picks a near-black track, which on this warm paper
   is the heaviest line on the screen. The deeper green keeps the track light. */
.fld input[type=range]{ width: 100%; accent-color: var(--row-1-ink); }

.fam-list{ display: grid; gap: .4rem; }
.fam{ display: flex; align-items: center; gap: .7rem; }
.fam-key{
  display: grid; place-items: center;
  width: 42px; height: 42px; border-radius: 30%;
  background: var(--pink); color: #7E2E4F;
  font-family: var(--display); font-weight: 800; font-size: 1.15rem;
}
.fam-name{ flex: 1; font-weight: 700; }
.fam-add{ display: flex; gap: .5rem; }
.fam-add input{
  flex: 1; font: inherit; padding: .65em .8em; min-height: 48px;
  border: 2px solid rgba(74,59,78,.14); border-radius: var(--r-sm);
  background: var(--paper); color: var(--ink);
}

.know-map{ display: grid; gap: .35rem; }
.know-row{ display: flex; gap: .35rem; }
.know-row[data-row="1"]{ padding-left: 2%; }
.know-row[data-row="2"]{ padding-left: 6%; }
.know{
  flex: 1 1 0; min-width: 0;
  aspect-ratio: 1; max-height: 52px;
  border: 2px solid rgba(74,59,78,.12);
  border-radius: var(--r-sm);
  background: var(--paper);
  font-family: var(--display); font-weight: 800;
  color: var(--ink-faint);
}
.know.is-on{ background: var(--mint); border-color: var(--mint-deep); color: #22513F; }
.row-gap{ display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }

/* The restore card: what is inside a file that has been opened but not yet
   applied. It is deliberately loud for this sheet (its own border, its own
   ground) because it stands between a click and a child's whole collection. */
.restore{
  display: grid; gap: .6rem;
  padding: 1rem 1.1rem;
  border: 2px solid var(--mint-deep);
  border-radius: var(--r-md);
  background: var(--paper);
}
.restore h3{ font-size: 1.05rem; }
.restore-what{ display: grid; gap: .25rem; margin: 0; padding-left: 1.1rem; font-size: .92rem; }
.restore-what li{ list-style: disc; }

.setrow{ display: flex; flex-wrap: wrap; gap: 1rem; }
.setrow label{ display: flex; align-items: center; gap: .45rem; font-weight: 700; min-height: 44px; }
.setrow input[type=checkbox],
.setrow input[type=radio]{ width: 22px; height: 22px; accent-color: var(--mint-deep); }
.setrow.modes{ gap: .4rem 1.3rem; }

/* The control the chosen mode does not use. Dimmed and disabled rather than
   removed, so a parent weighing the two options can still see what the other one
   is set to, and so the block does not change height under the finger. */
.fld.is-off{ opacity: .45; }

/* THE ONE LINE THAT SAYS WHAT WILL ACTUALLY HAPPEN. Set apart from the hints
   around it because it is not advice: it is this profile's settings read back as
   a sentence, and it changes as they are moved. */
.says{
  background: var(--paper);
  border-left: 4px solid var(--mint-deep);
  border-radius: var(--r-sm);
  padding: .7rem .85rem;
  font-size: .95rem; line-height: 1.5;
}

.ptable{ display: grid; gap: .3rem; }
.prow{
  display: grid;
  grid-template-columns: 2.2rem minmax(70px,1fr) 5rem 3.2rem 4.4rem 6.4rem minmax(90px,1.2fr);
  gap: .5rem; align-items: center;
  font-size: .85rem;
  padding: .3rem 0;
  border-top: 1px solid rgba(74,59,78,.07);
}
.prow-key{
  display: grid; place-items: center;
  width: 2rem; height: 2rem; border-radius: 30%;
  font-family: var(--display); font-weight: 800;
  background: rgba(74,59,78,.07);
}
.prow-key[data-stage="strong"]{ background: var(--mint); color: #22513F; }
.prow-key[data-stage="active"]{ background: var(--sun); color: #6A4A0E; }
.prow-who{ font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prow-bar{ height: 8px; border-radius: 4px; background: rgba(74,59,78,.1); overflow: hidden; }
.prow-bar i{ display: block; height: 100%; background: var(--mint-deep); }
.prow-num, .prow-pics{ color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.prow-note{ color: var(--ink-faint); }
/* Where this letter sits on the withdrawal ladder. Worth its own column rather
   than a sentence, because it is the one thing on this table a parent can watch
   change and the one that explains why a turn looked easier last week. */
.prow-show{
  justify-self: start;
  padding: .1rem .45rem;
  border-radius: 999px;
  font-size: .74rem; font-weight: 700;
  background: rgba(74,59,78,.07); color: var(--ink-soft);
}
.prow-show[data-tier="1"]{ background: rgba(255,217,138,.5); color: #6A4A0E; }
.prow-show[data-tier="2"]{ background: rgba(166,227,200,.6); color: #22513F; }

/* ------------------------------------------------------------ 12 responsive */
@media (max-width: 720px){
  /* Content-sized rows pushed to the two ends: the question at the top where the
     eye starts and the map at the bottom where a thumb already is. Stretched
     rows left the map floating in the middle of a phone with a couple of hundred
     pixels of nothing under it, which is the one part of this screen a child on
     a touch device has to be able to reach. */
  .stage{ grid-template-rows: auto auto auto; align-content: space-between; }
  .letter-plate{ width: min(24vh, 42vw); }
  .pic-wrap{ width: min(21vh, 38vw); }
  .again{ width: 52px; height: 52px; }
  .gohome{ width: 46px; height: 46px; }
  .gohome svg{ width: 21px; height: 21px; }
  .map{ aspect-ratio: 10 / 4.2; }
  .mkey-letter{ font-size: .5rem; }
  .prow{ grid-template-columns: 2rem 1fr 3.4rem 6.4rem; }
  .prow-bar, .prow-pics, .prow-note{ display: none; }
}

/* A short landscape phone is the worst case: the map and the picture both have
   to fit above the fold or the child is scrolling mid-turn. */
@media (max-height: 560px) and (orientation: landscape){
  .stage{
    grid-template-columns: 1fr 1.15fr;
    grid-template-rows: minmax(0, 1fr) auto;
    align-items: center;
  }
  /* The card and its help button stack in the left column; the map takes the
     right one whole. Placed explicitly, because a third grid item would
     otherwise wrap under the card and push the map off the bottom. */
  .stage-card{ grid-column: 1; grid-row: 1; }
  .helpbar{ grid-column: 1; grid-row: 2; }
  .map-wrap{ grid-column: 2; grid-row: 1 / span 2; }
  .helpme-pic{ width: 40px; height: 40px; }
  .letter-plate{ width: min(46vh, 22vw); }
  .pic-wrap{ width: min(40vh, 19vw); }
  .prompt{ font-size: clamp(1.1rem, 3vw, 1.6rem); }
}

/* -------------------------------------------------------- 13 reduced motion */
/* Everything decorative stops. What does NOT stop is the change of state: a
   child who gets no reaction at all concludes they were wrong, so the target
   still lifts, the right answer still rings green and the wrong one still rings
   mauve. Motion is removed; feedback is not. */
@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .pic{ animation: none; }
  .fx-shine{ display: none; }
  .lp-ask{ animation: none; }
  /* THE LETTER LEAVING IS INFORMATION, not decoration. A child who does not see
     it go is left unsure whether it was ever there, so it still fades out; what
     stops is the movement, which is the part this media query is about. */
  .lp-glyph{ transition-duration: .22s !important; }
  .letter-plate[data-show="off"] .lp-glyph{ transform: none; }
  /* THE ANSWER STILL ARRIVES, it just arrives without the bounce. Same reasoning
     as the tiles below: a child who gets no reaction at all concludes they were
     wrong, and the ring and the picture are the reaction. What goes is the
     movement, which is all this query was ever about. */
  .letter-plate.is-won{ animation: none; }
  .letter-plate.is-won .lp-duet{ animation: none; }
  .mkey.is-target .mkey-face{
    animation: none;
    transform: scale(1.07);
    box-shadow: 0 0 0 5px var(--sun-deep), 0 8px 22px rgba(233,174,60,.45);
  }
  .map-arc{ animation: none; stroke-dasharray: none; }
  /* The word tiles still ARRIVE, they just arrive at once. The letter being asked
     for still lifts, because that lift is the attention shift and not decoration:
     removing it would leave the child with no signal at all about which of four
     letters the game is waiting for. */
  .fx-word b{ animation: none; }
  .slot.is-now{ animation: none; transform: translateY(-6px) scale(1.06); }
  /* The route still appears, it just does not travel. Without this the dash
     pattern is left mid-draw and half the segment is missing. */
  .map-walk.is-new{ animation: none; stroke-dasharray: none; }
}

/* The family name plate: what a family anchor looks like before a parent has
   added a photo. It stands where a sticker would, because on those turns it is
   the friend, and it is the one picture in the game that belongs to the child
   rather than to us.
   IT CARRIES THE NAME AND NOT THE INITIAL. The initial used to be printed here
   at nine rem, which is exactly the letter the later rungs are deliberately not
   showing: the child could read the answer off Mama's plate on the one turn
   that was meant to be asking them to remember it. The letter now lives in one
   place on this card, its own plate, which is the place the withdrawal ladder
   controls. */
.plate{
  display: grid; place-items: center; gap: .2rem;
  width: 100%; height: 100%;
  padding: .6rem;
  border-radius: var(--r-xl);
  background: linear-gradient(180deg, #FFF1F6, var(--pink));
  box-shadow: var(--shadow-2);
}
.plate[hidden]{ display: none; }
.plate span{
  font-family: var(--display); font-weight: 800;
  font-size: clamp(1.4rem, 6vh, 2.8rem);
  line-height: 1.1;
  text-align: center;
  overflow-wrap: anywhere;
  color: #8E3A55;
}

/* The same letter in five typefaces, shown when a letter is first met. Small,
   quiet, and under the prompt: it is a thing to notice, not the question. */
.faces{
  display: flex; gap: clamp(.5rem, 2vw, 1.1rem);
  align-items: baseline; justify-content: center; flex-wrap: wrap;
  font-size: clamp(1.1rem, 3.4vw, 1.7rem);
  color: var(--ink-soft);
  opacity: .85;
}
.faces span{ line-height: 1; }

/* ============================================================================
   41  WHO IS PLAYING: several children in one household
   ============================================================================
   One game each, one file each (js/letters/kids.js). Two surfaces: a chip on
   the home screen saying who is playing, and a picker of faces behind it. Both
   are drawn from the album's own palette, because a child choosing themselves
   is choosing a sticker like every other choice in this game.

   The chip is ABSENT with one child rather than disabled, the same rule the
   new-friend door lives by: a control that cannot do anything is a control that
   teaches a child to press things. */

.whois{
  display: inline-flex; align-items: center; gap: .55rem;
  margin: 0 auto .4rem;
  padding: .3rem .8rem .3rem .3rem;
  border: 0; border-radius: 999px;
  background: var(--card);
  box-shadow: var(--shadow-1);
  font: inherit; color: var(--ink); cursor: pointer;
}
.whois[hidden]{ display: none; }
.whois-face{
  width: 2.1rem; height: 2.1rem; border-radius: 50%;
  background: var(--paper-2) center/cover no-repeat;
  box-shadow: inset 0 0 0 2px var(--card);
}
.whois-name{ font-weight: 800; }
.whois-swap{ color: var(--ink-faint); font-size: .82em; }
.whois[data-colour="pink"]{ background: var(--pink); }
.whois[data-colour="lilac"]{ background: var(--lilac); }
.whois[data-colour="mint"]{ background: var(--mint); }
.whois[data-colour="sun"]{ background: var(--sun); }
.whois[data-colour="sky"]{ background: var(--sky); }
.whois[data-colour="peach"]{ background: var(--peach); }

/* The picker. Faces rather than a list of names, because the audience cannot
   read a list, and large enough to be hit by a four year old on a tablet. */
.kids{
  /* Centred in the viewport rather than in the section: a screen element here
     is not stretched, so a percentage height has nothing to resolve against and
     the cards sit against the top with the page empty underneath them. */
  min-height: 76vh;
  display: grid; align-content: center; justify-items: center; gap: 1.4rem;
  padding: 2rem 1rem;
}
.kids-title{ font-family: var(--display); font-size: clamp(1.6rem, 5vw, 2.6rem); margin: 0; }
.kids-row{ display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; }
.kidcard{
  width: clamp(120px, 26vw, 190px);
  display: grid; justify-items: center; gap: .5rem;
  padding: 1rem .8rem;
  border: 0; border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--shadow-2);
  font: inherit; color: var(--ink); cursor: pointer;
  transition: transform .18s cubic-bezier(.2,.9,.3,1.4);
}
.kidcard:hover{ transform: translateY(-4px); }
.kidcard-face{
  width: clamp(72px, 16vw, 118px); aspect-ratio: 1; border-radius: 50%;
  background: var(--paper-2) center/cover no-repeat;
}
.kidcard-name{ font-family: var(--display); font-weight: 800; font-size: 1.1rem; }
.kidcard.is-active{ box-shadow: 0 0 0 4px var(--yes), var(--shadow-2); }
.kidcard[data-colour="pink"]{ background: var(--pink); }
.kidcard[data-colour="lilac"]{ background: var(--lilac); }
.kidcard[data-colour="mint"]{ background: var(--mint); }
.kidcard[data-colour="sun"]{ background: var(--sun); }
.kidcard[data-colour="sky"]{ background: var(--sky); }
.kidcard[data-colour="peach"]{ background: var(--peach); }
.kids-back{
  border: 0; border-radius: 999px; padding: .6rem 1.4rem;
  background: var(--card); box-shadow: var(--shadow-1);
  font: inherit; font-weight: 700; color: var(--ink-soft); cursor: pointer;
}

/* The grown-up side: a row per child, with the two things an adult can do. */
.kid-h{ margin: 1.1rem 0 .2rem; font-size: 1rem; }
.kid-list{ display: grid; gap: .5rem; margin: .5rem 0; }
.kid-row{
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .45rem .6rem;
  border-radius: var(--r-md);
  background: var(--paper-2);
}
.kid-row.is-active{ box-shadow: inset 0 0 0 2px var(--mint-deep); }
.kid-face{
  width: 2.2rem; height: 2.2rem; border-radius: 50%;
  background: var(--card) center/cover no-repeat;
}
.kid-name{ font-weight: 700; flex: 1 1 6rem; }
.kid-tag{ color: var(--mint-deep); font-weight: 700; font-size: .85em; }
.btn.small{ padding: .3rem .7rem; font-size: .85em; }
.btn.danger{ color: var(--pink-deep); }
