/* Magic Story skin for Story Together.
 *
 * Imported from the Claude Design project "Magic Story Prototype" (fa8d824a);
 * the 17-screen reference lives at /demos/magic-story.
 *
 * This file re-skins the page by REDEFINING ITS TOKENS, which is the seam
 * story-together.html's own stylesheet documents: "Only the tokens are
 * redefined. Every rule below consumes them, so the whole redesign follows
 * from this block." Loading this after that stylesheet therefore moves the
 * whole page to the new palette without touching a single existing rule -
 * and story-css-pins.test.mjs / story-host-closing.test.mjs, which grep the
 * inline blocks, keep passing because nothing inline moved.
 *
 * The component rules below only cover the places where the design does
 * something the tokens cannot express on their own: the paw-print ground,
 * the condensed display type, the cyan pill (which needs DARK ink, not the
 * white the warm-orange accent used), the pup's glow pedestal, the
 * narrator halo at the bottom of the frame, the page shimmer and the
 * karaoke words.
 */

/* ── Tokens ───────────────────────────────────────────────────────────────
 * Dark is the design's only mode, so both the default and the explicit
 * data-theme="dark" land on the same values. "Bright" is left to the page's
 * own light tokens: a grown-up who picks it still gets the readable cream
 * storybook rather than a half-applied navy.                              */
:root[data-theme="dark"], body.ms-skin{
  --m-app:#16123A;              /* deep indigo ground */
  --m-card:#241F52;             /* the book / card face */
  --m-sunken:#1B1745;
  --m-ink:#FFFFFF;
  --m-ink-muted:#C9C6E8;
  --m-ink-faint:#8F8ABF;
  --m-eyebrow:#9B93D6;
  --m-hairline:#3B3480;
  --m-hairline-soft:#2C2663;
  --m-accent:#A6F3FF;           /* cyan - the one saturated thing on screen */
  --m-accent-shadow:#4FB6CC;
  --m-highlight:#2A2560;        /* the question banner, as an indigo wash */
  --m-highlight-ink:#FFF3A8;    /* the karaoke warm yellow */
  --m-font:"Quicksand","Plus Jakarta Sans",-apple-system,system-ui,sans-serif;

  /* Skin-only additions. */
  --ms-violet:#C8A8FF;
  --ms-on-accent:#0B1020;       /* cyan is light: its label must be dark ink */
  --ms-display:"Saira Condensed","Arial Narrow",sans-serif;
  --ms-story:"Mali",-apple-system,"Segoe UI",sans-serif;
  --ms-glow-lit:rgba(255,217,66,.65);
  /* The story text page's face. A token because the turning sheet paints a
   * PICTURE of this page (its front face) and the two must never drift apart
   * - the cascade enforces it instead of a comment. */
  --ms-page-face:linear-gradient(160deg, #221D4E 0%, #1B1745 100%);

  /* The ground, declared once and worn by the app and by the style step:
   * the design's OWN icon-field bitmap (owner-supplied from the Figma,
   * 2026-08-13; 2000x1116, ~52KB webp), under a violet bloom. A hand-drawn
   * SVG imitation lived here first and read as blurry smudges next to the
   * real thing - the asset IS the design, so the asset ships. */
  --ms-glow:radial-gradient(120% 80% at 50% 0%, rgba(103,88,200,.34) 0%, transparent 62%);
  --ms-paw:url("/demos/assets/magic-story/paw-ground.webp");
}

/* The legacy two-pane .book shell consumes its own vars, so it needs the
 * same move or the spread would stay cream inside a navy app. */
body.ms-skin{ --paper:#16123A; --left:#1B1745; --ink:#FFFFFF;
              --accent:#A6F3FF; --bubble:#2A2560; }

/* ── The paw-print ground ─────────────────────────────────────────────────
 * The design's navy is patterned, not flat. Both layers are tokens (above),
 * so the app and the style step wear one ground rather than two copies.   */
body.ms-skin{
  font-family:var(--m-font);
  background-color:var(--m-app);
  background-image:var(--ms-glow), var(--ms-paw);
  background-attachment:fixed, fixed;
  /* cover, not a tile: the asset is one designed field, not a seamless
   * repeat - tiling it would print its edges as visible seams. Fixed
   * attachment pins it to the viewport, so cover always fills. */
  background-size:auto, cover;
}
body.ms-skin #setup-screen,
body.ms-skin #keepsake-screen{ background:transparent; }

/* ── Display type ─────────────────────────────────────────────────────────
 * The design sets every title in a tight condensed uppercase; the cover
 * titles and story body stay in the rounded storybook faces.              */
body.ms-skin h1,
body.ms-skin .setup-head h2,
body.ms-skin #keepsake-screen .eyebrow{
  font-family:var(--ms-display);
  font-weight:800;
  text-transform:uppercase;
  letter-spacing:.02em;
  line-height:.95;
  color:var(--m-ink);
}
body.ms-skin .setup-head h2{ font-size:clamp(30px,7vw,52px); margin:.1em 0 .18em; }
body.ms-skin .setup-head .lede{ color:var(--m-ink-muted); font-size:15.5px; }
body.ms-skin #keepsake-screen .eyebrow{ color:var(--m-accent); letter-spacing:.16em; font-size:13px; }
body.ms-skin .cover-title{ font-family:var(--ms-display); text-transform:uppercase;
  font-weight:800; color:var(--m-accent); letter-spacing:.01em; }

/* ── The book card ────────────────────────────────────────────────────────
 * Everything in the design that holds content is a portrait book: a soft
 * indigo face, a hairline edge and an inset spine line down the left.      */
body.ms-skin #setup-screen > .setup-head,
body.ms-skin #keepsake-screen .book-cover,
body.ms-skin .cover-tile{
  border-radius:26px;
  background:linear-gradient(158deg, #2C2662 0%, #221D4E 55%, #1C1843 100%);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16),
             inset 14px 0 0 -12px rgba(255,255,255,.22),
             0 26px 60px rgba(0,0,0,.5);
}

/* ── The cyan pill ───────────────────────────────────────────────────────
 * The accent token carries the fill, but the page's own rules put WHITE on
 * the old warm orange. On cyan that is ~1.3:1, so the label is pinned to
 * dark ink here rather than left to inherit.                              */
body.ms-skin #intro-continue,
body.ms-skin #keepsake-again,
body.ms-skin #turn-retry,
body.ms-skin #style-start,
body.ms-skin .ms-cta{
  background:var(--m-accent);
  color:var(--ms-on-accent);
  border:0;
  border-radius:999px;
  font-family:var(--m-font);
  font-weight:700;
  letter-spacing:.01em;
  box-shadow:0 10px 26px rgba(133,244,250,.28), 0 2px 0 var(--m-accent-shadow);
}
body.ms-skin #intro-continue:hover,
body.ms-skin .ms-cta:hover{ filter:brightness(1.06); }
/* The two Send buttons keep the page's own 3D pill - a solid offset shadow that
 * DEEPENS on hover and sinks on press (#607). The flat glow above would have
 * been an equal-specificity, later-loading override of the idle state only, so
 * the button would have sat at a 10px glow and "lifted" to a 7px shadow: the
 * hover read as a press. Only the colour moves here. */
body.ms-skin #send:not(.sending),
body.ms-skin #host-send:not(.sending){
  background:var(--m-accent);
  color:var(--ms-on-accent);
  border:0;
  border-radius:999px;
  font-family:var(--m-font);
  font-weight:700;
}
body.ms-skin #keepsake-shelf,
body.ms-skin .ms-ghost{
  background:transparent; color:var(--m-ink);
  border-radius:999px; box-shadow:inset 0 0 0 1.5px var(--m-hairline);
}

/* ── The pup on a glow pedestal ───────────────────────────────────────────
 * In every design screen the pup stands on an elliptical cyan/violet pool
 * of light. The page already has .narrator-glow behind the 3D mount, so
 * this only restates it in the design's colours.                          */
body.ms-skin .narrator-glow{
  background:radial-gradient(closest-side,
    rgba(166,243,255,.42) 0%, rgba(200,168,255,.24) 55%, transparent 76%);
  filter:blur(2px);
}
body.ms-skin .narrator-ph{
  background:var(--m-sunken);
  border-color:var(--m-hairline);
  color:var(--m-ink-muted);
}

/* ── The narrator halo: a spotlight rises from the bottom of the frame ────
 * (Owner spec 2026-08-12; Gemini Live's bottom-of-frame glow is the
 * reference.) Replaces the `.listening::after` glow that shipped here dead:
 * nothing ever toggled `listening`/`ms-listening` on the body, so the
 * "signature cue" never once painted. The halo is a real element
 * (#ms-halo, mounted by story-halo.js), which is also what makes it
 * drivable: --ms-halo-v is the smoothed output loudness (sink.level()
 * via window.spinAudioLevel), written at rAF rate while the voice plays.
 *
 * The hotspot sits at bottom-centre on every screen: that is under the pup
 * on setup (he is centred there) and around the voice in play. --ms-halo-x
 * is not speculative — it is the ONE value both gradients and the
 * transform-origin share (four usages), so the hotspot cannot half-move.  */
body.ms-skin #ms-halo{
  --ms-halo-x:50%;
  --ms-halo-v:0;
  /* Shorter than it was (42vh/400px): at that size the glow washed halfway up
   * the panel and stopped reading as a light at the bottom edge at all
   * (owner, 2026-08-13). It has to end under the pup, not behind him. */
  position:fixed; left:0; right:0; bottom:0; height:min(26vh,240px);
  z-index:3; pointer-events:none; mix-blend-mode:screen;
  opacity:0; transition:opacity .5s ease;
  transform-origin:var(--ms-halo-x) 100%;
}
/* Quiet: smaller, dimmed, just breathing. White centre to #89FFE7 edge. */
body.ms-skin #ms-halo[data-mode="quiet"]{
  opacity:.55;
  background:radial-gradient(36% 50% at var(--ms-halo-x) 100%,
    rgba(255,255,255,.72) 0%, rgba(137,255,231,.32) 44%, rgba(137,255,231,0) 72%);
  animation:msHaloBreathe 3.2s ease-in-out infinite;
}
/* Voice: bigger, almost the whole bottom, and it moves with the voice.
 * #C8A8FF centre to #89FFE7 edge; --ms-halo-v swells size and brightness.
 * The pulse keeps it alive even at level 0 (the child's voice never reaches
 * the sink, so while SHE talks the meter reads nothing). */
body.ms-skin #ms-halo[data-mode="voice"]{
  opacity:1;
  /* The horizontal radius is 50% of the box and the last stop is 100% OF that
   * radius, so the glow reaches zero exactly at the left and right edges. It
   * used to be 74% with its last stop at 84% — zero fell 24% of the width
   * OUTSIDE the box, so the element's own edge sliced the glow and the bottom
   * corners read as cut off on a phone (owner, 2026-08-13). */
  background:radial-gradient(50% 100% at var(--ms-halo-x) 100%,
    rgba(200,168,255,.82) 0%, rgba(200,168,255,.36) 40%,
    rgba(137,255,231,.28) 74%, rgba(137,255,231,0) 100%);
  /* scaleY, not scale: a uniform scale shrank the BOX to 80% of the viewport
   * (312 of 390px, measured), which put two hard vertical edges in the glow —
   * the same cut, from the other direction. The voice moves the halo's HEIGHT;
   * its width is the frame's, always. */
  transform:scaleY(calc(.8 + .3 * var(--ms-halo-v,0)));
  filter:brightness(calc(.85 + .5 * var(--ms-halo-v,0)));
  animation:msHaloPulse 2.6s ease-in-out infinite;
}
@keyframes msHaloBreathe{
  0%,100%{ transform:scaleY(.86); opacity:.45; }
  50%    { transform:scaleY(1);   opacity:.62; }
}
@keyframes msHaloPulse{ 0%,100%{ opacity:.9; } 50%{ opacity:1; } }
body.ms-skin #mic-hint{ color:var(--m-ink-muted); }
/* The halo IS the voice affordance now (owner, 2026-08-12): the big cyan orb
 * goes. Display:none rather than deletion, deliberately — orbState() writes
 * the mic state machine onto #orb's classList and the halo reads its modes
 * from exactly there, so the element is the wire, not the lamp. (The orb's
 * old gradient/shadow rule left with it — an invisible element needs no
 * paint.) */
body.ms-skin #orb{ display:none; }

/* ── Making the next page ─────────────────────────────────────────────────
 * There is deliberately NO #img-skeleton rule here any more. This skin used
 * to restate the cream skeleton's sweep in violet/cyan, which was fine while
 * the page's own #img-skeleton also drew a sweep and this one merely won.
 * The page now paints the magic brush there instead and declares no
 * background of its own — so a sweep left here has nothing to override and
 * simply animates underneath the brush, which is the old shimmer still
 * running, recoloured. `ms-skin` is unconditional on <body>, so that would
 * be every drawing state, on every page. story-skin-pins.test.mjs pins its
 * absence (macs-review, round 2).                                          */

/* ── The spread ───────────────────────────────────────────────────────────*/
body.ms-skin .book{ box-shadow:0 34px 90px rgba(0,0,0,.55); border-radius:22px; }
body.ms-skin .left-page{
  background:var(--ms-page-face);
  color:var(--m-ink);
}
body.ms-skin .right-page{ background:#12102E; }
body.ms-skin #page-meta{ background:rgba(27,23,69,.86); color:var(--m-ink-muted); }
body.ms-skin #question-bubble{
  background:var(--m-highlight); color:var(--m-highlight-ink);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.1);
}

/* ── Story text + karaoke ─────────────────────────────────────────────────
 * The design narrates in Mali and lights each word as it is spoken. The
 * .ms-lit / .ms-dim classes are the contract for that: whatever drives the
 * narration adds .ms-lit to words already read and .ms-dim to the text of a
 * page that has loaded but not yet been spoken (design screen 14).         */
body.ms-skin #page-text{
  font-family:var(--ms-story);
  font-weight:500;
  line-height:1.34;
  letter-spacing:-.02em;
  color:#FFFFFF;
  text-shadow:0 2px 18px rgba(0,0,0,.55);
}
body.ms-skin #page-text .ms-lit{
  color:var(--m-highlight-ink);
  filter:drop-shadow(0 0 12px var(--ms-glow-lit));
  transition:color .18s linear, filter .18s linear;
}
body.ms-skin #page-text.ms-dim{ color:rgba(255,255,255,.42); text-shadow:none; }

/* ── Choosing the picture style ───────────────────────────────────────────*/
body.ms-skin #style-picker{ background:transparent; }
body.ms-skin #style-grid > *{
  border-radius:18px;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.14), 0 12px 28px rgba(0,0,0,.4);
}
body.ms-skin #style-grid > [aria-pressed="true"],
body.ms-skin #style-grid > .selected{
  box-shadow:inset 0 0 0 2.5px var(--m-accent), 0 14px 34px rgba(133,244,250,.3);
}

/* ── Keepsake / gallery ───────────────────────────────────────────────────*/
body.ms-skin #keepsake-screen .cover-meta{ color:var(--m-ink-faint); }

@media (prefers-reduced-motion: reduce){
  /* The MODE selectors, not the bare element: the mode rules above are
   * (1,2,1) and a bare `body.ms-skin #ms-halo` is (1,1,1), which LOSES on
   * specificity regardless of the media query — the halo kept animating for
   * exactly the users who asked it not to (macs-review, round 1). */
  /* #img-skeleton is gone from this list with its sweep: the brush that
   * replaced it is the page's own, and the page parks it (rig, spin, sparks
   * and washes) in its own reduced-motion block. */
  body.ms-skin #ms-halo[data-mode="quiet"],
  body.ms-skin #ms-halo[data-mode="voice"]{ animation:none; }
  /* the voice level would still scale it every frame — hold it steady too */
  body.ms-skin #ms-halo[data-mode="voice"]{ transform:none; filter:none; }
}

/* ── The front of the flow (sandbox/story-front.js) ───────────────────────
 * Onboarding 1-2 and the gallery home. One overlay above the page: the game's
 * setup screen is already underneath, so dismissing this reveals it.        */
.ms-front{
  position:fixed; inset:0; z-index:60;
  background-color:var(--m-app,#16123A);
  background-image:inherit;
  background-size:inherit;   /* the body's cover-scaled ground, not natural size */
  transition:opacity .3s ease;
  font-family:var(--m-font,system-ui);
}
.ms-front.ms-gone{ opacity:0; pointer-events:none; }
.ms-deck{ position:absolute; inset:0; }
.ms-deck > section{
  position:absolute; inset:0; opacity:0; pointer-events:none;
  transition:opacity .42s ease;
  display:flex; flex-direction:column;
}
.ms-deck > section.ms-on{ opacity:1; pointer-events:auto; }

/* Onboarding: full-bleed photo, the copy sitting in its shade. */
.ms-slide{ justify-content:flex-end; overflow:hidden; }
.ms-slide::before{
  content:""; position:absolute; inset:0;
  background-image:var(--ms-slide-art);
  background-size:cover; background-position:52% 30%;
}
.ms-slide-shade{
  position:absolute; left:0; right:0; bottom:0; height:78%;
  background:linear-gradient(180deg, rgba(8,6,26,0) 0%, rgba(8,6,26,.82) 58%, #08061A 100%);
}
.ms-slide-body{
  position:relative; padding:0 26px 44px; text-align:center;
  display:flex; flex-direction:column; align-items:center; gap:12px;
}
/* Two lines, as the design sets it - one long line shrinks the type to fit and
 * loses the "BUILT FOR KIDS. / CONTROLLED BY PARENTS." beat. balance keeps the
 * two halves even instead of orphaning a word. */
.ms-slide-title{
  margin:0; max-width:22ch; text-wrap:balance;
  font-family:var(--ms-display,sans-serif); font-weight:800; text-transform:uppercase;
  font-size:clamp(30px,7vw,54px); line-height:.96; letter-spacing:.01em; color:#fff;
  text-shadow:0 4px 26px rgba(0,0,0,.6);
}
/* The safety headline is two-tone in the design (screen 2): the first
 * sentence cyan, the second white. story-front.js splits the sentences into
 * spans; this is the tone the first one wears. */
.ms-slide-title .ms-title-accent{ color:var(--m-accent,#A6F3FF); display:block; }
.ms-slide-sub{ margin:0; color:rgba(255,255,255,.8); font-size:15px; max-width:24em; }
.ms-checks{ display:flex; gap:18px; flex-wrap:wrap; justify-content:center;
  color:rgba(255,255,255,.86); font-size:13.5px; font-weight:600; }
.ms-badge{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  width:112px; height:124px; margin-bottom:6px; color:#0B1020;
  font-family:var(--ms-display,sans-serif); font-weight:800; text-transform:uppercase;
  line-height:.94; font-size:22px; letter-spacing:.02em;
  background:linear-gradient(165deg,#BFF6FF,#7FD9F0 62%,#4FB6CC);
  /* a shield, not a rectangle */
  clip-path:polygon(50% 0,100% 14%,100% 62%,50% 100%,0 62%,0 14%);
  box-shadow:0 14px 34px rgba(133,244,250,.34);
}
.ms-slide-cta{ margin-top:10px; padding:15px 32px; font-size:16px; cursor:pointer; }

/* The gallery home. */
.ms-gallery{ justify-content:center; gap:22px; padding:32px 0 120px; }
.ms-gallery-head{ text-align:center; }
.ms-eyebrow{
  font-family:var(--ms-display,sans-serif); text-transform:uppercase;
  letter-spacing:.16em; font-size:13px; color:var(--m-accent,#A6F3FF);
}
/* One book is the hero of this screen (design screen 5), so the shelf centres
 * and the cover is sized off the viewport instead of a fixed 200px. `safe`
 * matters: a plain `center` on a scrolling row pushes the first book past the
 * scroll origin once the shelf overflows, and it can never be reached again.
 * --ms-book-w drives every part of the cover; at its 200px floor the numbers
 * below reproduce the original size exactly, so this is a pure scale-up. */
.ms-shelf{
  --ms-book-w:clamp(200px, min(38vh, 62vw), 380px);
  display:flex; gap:20px; align-items:center; justify-content:safe center;
  padding:14px 26px; overflow-x:auto; scrollbar-width:none;
  scroll-snap-type:x proximity;
}
.ms-shelf::-webkit-scrollbar{ display:none; }
.ms-shelf{ perspective:1500px; perspective-origin:50% 45%; }
.ms-book{
  position:relative; flex:0 0 auto; scroll-snap-align:center;
  width:var(--ms-book-w,200px); height:calc(var(--ms-book-w,200px) * 1.36);
  border:0; cursor:pointer;
  border-radius:4px 16px 16px 4px; color:#fff;
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px;
  padding:18px 18px 18px 26px; text-align:center;
  background-image:var(--ms-book-art,none);
  background-size:cover; background-position:center;
  background-color:#241F52;
  transform-style:preserve-3d;
  transform:rotateY(-15deg) rotateZ(-1.6deg);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.14),
             28px 26px 54px rgba(0,0,0,.5);
  transition:transform .28s cubic-bezier(.22,.7,.3,1), box-shadow .28s ease;
}
/* the page block, standing slightly proud of the cover on the fore edge */
.ms-book::after{
  content:""; position:absolute; right:-5px; top:7px; bottom:7px; width:7px;
  border-radius:0 3px 3px 0;
  background:repeating-linear-gradient(to right,
    #F6F2E8 0 2px, #D9D2C4 2px 3px);
  transform:translateZ(-3px);
  box-shadow:1px 0 3px rgba(0,0,0,.3);
}
/* the spine, darker where the cover wraps */
.ms-book::before{
  content:""; position:absolute; left:0; top:0; bottom:0; width:13px;
  border-radius:4px 0 0 4px;
  background:linear-gradient(90deg, rgba(0,0,0,.34), rgba(255,255,255,.16) 62%, rgba(0,0,0,.12));
}
.ms-book:hover{ transform:rotateY(-8deg) rotateZ(-.6deg) translateY(-8px) scale(1.03); }
/* Saved stories alternate the design's cover palette when they have no art. */
.ms-shelf .ms-book:not(.ms-book-new):nth-of-type(even){
  background-image:var(--ms-book-art, linear-gradient(152deg,#CFEBA6,#F1E6A0));
}
.ms-shelf .ms-book:not(.ms-book-new):nth-of-type(odd){
  background-image:var(--ms-book-art, linear-gradient(152deg,#F3A8C3,#F3B78E));
}
.ms-book-title{
  font-family:var(--ms-display,sans-serif); font-weight:800; text-transform:uppercase;
  font-size:calc(var(--ms-book-w,200px) * .095); line-height:1; letter-spacing:.02em;
}
/* "Create new story" is a NOTEBOOK, not a finished picture book: a pale cover
 * among the coloured ones, so an empty shelf reads as "here is a blank book to
 * fill" rather than as another story you could open. The shelf holds nothing
 * else until a story is actually saved (shelfList() in story-front.js reads
 * localStorage and nothing seeds it). */
.ms-book-new{
  background-image:linear-gradient(152deg,#C3E6F7 0%,#C6C2F2 46%,#E2C6EF 100%);
  background-color:transparent;
}
/* The Figma leaves this book blank and lets the sparkle carry the meaning. On a
 * real phone that is a blank pastel rectangle with a small mark on it: nothing
 * tells the grown-up it is the thing to tap. It says so (owner, 2026-08-12).
 * Dark ink, because this is the one cover in the shelf that is light - with a
 * highlight travelling across the words, which is the invitation to tap it. */
.ms-book-new .ms-book-title{
  font-size:calc(var(--ms-book-w,200px) * .085); max-width:11ch;
  color:#2A2158;
  background-image:linear-gradient(100deg,
    #2A2158 0%, #2A2158 34%, #6E58D8 46%, #B49BFF 52%, #6E58D8 58%, #2A2158 70%, #2A2158 100%);
  background-size:300% 100%;
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
  animation:msInvite 3.4s cubic-bezier(.5,0,.5,1) infinite;
}
@keyframes msInvite{
  0%       { background-position:118% 0; }
  55%,100% { background-position:-38% 0; }
}
/* The sparkle breathes with it, so the whole cover reads as one invitation. */
.ms-book-new .ms-sparkle{ animation:msInviteSpark 3.4s ease-in-out infinite; }
@keyframes msInviteSpark{
  0%,100% { transform:scale(1);    opacity:.72; }
  52%     { transform:scale(1.14); opacity:1; }
}
@media (prefers-reduced-motion:reduce){
  .ms-book-new .ms-book-title{ animation:none; -webkit-text-fill-color:#2A2158; }
  .ms-book-new .ms-sparkle{ animation:none; }
}
.ms-book-new .ms-sparkle{ color:#3B2E6B; opacity:.72; }
.ms-book-new .ms-sparkle svg{ filter:drop-shadow(0 2px 6px rgba(255,255,255,.6)); }
.ms-sparkle{ font-size:30px; color:var(--m-accent,#A6F3FF);
  filter:drop-shadow(0 0 14px rgba(133,244,250,.7)); }
/* the svg carries width/height="30" attributes; override so it grows with the cover */
.ms-book .ms-sparkle svg{ width:calc(var(--ms-book-w,200px) * .15); height:auto; }

.ms-nav{
  position:absolute; left:50%; bottom:34px; transform:translateX(-50%);
  display:flex; align-items:center; gap:26px;
}
.ms-nav-btn{
  width:46px; height:46px; border:0; border-radius:50%; cursor:pointer;
  background:transparent; color:rgba(255,255,255,.62);
  display:flex; align-items:center; justify-content:center;
  transition:color .18s ease, background .18s ease;
}
.ms-nav-btn svg{ width:24px; height:24px; display:block; }
.ms-nav-btn:hover{ color:#fff; background:rgba(255,255,255,.08); }
/* The signature "AI creation" button: the one filled, raised control in the bar. */
.ms-nav-new{
  width:58px; height:58px; color:#0B1020;
  background:var(--m-accent,#A6F3FF);
  box-shadow:0 12px 30px rgba(133,244,250,.42), inset 0 1px 0 rgba(255,255,255,.5);
}
.ms-nav-new svg{ width:27px; height:27px; }
.ms-nav-new:hover{ color:#0B1020; background:#BFF7FF; }

/* ── Specificity catch-ups ────────────────────────────────────────────────
 * A few of the page's own rules are two-id (#setup-screen #intro-continue)
 * or carry a hard-coded warm hex from the orange palette. Those beat, or
 * survive, a token swap, so they are answered here at matching specificity
 * rather than by editing the inline block.
 *
 * The CTA label is the important one: the old accent was a dark orange that
 * white sat on happily. Cyan is a LIGHT fill - white on it is ~1.3:1, which
 * is why the label is forced to dark ink (~14:1) here.                     */
body.ms-skin #setup-screen #intro-continue{
  background:var(--m-accent);
  color:var(--ms-on-accent);
  border-radius:999px;
  box-shadow:0 6px 0 var(--m-accent-shadow), 0 12px 30px rgba(133,244,250,.26);
}
body.ms-skin #setup-screen #intro-continue:hover{
  background:#BFF7FF;          /* was #DA5726 - a leftover from the orange palette */
}
body.ms-skin #setup-screen #intro-continue:active{
  box-shadow:0 3px 0 var(--m-accent-shadow);
}
/* :not(.sending) matters: while a turn is in flight the page paints the label
 * transparent and spins a ring in its place (#607), and that rule has exactly
 * this specificity - so without the exclusion this one loads later, wins, and
 * "Send" sits painted under the spinner. */
body.ms-skin.play #send:not(.sending),
body.ms-skin.play #host-send:not(.sending){
  background:var(--m-accent); color:var(--ms-on-accent); border-radius:999px;
}

/* ── The pup chooser (sandbox/pup-picker.js) ──────────────────────────────
 * Design screens 8 and 16: the pups line the bottom of the screen and the one
 * in the middle is the host. Snap points are half the strip wide, so the
 * neighbours sit half in frame - that spacing is the picker's whole feel, and
 * it comes from --pup-item, which the module keeps in sync with the width.  */
.pup-picker{
  position:fixed; left:0; right:0; bottom:0; z-index:5;
  pointer-events:none;                 /* only the strip itself takes input */
}
body.ms-skin:not(.setup) .pup-picker{ display:none; }
/* ── Setup in two steps (design screens 4 + 20) ───────────────────────────
 * Step 1 shows the name alone; step 2 shows the narrator alone. The classes
 * are written by the step controller in the page's second module; with no
 * class (controller dead), nothing here matches and the screen shows whole. */
/* The 3D model never shows on EITHER setup step (owner, 2026-08-13): the
 * picker's 2D art is the chooser, and mountPup() keeps loading the GLB in the
 * background behind this display:none so the play screen gets him warm. The
 * hide carries #setup-screen because the slot's own display rule does — a
 * class-only hide loses to the id and the pup stayed painted (the same
 * cascade trap as the parents link below, and the pins file's oldest lesson). */
body.ms-skin.setup-step-name #setup-screen .narrator-slot,
body.ms-skin.setup-step-pup #setup-screen .narrator-slot{ display:none; }
body.ms-skin.setup-step-name .pup-picker,
body.ms-skin.setup-step-name #mode-field,
body.ms-skin.setup-step-name .setup-head .lede,
body.ms-skin.setup-step-name #intro-continue{ display:none; }
body.ms-skin.setup-step-pup .field:has(#child-name),
body.ms-skin.setup-step-pup #setup-name-next,
body.ms-skin.setup-step-pup #mode-field,
/* the disclosure + parents link are the GROWN-UP's reading, and on this step
 * they sat under the centred pup — step 1 (the parent's screen) keeps them.
 * The link's hide carries #setup-screen too: its own display rule is
 * `#setup-screen #setup-debug-open:not([hidden])` (two ids), which outranks
 * a one-id hide and kept the link painted on the child's step. */
body.ms-skin.setup-step-pup .disclosure,
body.ms-skin.setup-step-pup #setup-screen #setup-debug-open,
body.ms-skin.setup-step-pup .setup-head .lede{ display:none; }
/* ("We're Ready" lives ON the cover now — the step controller moves it into
 * .setup-head; its sizing rides with the cover block below.) */
#setup-name-next{ min-height:56px; width:100%; margin-top:6px;
  font-size:17px; cursor:pointer; }
#setup-name-next[disabled]{ opacity:.4; cursor:not-allowed; box-shadow:none; }
/* ONE tall portrait cover carries each step (the design's notebook, screens
 * 4 + 20): everything on it, like writing on a real book. Two ids where the
 * page's own rules carry ids. */
body.ms-skin.setup-step-name #setup-screen .setup-head,
body.ms-skin.setup-step-pup #setup-screen .setup-head{
  width:min(480px, 90vw); min-height:min(70vh, 640px);
  margin-inline:auto;
  display:flex; flex-direction:column; align-items:center;
  gap:16px; padding:30px 30px 36px;
}
body.ms-skin.setup-step-name #setup-screen .setup-head{ justify-content:center; }
/* step 2: title + CTA sit high; the strip's pups overlap the cover's foot */
body.ms-skin.setup-step-pup #setup-screen .setup-head{
  justify-content:flex-start; padding-top:11vh;
}
/* step 1's h2 is the SMALL kicker; the field's own label is the big line */
body.ms-skin.setup-step-name #setup-screen .setup-head h2{
  font:700 16px/1.2 var(--m-font); letter-spacing:.04em; text-transform:none;
  color:var(--m-ink-muted); margin:0;
}
body.ms-skin.setup-step-name #setup-screen .field{ width:100%; align-items:center; }
body.ms-skin.setup-step-name #setup-screen .field>label{
  font-family:var(--ms-display); font-weight:800; text-transform:uppercase;
  font-size:clamp(28px,5.5vw,42px); line-height:.95; letter-spacing:.02em;
  color:var(--m-ink); text-align:center;
}
/* the white input the design writes the name into */
body.ms-skin.setup-step-name #setup-screen #child-name{
  background:#fff; color:#1B1745; text-align:center;
  border-radius:14px; border-color:#fff; font-size:20px; font-weight:600;
}
body.ms-skin.setup-step-name #setup-screen #setup-name-next{ width:82%; }
/* step 2's cover title in the accent, like the design's DANNY'S STORY */
body.ms-skin.setup-step-pup #setup-screen .setup-head h2{ color:var(--m-accent); }
body.ms-skin.setup-step-pup #setup-screen #intro-continue{
  width:82%; margin-top:6px; min-height:58px;
  /* clips the sheen below to the pill, and makes the pill a stacking context so
   * the sheen can sit BEHIND the label: a negative-z child paints above its
   * parent's background and below the parent's own text. Without the isolate
   * the sheen would either wash the word out as it passed (measured: "Ready"
   * went grey mid-sweep) or, at z-index:-1 with no stacking context, disappear
   * behind the cyan fill entirely. */
  position:relative; overflow:hidden; isolation:isolate;
}
/* "We're Ready" carries the same invitation as the shelf's "Create new story"
 * cover: a highlight travelling across it, so the two "this is the thing to
 * tap" moments read as one family. Literally the same one - msInvite above,
 * keyframes and cadence and easing - because both are a gradient sliding across
 * a 300%-wide background. The only difference is what the gradient paints: the
 * cover clips it to its ink, and the pill (a solid fill, not text) wears it as a
 * sheen. A tweak to the invitation is therefore meant to move both.
 *
 * Only the sheen animates. A breathing scale or glow on the BUTTON would have
 * been an animation on transform/box-shadow, and an animation outranks every
 * normal declaration - including #setup-screen #intro-continue:active's sink,
 * which is the button's whole press feedback. The overlay rides along with that
 * sink instead of replacing it. */
body.ms-skin.setup-step-pup #setup-screen #intro-continue::after{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background-image:linear-gradient(100deg,
    transparent 0%, transparent 36%, rgba(255,255,255,.45) 50%,
    transparent 64%, transparent 100%);
  background-size:300% 100%;
  animation:msInvite 3.4s cubic-bezier(.5,0,.5,1) infinite;
}
@media (prefers-reduced-motion:reduce){
  body.ms-skin.setup-step-pup #setup-screen #intro-continue::after{ animation:none; opacity:0; }
}
.pup-strip{
  pointer-events:auto;
  /* "way way bigger" (owner, 2026-08-12, against the Figma where the pups
   * stand ~2/3 of the book's height): the card width derives from THIS
   * height (pup-picker.js itemWidth), so one clamp scales the whole strip. */
  display:block; width:100%; height:clamp(230px,46vh,480px);
  overflow-x:auto; overflow-y:hidden;
  scroll-snap-type:x mandatory;
  scrollbar-width:none; -ms-overflow-style:none;
  -webkit-overflow-scrolling:touch;
  /* The strip is a horizontal scroller inside a page that scrolls vertically,
   * so a fling that reaches the end of the loop buffer would otherwise chain
   * out to the page - or, in iOS Safari, to the edge back-swipe. */
  overscroll-behavior-x:contain;
  /* The pups stand in a pool of light rather than on a visible shelf. */
  background:radial-gradient(120% 84% at 50% 100%,
    rgba(166,243,255,.26) 0%, rgba(200,168,255,.14) 46%, transparent 74%);
}
/* The ONE exception to the snap above, held only while a desktop mouse drag is
 * in flight (pup-picker.js toggles the class). A mouse cannot pan an overflow
 * container, so the module writes scrollLeft itself — and `mandatory` re-snaps
 * the scroller after every such write, which undid the drag completely: a 30px
 * pull moved the row 0px. It must NOT be relaxed permanently, and `proximity`
 * is not a shortcut to the same place: the child's real gesture is a touch
 * fling, and only the browser's own mandatory snap decelerates into a pup as
 * part of that fling. */
.pup-strip.is-dragging{ scroll-snap-type:none; }
.pup-strip::-webkit-scrollbar{ display:none; }
/* The row's arrival, played when the step that shows the strip opens
 * (pup-picker.js arrive()): the pups start a little to the right of home and
 * are dragged into place, so the child's first sight of the row is one already
 * in motion. A transform on the TRACK, deliberately - it moves no scroll
 * position, so the snap grid the fling depends on is untouched (pup-picker.js
 * says what scrolling it instead did). The distance is a fraction of one card,
 * so it stays a hint of the drag on every screen size rather than a fixed
 * number of pixels that is a nudge on a tablet and a lurch on a phone. */
.pup-strip.is-arriving .pup-track{
  animation:msPupArrive .5s cubic-bezier(.22,.61,.36,1) both;
}
@keyframes msPupArrive{
  from{ transform:translate3d(calc(var(--pup-item,200px) * .3), 0, 0); }
  to  { transform:none; }
}
@media (prefers-reduced-motion:reduce){
  .pup-strip.is-arriving .pup-track{ animation:none; }
}
.pup-track{ display:flex; align-items:flex-end; height:100%; }
.pup-card{
  flex:0 0 auto; width:var(--pup-item,200px); height:100%;
  scroll-snap-align:center;
  position:relative; border:0; background:transparent; padding:0 0 10px;
  cursor:pointer; transform-origin:50% 100%;
  display:flex; flex-direction:column; align-items:center; justify-content:flex-end;
  /* transform/opacity only: the module repaints these every frame. */
  transition:opacity .26s ease;
  -webkit-tap-highlight-color:transparent;
}
/* Off centre is dimmed and stepped back (the card's own scale, set per frame by
 * the module); the centred pup is fully lit.
 *
 * NO blur() here, deliberately. The design shows the siblings defocused, and
 * two ways of getting there have both failed on a real device: a second,
 * blurred copy of the sprite stacked over the sharp one reads as a
 * misregistered ghost rather than defocus, and blurring the sprite itself
 * paints the whole centred card as an opaque WHITE rectangle on iOS Safari -
 * filter on a child of a 3D-transformed, composited card. Opacity and scale
 * separate centre from sides on every browser, and cost a phone nothing. */
.pup-art{
  height:73%; width:100%; object-fit:contain; object-position:50% 100%;
  filter:drop-shadow(0 14px 22px rgba(0,0,0,.5));
  opacity:.52;
  transition:opacity .24s ease;
  user-select:none;
}
.pup-card.is-centred .pup-art{ opacity:1; }
@media (prefers-reduced-motion:reduce){ .pup-art{ transition:none; } }
/* The sprite never arrived (pup-picker.js retries once, then marks the card).
 * Safari paints a broken-image "?" for a failed <img>, and three of those where
 * the pups stand is what the child was actually shown (owner report,
 * 2026-08-13). The card falls back to the name it already carries for the
 * screen reader, so the strip still says who is who and can still pick a host. */
.pup-card[data-art-failed] .pup-art{ display:none; }
.pup-card[data-art-failed]::after{
  content:attr(aria-label);
  font-family:var(--ms-display); font-weight:800; text-transform:uppercase;
  font-size:clamp(15px,3.4vw,22px); line-height:1.1; letter-spacing:.03em;
  color:var(--m-ink); text-shadow:0 2px 10px rgba(0,0,0,.55);
  padding-bottom:14%;
}
/* The host on the story screen is sized for a tablet: at 240x190 on a phone he
 * covers the cover's own title and half the picture beside it. He keeps his
 * place on the seam, at a size the narrower screen can hold. */
@media (max-width:560px){
  /* One size for every state. `planning` is a sub-state of `play`, so this rule
   * covers the wait too - and deliberately does not resize him for it. Sizing
   * him up during the wait makes him visibly SHRINK the moment the first page
   * arrives (#566), and because mountPup sizes the WebGL canvas once at mount,
   * the larger box is an upscale: a soft pup for the rest of the story. The
   * page's own stylesheet says the same thing above its `body.play #pup` rule;
   * this file loads later and outranks it, so it has to agree. */
  /* The phone puts him at the BOTTOM of the screen, centred, so the halo -
   * which rises from the bottom edge - glows from under his feet rather than
   * behind his shoulder halfway up the panel (owner, 2026-08-13). Centring by
   * inset+margin, never transform: a transformed ancestor would capture the
   * fixed corner controls, which is the #mic-wrap lesson from #629. */
  body.ms-skin.play #pup{
    width:168px; height:133px;
    left:0; right:0; margin-inline:auto;
    top:auto; bottom:6px;
  }
}
/* The card carries no printed name (owner, 2026-08-13) - the pup is the label.
 * The rules that styled one are gone rather than hidden: a display:none rule for
 * an element nobody creates is the kind of thing that outlives the reason for it. */
.pup-strip:focus-visible{ outline:2px solid var(--m-accent,#A6F3FF); outline-offset:-4px; }

/* Onboarding 2's shield is real artwork in the design, not a CSS polygon - and
 * it is the biggest thing on that screen, sitting above the headline. The
 * artwork's ribbon is blank: the words on it are type, so they are type here
 * too, placed over the ribbon rather than baked into the bitmap. */
.ms-badge{
  position:relative; display:block;
  background:none; clip-path:none; box-shadow:none;
  width:clamp(150px,20vw,196px); aspect-ratio:903/1020; height:auto;
  margin-bottom:6px; color:#0B1020;
  font-size:clamp(22px,2.6vw,29px);
  background-image:url("/demos/assets/magic-story/39964e217052d1df.webp");
  background-size:contain; background-position:center; background-repeat:no-repeat;
  filter:drop-shadow(0 12px 26px rgba(0,0,0,.45));
}
/* Every line is placed against the ARTWORK, by the fractions its own pixels
 * give up: the shield face runs 12%-81% down the image and the ribbon crosses
 * it at 57%-80%. Percentages of the badge's own height, which is what `top` on
 * an absolute child resolves against - percentage PADDING would resolve
 * against the slide's width instead, which is how this box ended up 397px tall
 * on a 223px shield. */
.ms-badge b,
.ms-badge span,
.ms-badge-ribbon{ position:absolute; left:0; right:0; text-align:center; line-height:.94; }
.ms-badge b{ top:24%; }
.ms-badge span{ top:37%; }
.ms-badge-ribbon{
  top:65.5%;
  font-family:var(--ms-display,sans-serif); font-style:normal; font-weight:800;
  text-transform:uppercase; letter-spacing:.04em;
  font-size:clamp(9px,1.15vw,13px); line-height:1; color:#1B2A6B;
}

@media (prefers-reduced-motion: reduce){
  .pup-strip{ scroll-behavior:auto; }
}

/* The strip is fixed to the bottom edge, so the setup screen has to give up
 * that much room or the centred pup lands on the CTA and the disclosure.
 *
 * Note this reserve only helps while the column still has slack: #intro-continue
 * uses margin-top:auto, so once the content is taller than the viewport that
 * auto collapses and padding can no longer lift the text. That is why the strip
 * itself is sized small (the design's bottom pups are small) rather than relying
 * on this alone. Kept in step with .pup-strip's height by the shared clamp. */
body.ms-skin.setup #setup-screen{
  padding-bottom:calc(clamp(158px,30vh,290px) + 22px);
  padding-top:22px;
  gap:12px;
}

/* Reclaiming the room the bigger pups need.
 *
 * The 3D narrator preview was the only way to see who would tell the story, so
 * it got a 210-270px card of its own. The pup strip now shows that pup - big,
 * and with its neighbours - so the preview is duplicated, and at 270px it was
 * the single largest thing standing between the strip and a decent size. It is
 * scaled down here rather than removed: #pup keeps a non-zero box, so
 * mountPup()'s `el.clientWidth || 230` still measures a real size and the 3D
 * host is unaffected on the play screen, where it is moved into .book. */
body.ms-skin.setup #setup-screen .narrator-slot{ width:200px; height:152px; margin-bottom:0; }
/* #narrator-mount is deliberately NOT shrunk. mountPup() sizes the WebGL canvas
 * from `el.clientWidth || 230` ONCE, at mount time, and #pup is later MOVED into
 * .book for the play screen - so a smaller box here would create a small canvas
 * that stays small (and soft) for the whole story. Only the slot around it gives
 * up room. */

/* ── Choosing the book's style (design screens 9 and 10) ──────────────────
 * This step owns the WHOLE screen in the design, so the picker is lifted out
 * of the two-page .book card (story-front.js moves the element to the body)
 * and painted as its own layer over it.
 *
 * The four styles are BOOKS, not thumbnails: portrait covers with a spine, two
 * stacked down each side. Between them is .ms-stage - before a pick it is the
 * child's own closed story book with their name on it, and on a pick it shows
 * that style's cover large while the tapped book stays in its slot wearing a
 * cyan outline. The pup stands in front of the stage on a pool of light and
 * steps aside once a cover is chosen, which is when "Start Story" appears.
 *
 * renderStyleTiles() is untouched; the pick/confirm split lives in the page. */
body.ms-skin #style-picker{
  position:fixed; inset:0; z-index:40;
  padding:clamp(12px,2.6vh,30px) clamp(12px,3vw,44px)
          calc(clamp(12px,2.6vh,30px) + env(safe-area-inset-bottom, 0px));
  gap:clamp(8px,1.8vh,18px);
  align-items:center; justify-content:center;
  background-color:var(--m-app);
  background-image:var(--ms-glow), var(--ms-paw);
  background-size:auto, cover;   /* one designed field, not a tile (see body) */
}
body.ms-skin .ms-style-head{
  margin:0; text-align:center; flex:0 0 auto;
  font-family:var(--ms-display); font-weight:800; text-transform:none;
  letter-spacing:.01em; font-size:clamp(21px,3.2vh,34px); color:var(--m-ink);
}
body.ms-skin #style-grid{
  flex:1 1 auto; min-height:0; width:min(1160px,100%);
  display:grid;
  grid-template-columns:minmax(84px,1fr) minmax(0,2fr) minmax(84px,1fr);
  grid-template-rows:1fr 1fr;
  gap:clamp(10px,2.2vh,26px) clamp(14px,4vw,56px);
  align-items:center;
  justify-items:center;
}

/* Every cover in this picker is a book: portrait, rounded on the outer edge,
 * with a spine down the left and a hint of page block on the right. */
body.ms-skin #style-grid .style-tile,
body.ms-skin .ms-stage{
  aspect-ratio:165 / 225;
  border-radius:5px 17px 17px 5px;
  box-shadow:inset 5px 0 0 -1px rgba(255,255,255,.34),
             inset -3px 0 0 -1px rgba(255,255,255,.16),
             0 16px 34px rgba(0,0,0,.46);
}
/* The centre stage: the closed story book until a style is chosen, then that
 * style's cover. It spans both rows, so it is the tallest thing on screen. */
body.ms-skin .ms-stage{
  grid-column:2; grid-row:1 / 3;
  position:relative;
  width:auto; height:100%; max-height:100%; max-width:100%; justify-self:center;
  background-size:cover; background-position:center;
  background-color:#221D4E;
  border-radius:6px 22px 22px 6px;
  box-shadow:inset 6px 0 0 -1px rgba(255,255,255,.36),
             inset -4px 0 0 -1px rgba(255,255,255,.18),
             0 26px 60px rgba(0,0,0,.55);
}
/* The pool of light the pup stands on, spilling past the book's bottom edge. */
body.ms-skin .ms-stage::after{
  content:""; position:absolute; left:50%; bottom:-16%;
  width:150%; height:48%; transform:translateX(-50%);
  pointer-events:none; z-index:1;
  background:radial-gradient(closest-side,
    rgba(166,243,255,.46) 0%, rgba(200,168,255,.26) 55%, transparent 78%);
  filter:blur(2px);
  transition:opacity .3s ease;
}
body.ms-skin .ms-stage-title{
  position:absolute; inset:8% 8% auto; z-index:2;
  top:38%; transform:translateY(-50%);
  text-align:center; text-wrap:balance;
  font-family:var(--ms-display); font-weight:800; text-transform:uppercase;
  font-size:clamp(28px,5.2vh,64px); line-height:.94; letter-spacing:.01em;
  color:var(--m-accent);
  text-shadow:0 0 34px rgba(133,244,250,.45);
  transition:opacity .3s ease;
}
/* Screen 10: the cover art is the whole face, so the name comes off it. */
body.ms-skin .ms-stage.has-cover .ms-stage-title{ opacity:0; }
/* The child cannot read "Start Story". Once a cover is chosen the BOOK ITSELF
 * is the way in - tap a small book to try it on, tap the big one to open it -
 * so the step can be completed with no words at all. It breathes to say so. */
body.ms-skin.style-chosen .ms-stage{ cursor:pointer; animation:msOpenMe 2.6s ease-in-out infinite; }
/* Glow only, no transform: the pup stands INSIDE this box, so a pulsing scale
 * would rock him with it - and an element whose box never settles is one no
 * automated check can ever click. */
@keyframes msOpenMe{
  0%,100%{ box-shadow:inset 6px 0 0 -1px rgba(255,255,255,.36),
                      inset -4px 0 0 -1px rgba(255,255,255,.18),
                      0 26px 60px rgba(0,0,0,.55), 0 0 0 rgba(133,244,250,0); }
  52%    { box-shadow:inset 6px 0 0 -1px rgba(255,255,255,.44),
                      inset -4px 0 0 -1px rgba(255,255,255,.22),
                      0 26px 60px rgba(0,0,0,.55), 0 0 46px rgba(133,244,250,.5); }
}
/* Sent: the wait after this is the longest in onboarding, and the picker covers
 * the card the game writes its progress into - so the CTA becomes the progress.
 * A frozen screen with a dead button is what this replaces. */
body.ms-skin.style-starting .ms-stage{ animation:none; cursor:default; }
body.ms-skin.style-starting #style-start{
  color:transparent; position:relative; pointer-events:none;
  background:rgba(255,255,255,.1); box-shadow:none;
}
body.ms-skin.style-starting #style-start::after{
  content:"Getting your story ready…";
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  color:var(--m-ink); font-weight:700;
}
body.ms-skin.style-starting #style-start::before{
  content:""; position:absolute; left:0; top:0; bottom:0; width:45%;
  border-radius:999px;
  background:linear-gradient(90deg,rgba(166,243,255,0),rgba(166,243,255,.35),rgba(166,243,255,0));
  animation:msBoot 1.15s ease-in-out infinite;
}
@media (prefers-reduced-motion:reduce){
  body.ms-skin.style-chosen .ms-stage{ animation:none; }
  body.ms-skin.style-starting #style-start::before{ animation:none; }
}

/* The live 3D host, borrowed from the play shell (story-front.js). Only a
 * little bigger than the 240x190 mountPup() built his canvas at - scaling one
 * UP softens it, and pup3d's 1.5x supersample is the only headroom there is -
 * and anchored to the stage's bottom edge so he stands in FRONT of the book,
 * his paws over its edge. */
body.ms-skin.style-picking #pup{
  position:absolute; left:50%; bottom:-22px; z-index:2;
  width:300px; height:238px; margin:0; display:block;
  transform:translateX(-50%);
  transform-origin:50% 100%;
  pointer-events:none;
  transition:transform .42s cubic-bezier(.34,.8,.3,1), opacity .32s ease;
}
body.ms-skin.style-picking #pup canvas{ width:100%; height:100%; display:block; }
/* Screen 10 is the cover's moment, so the pup steps off the art the child just
 * chose - but he STAYS. He is the one who has been talking to them all session,
 * and the moment a cover is picked is the moment they have to understand there
 * is a second step; fading him out there deletes the messenger with the
 * message. He moves to the stage's edge and shrinks instead. */
body.ms-skin.style-picking.style-chosen #pup{
  transform:translateX(-96%) scale(.74);
}
body.ms-skin.style-picking.style-chosen .ms-stage::after{ opacity:.5; }

body.ms-skin #style-grid .style-tile:nth-of-type(1){ grid-column:1; grid-row:1; }
body.ms-skin #style-grid .style-tile:nth-of-type(2){ grid-column:1; grid-row:2; }
body.ms-skin #style-grid .style-tile:nth-of-type(3){ grid-column:3; grid-row:1; }
body.ms-skin #style-grid .style-tile:nth-of-type(4){ grid-column:3; grid-row:2; }
body.ms-skin #style-grid .style-tile{
  margin:0; padding:0; overflow:hidden; border:0;
  background:var(--m-sunken); height:100%; width:auto; max-width:100%;
  transition:transform .22s ease, box-shadow .22s ease;
}
body.ms-skin #style-grid .style-tile:hover{ transform:translateY(-4px); }
body.ms-skin #style-grid .style-tile img{
  width:100%; height:100%; object-fit:cover; display:block; border-radius:inherit;
}
/* The pick stays in its slot and is ringed, as in screen 10. */
body.ms-skin #style-grid .style-tile[aria-pressed="true"]{
  outline:2.5px solid var(--m-accent);
  outline-offset:5px;
  box-shadow:inset 5px 0 0 -1px rgba(255,255,255,.34),
             0 16px 34px rgba(0,0,0,.46), 0 0 30px rgba(133,244,250,.32);
}
body.ms-skin #style-grid .style-tile .badge{
  background:var(--m-accent); color:var(--ms-on-accent); border:0;
}
/* The CTA the pick unlocks. Sized like the design's pill; the cyan fill and
 * dark ink come from the shared rule at the top of this file. */
body.ms-skin #style-start{
  flex:0 0 auto; padding:15px 42px;
  font-family:var(--m-font); font-size:clamp(17px,2.2vh,20px);
}

/* Portrait phones have no room for side columns AND a stage: keep the stage on
 * top (it is the thing being chosen) with the four books in a row beneath. */
@media (max-width:560px){
  body.ms-skin #style-grid{
    grid-template-columns:repeat(4,1fr);
    grid-template-rows:minmax(0,1fr) auto;
    gap:10px;
  }
  /* The height is capped in vw, not by max-width: a definite height AND a
   * definite max-width both apply, and the pair beats aspect-ratio - which is
   * what stretched the book into a tall slab here. 126vw is the tallest this
   * ratio can be while its own width still fits the padded viewport. */
  body.ms-skin .ms-stage{
    grid-column:1 / 5; grid-row:1;
    height:min(100%,126vw); width:auto; max-width:none;
  }
  body.ms-skin #style-grid .style-tile{ height:auto; width:100%; }
  body.ms-skin #style-grid .style-tile:nth-of-type(1){ grid-column:1; grid-row:2; }
  body.ms-skin #style-grid .style-tile:nth-of-type(2){ grid-column:2; grid-row:2; }
  body.ms-skin #style-grid .style-tile:nth-of-type(3){ grid-column:3; grid-row:2; }
  body.ms-skin #style-grid .style-tile:nth-of-type(4){ grid-column:4; grid-row:2; }
  body.ms-skin #style-grid .style-tile[aria-pressed="true"]{ outline-offset:3px; }
  /* He is nearly as wide as the stage here, so he shrinks with it. */
  body.ms-skin.style-picking #pup{ width:170px; height:135px; bottom:-14px; }
}

/* ── The grown-ups' sheet and its controls ────────────────────────────────
 * Reached from "Settings for parents" on the setup screen and from the ···
 * during play. The page MOVES the live controls in here (openGrownup), so these
 * are the same elements the setup screen owns - styling them once covers both
 * places, and is why the selectors are not scoped to the sheet.             */
/* Above the gallery overlay (.ms-front, z-index 60). The sheet is opened FROM
 * that overlay's ⚙, and at its own z-index of 50 it opened silently behind it:
 * the tap did nothing visible, and the sheet was discovered later, sitting
 * under the story. Only the boot veil (200) is allowed over this. */
body.ms-skin #grownup-scrim{
  z-index:120;
  background:rgba(6,4,20,.66); backdrop-filter:blur(6px);
}
body.ms-skin #grownup-sheet{
  background:linear-gradient(158deg,#282253 0%,#1F1A47 60%,#191540 100%);
  color:var(--m-ink);
  border-radius:26px 26px 0 0;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.16), 0 -18px 60px rgba(0,0,0,.6);
}
body.ms-skin #grownup-sheet .sheet-head h3{
  font-family:var(--ms-display); font-weight:800; text-transform:uppercase;
  letter-spacing:.03em; color:var(--m-ink);
}
body.ms-skin #grownup-close{
  background:rgba(255,255,255,.08); color:var(--m-ink-muted);
  border:0; border-radius:50%;
}
body.ms-skin #grownup-close:hover{ background:rgba(255,255,255,.16); color:#fff; }
body.ms-skin #grownup-done{
  background:var(--m-accent); color:var(--ms-on-accent);
  border:0; border-radius:999px; font-weight:700;
  box-shadow:0 8px 22px rgba(133,244,250,.26);
}

/* Form controls, in the sheet or on the setup screen. */
body.ms-skin #setup-screen label,
body.ms-skin #grownup-sheet label,
body.ms-skin .field-label,
body.ms-skin .row-label{ color:var(--m-ink-muted); }
body.ms-skin input[type="text"],
body.ms-skin #child-name,
body.ms-skin #host-answer,
body.ms-skin #answer,
body.ms-skin #skip-topic,
body.ms-skin select,
body.ms-skin textarea{
  background:var(--m-sunken); color:var(--m-ink);
  border:1.5px solid var(--m-hairline); border-radius:14px;
  font-family:var(--m-font);
}
/* The play screen's two answer boxes keep the page's own locked/unlocked
 * contrast. The blanket rule above paints every field --m-sunken, which is
 * exactly the colour the page uses for :disabled - so a box locked mid-turn
 * looked identical to an editable one, and the "you cannot type right now"
 * signal (#607) disappeared under the skin. */
body.ms-skin.play #answer:not(:disabled),
body.ms-skin.play #host-answer:not(:disabled){ background:var(--m-card); }

body.ms-skin input::placeholder,
body.ms-skin textarea::placeholder{ color:var(--m-ink-faint); }
body.ms-skin input:focus-visible,
body.ms-skin select:focus-visible,
body.ms-skin textarea:focus-visible{
  outline:0; border-color:var(--m-accent);
  box-shadow:0 0 0 3px rgba(166,243,255,.22);
}
/* Talk / Type - the chosen one carries the accent, so it needs dark ink too. */
body.ms-skin #mode-toggle{ background:rgba(255,255,255,.05); border-radius:16px; }
body.ms-skin #mode-toggle .mode-opt{
  background:transparent; color:var(--m-ink-muted); border:0; border-radius:13px;
  font-family:var(--m-font);
}
body.ms-skin #mode-toggle .mode-opt.active{
  background:var(--m-accent); color:var(--ms-on-accent); font-weight:700;
  box-shadow:0 6px 16px rgba(133,244,250,.24);
}
body.ms-skin #mode-toggle .mode-opt .sub{ color:inherit; opacity:.72; }
body.ms-skin #mode-note{ color:var(--m-ink-faint); }
body.ms-skin .rows,
body.ms-skin .rows .row,
body.ms-skin .dev-card{
  background:rgba(255,255,255,.04); border-color:var(--m-hairline-soft);
  border-radius:16px; color:var(--m-ink);
}
body.ms-skin .dev-card summary{ color:var(--m-ink); font-family:var(--m-font); }
body.ms-skin .dev-card .hint,
body.ms-skin #skip-note{ color:var(--m-ink-faint); }
body.ms-skin #skip-source button{
  background:rgba(255,255,255,.06); color:var(--m-ink-muted);
  border:1.5px solid var(--m-hairline); border-radius:999px;
}
body.ms-skin #skip-source button[aria-pressed="true"]{
  background:var(--m-accent); color:var(--ms-on-accent); border-color:transparent; font-weight:700;
}
body.ms-skin #skip-go,
body.ms-skin #skip-use-example{
  background:var(--m-accent); color:var(--ms-on-accent);
  border:0; border-radius:999px; font-weight:700;
}
/* The setup screen's own entry into the sheet - a quiet link, not a dev button. */
body.ms-skin #setup-debug-open{
  background:transparent; color:var(--m-ink-muted); border:0;
  font-family:var(--m-font); font-size:13px; text-decoration:underline;
  text-underline-offset:3px; cursor:pointer;
}
body.ms-skin #setup-debug-open:hover{ color:var(--m-accent); }
body.ms-skin #grownup-open{
  background:rgba(10,10,30,.6); color:var(--m-ink); border:0;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
}

/* ── "Switch to typing": a corner affordance that gets out of the way ────
 * It used to be a permanent line of text under the story - a reading
 * instruction, in a game for a child who cannot read, sitting on screen for
 * the whole book. It is now the top-left twin of the grown-up's ··· button:
 * present and tappable while the child settles into the first page, then
 * faded almost out of existence (owner, 2026-08-13). Fading, never hiding,
 * during play - a child (or a grown-up) whose mic fails must still be able to
 * reach it, and hover/focus brings it straight back. The ONE exception is the
 * style pick below: that screen is a full-screen layer whose only action is
 * tapping a cover, so a floating control there is noise the child cannot use.
 * It TAKES the screen's top-left corner; #page-meta, which used to own it,
 * moves down to sit under it. */
body.ms-skin.play #mode-switch{
  /* FIXED, not absolute: this button is a child of the text panel, so an
   * absolute corner is the PANEL's corner — which on a phone is right on top
   * of the host's first line (owner, 2026-08-13: "it hides text"). Fixed puts
   * it in the screen's own top-left, the mirror of the grown-up's ··· at the
   * top-right. Nothing in its ancestor chain may carry a transform or the
   * containing block moves back to the panel — see the #mic-wrap lesson. */
  position:fixed; top:14px; left:16px; right:auto; bottom:auto; margin:0;
  /* the old placement centred it with translateX(-50%); left over, that
   * pulled the pill half its own width off the screen (measured -37px) */
  transform:none;
  /* 35: above the page and its art (30) so it is reachable during play, and
   * BELOW everything that must own the frame while it is up — the turning
   * page (.ms-leaf, 40), the style picker (40) and the grown-up sheet (50).
   * It was 45, which floated it over the picker and intercepted taps meant
   * for a book cover (macs-review, 2026-08-13). Hidden outright during the
   * pick below: typing is not one of the choices on that screen. */
  z-index:35; display:inline-flex; align-items:center; gap:6px;
  padding:7px 11px; border-radius:999px; border:0;
  background:rgba(10,10,30,.6); box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
  font:600 11px var(--m-font); color:var(--m-ink); text-decoration:none;
  opacity:1; transition:opacity 1.4s ease;
}
/* The page counter yields the corner to it and sits just below: the counter
 * is permanent and can move, the typing control is the one the owner asked to
 * be in the screen's very top-left. */
body.ms-skin.play #page-meta{ top:52px; }
/* The one place it is HIDDEN rather than faded (the section header above says
 * why that is the exception): the style step is a full-screen layer whose
 * only action is tapping a cover, and there is no typed or spoken turn to
 * escape from while it is up. */
body.ms-skin.play.style-picking #mode-switch{ display:none; }
/* The fade the child never has to dismiss: armed when the story starts, so
 * the control is there for the first half-minute and then all but gone. */
body.ms-skin.play.ms-typing-faded #mode-switch{ opacity:.1; }
body.ms-skin.play #mode-switch:hover,
body.ms-skin.play #mode-switch:focus-visible{ opacity:1; transition-duration:.15s; }
@media (prefers-reduced-motion: reduce){
  body.ms-skin.play #mode-switch{ transition:none; }
}

/* ── Turning a page (design screen 15, Apple Books reference) ──────────────
 * A real turn, not a cross-fade: a sheet rotates about the spine over the
 * spread. It has TWO faces - the page being left behind and the paper back of
 * it - because a single face with backface-visibility:hidden disappeared at 90
 * degrees, which is why every earlier attempt read as a blank flash and a cut
 * rather than a turn. story-karaoke.js adds .ms-turning for one animation and
 * hands the outgoing picture in on --ms-leaf-art.                            */
body.ms-skin .ms-leaf{
  position:absolute; inset:0; z-index:40; pointer-events:none;
  perspective:2400px; perspective-origin:0% 50%;
  display:none;
}
body.ms-skin.ms-turning .ms-leaf{ display:block; }
/* .book clips its children so the picture cannot escape the pane; for the one
 * animation the sheet needs that clip lifted, or its far edge is sliced off
 * mid-sweep and the paper reads as flat. */
body.ms-skin.ms-turning .book{ overflow:visible; }
body.ms-skin .ms-leaf i{
  position:absolute; left:0; top:0; width:100%; height:100%;
  transform-origin:left center; transform-style:preserve-3d;
  animation:msLeaf .82s cubic-bezier(.36,.02,.26,1) forwards;
}
/* The shadow falls to the RIGHT of the spine, onto the page being revealed -
 * that shadow, travelling with the sheet, is what sells the lift. */
body.ms-skin .ms-leaf b,
body.ms-skin .ms-leaf u{
  position:absolute; inset:0; overflow:hidden;
  border-radius:0 14px 14px 0;
  backface-visibility:hidden;
  box-shadow:26px 0 54px rgba(0,0,0,.45);
}
/* The front: the page the child was reading - its picture on top, its story
 * card below, in the same 44/56 split the play screen uses. Both are handed in
 * by story-karaoke.js, which keeps them: the game has already replaced the real
 * ones by the time a turn starts. */
/* The split and the type size are the PAGE's, not the leaf's own guesses. They
 * mirror body.play .right-page{flex:0 0 44%} / its min-width:700px override,
 * and #page-text's max-height:760px size step - the three numbers the leaf has
 * to agree with or the outgoing page visibly jumps the frame it lifts. Declared
 * here so both media queries below change one value each. */
body.ms-skin.play .ms-leaf{ --ms-leaf-split:44%; --ms-leaf-size:19px; }
@media (min-width:700px){ body.ms-skin.play .ms-leaf{ --ms-leaf-split:50%; } }
@media (max-height:760px){ body.ms-skin.play .ms-leaf{ --ms-leaf-size:17px; } }
body.ms-skin .ms-leaf b{
  background-color:var(--m-card);
  background-image:var(--ms-leaf-art,none);
  background-size:100% var(--ms-leaf-split,44%);
  background-position:top center;
  background-repeat:no-repeat;
}
body.ms-skin .ms-leaf b span{
  position:absolute; left:0; right:0; top:var(--ms-leaf-split,44%);
  padding:34px 22px 0;
  /* All four handed in by story-karaoke's turn(), read off #page-text itself.
   * The literals are fallbacks for a turn that somehow arrives without them,
   * not the intended values: a constant here is a guess about a size three
   * media queries and one JS function are all entitled to change. */
  font:var(--ms-leaf-weight,500) var(--ms-leaf-size,19px)/var(--ms-leaf-line,1.45)
       var(--ms-leaf-face,var(--ms-story,var(--m-font)));
  color:var(--ms-ink,var(--m-ink));
  /* Full ink, like the page it is a picture of. Dimming it made the words the
   * child had just been reading fade the instant the sheet lifted. */
  opacity:1;
}
/* The back: the page being turned TO, over paper shaded towards the spine
 * where the sheet lifts. --ms-leaf-back-art is the incoming picture when the
 * caller already has it (the reader does; a live story's next picture has not
 * been drawn yet), so the verso of the sheet is the recto of the next page,
 * as in a real book. With no picture the layer is `none` and the paper shows
 * through opaque — the sheet must never be a window onto the live page. */
body.ms-skin .ms-leaf u{
  transform:rotateY(180deg);
  background:linear-gradient(95deg,#2E2867 0%,#241F52 58%,#1B1745 100%);
  box-shadow:inset -80px 0 90px -60px rgba(0,0,0,.6), 26px 0 54px rgba(0,0,0,.45);
}
@keyframes msLeaf{
  from{ transform:rotateY(0deg); }
  to  { transform:rotateY(-172deg); }
}
/* The landing dissolve the full-bleed page uses (see the block at the end of
 * this file); opaque paper for the whole sweep, gone by the time the sheet
 * would otherwise be yanked off the page it settled on. */
@keyframes msLeafLand{
  0%,60%{ opacity:1; }
  100%  { opacity:0; }
}
/* On the open-book spread (>=820px, story-together.html's Concept A) only the
 * RIGHT page turns. The full-spread leaf (inset:0) is the PHONE's: there the
 * whole column is one page, so the whole sheet lifting is a page. On the
 * spread the same sheet covered BOTH squares and pivoted on the book's outer
 * left edge - the entire book flipped shut like a folder. The sheet is now the
 * right half alone, so its left edge (transform-origin:left, perspective-
 * origin:0%) IS the spine at 50%, and the left square never moves.
 * The front face matches what that half shows - the TEXT page (the skin's
 * .left-page gradient, no art split): the picture lives on the square that
 * stays put, so a copy of it sweeping by would double it. */
@media (min-width:820px){
  body.ms-skin.play .ms-leaf{ inset:0 0 0 50%; }
  body.ms-skin.play .ms-leaf b{
    background:var(--ms-page-face);
    /* the spine shadow the real text page wears, so the lifting sheet is a
     * picture of the page it replaces down to the gutter shading */
    box-shadow:inset 22px 0 30px -22px rgba(0,0,0,.4), 26px 0 54px rgba(0,0,0,.45);
  }
  body.ms-skin.play .ms-leaf b span{ top:0; padding:44px 40px 0; }
  /* The verso: the page being turned TO, painted over the paper. Scoped to the
   * SPREAD, where the sheet is one square and lands on the picture's square —
   * the same box, so the same cover crop. On the phone column the sheet is the
   * whole frame and a square illustration stretched down it would be a
   * different, undesigned picture; there the back stays paper.
   * `none` when the caller has no picture to give (a live story's next one has
   * not been drawn yet), and the gradient beneath keeps the sheet opaque —
   * it must never be a window onto the live page. */
  body.ms-skin.play .ms-leaf u{
    background-color:#241F52;
    background-image:var(--ms-leaf-back-art,none),
                     linear-gradient(95deg,#2E2867 0%,#241F52 58%,#1B1745 100%);
    background-size:cover; background-position:center; background-repeat:no-repeat;
  }
  /* Past 90 degrees the sheet lies over the PICTURE half, so with no verso
   * picture to reveal its paper back is an opaque slab across the
   * illustration for the second half of the turn, still solid when the sheet
   * is yanked at 820ms (owner, 2026-08-14: "I see blue when passing pages").
   * Dissolve the back as it lands — the same landing fade the full-bleed
   * sheet already gets, on the back face alone so the front stays solid for
   * the whole lift. Excluded when the sheet HAS the next page's picture:
   * there the child is meant to watch it arrive, not fade.
   * [data-text-spot] is the full-bleed layout, whose own rule below fades the
   * whole leaf; fading its back too would just double the fade. */
  body.ms-skin.play:not([data-text-spot]) .ms-leaf:not([data-back]) u{
    animation:msLeafLand .82s linear forwards;
  }
}

/* The cut above reaches `.ms-leaf i` (the rotation) and the full-bleed leaf's
 * own fade; the spread's back-face dissolve is a third animation and needs its
 * own, at the same specificity and after it so source order decides. */
@media (prefers-reduced-motion: reduce){
  body.ms-skin.play:not([data-text-spot]) .ms-leaf:not([data-back]) u{
    animation-duration:.01s;
  }
}

/* ── Opening the book (the cover swings, it does not fade) ─────────────────
 * The right page wears .cover until the story starts. story-karaoke.js clones
 * the cover into this lid the moment that class comes off, so the page beneath
 * is already live while the cover swings away from the spine.                */
body.ms-skin .ms-cover-open{
  position:absolute; inset:0; z-index:30; pointer-events:none;
  perspective:2200px; perspective-origin:0% 50%;
}
body.ms-skin .ms-cover-open i{
  position:absolute; inset:0;
  transform-origin:left center;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:12px; text-align:center; padding:28px; color:#ffeccc;
  background:
    radial-gradient(120% 90% at 70% 15%,#5b3f77 0%,transparent 60%),
    linear-gradient(160deg,#2b2140 0%,#43305c 48%,#7a4a6b 100%);
  box-shadow:26px 0 60px rgba(0,0,0,.5);
  animation:msCover .9s cubic-bezier(.32,.04,.24,1) forwards;
}
@keyframes msCover{
  from{ transform:rotateY(0deg); opacity:1; }
  70% { opacity:1; }
  to  { transform:rotateY(-116deg); opacity:0; }
}

@media (prefers-reduced-motion: reduce){
  body.ms-skin .ms-leaf i,
  body.ms-skin .ms-cover-open i{ animation-duration:.01s; }
}

/* ── Full-bleed pages (tablet/desktop) ────────────────────────────────────
 * When a page reveals on a wide screen, body carries data-text-spot (the
 * region the illustration's PROMPT reserved as calm background) and the
 * picture becomes the page itself: landscape art spanning both halves, the
 * story text floating on the reserved region. Attribute-gated, so the
 * open-book spread stays exactly as it was for anything that never stamps
 * a spot (planning, Q&A recovery, a failed draw). Phone never stamps one.
 *
 * The art spans the spread; it does NOT replace the book (STU-87). This
 * block used to size .book to the viewport and switch off its radius,
 * shadow and spine, so a wide page was a full-screen picture and the
 * product's own metaphor — the child is making a real picture book — was
 * gone on the primary device, on exactly the pages that have a picture.
 * The frame is back and nothing inside it moved: the art fills the frame
 * instead of the viewport, and every child of .book (the text panel, the
 * pup, the meta pill, the turning sheet) is positioned against .book, so
 * they ride the frame at the offsets they already had.
 *
 * The page edge is a box-SHADOW ring, not padding. .book's border box is
 * the illustration's box, and the leaf is `inset:0 0 0 50%` of it — the
 * turn's crop, and check-story-mobile-layout.py's `leafW == bookW/2`
 * assertion, are both derived from that one box. Padding would inset the
 * abspos containing block by the edge width and leave bookW measuring the
 * edge too, shearing the crop off the picture it is supposed to be. */
@media (min-width:820px){
  body.ms-skin.play[data-text-spot]{
    padding:0; background:#000;
    /* Off the design frames (Figma "Smiti - Magic Story", 18-1558 and the
     * "Different Style" siblings), measured as a fraction of the 1366x1024
     * canvas and re-expressed in vmin: margin ~1.6%, page block ~0.75%, corner
     * ~2.0%. The clamps keep all of them sane between a 820px-wide tablet and a
     * large desktop, where a pure vmin would go spindly or cartoonish. */
    --ms-book-gap:clamp(12px, 2.2vmin, 26px);    /* black margin around the book */
    --ms-book-edge:clamp(5px, .9vmin, 10px);     /* flush cut edge, top and bottom */
    --ms-book-block:clamp(8px, 1.4vmin, 17px);   /* EXTRA depth down the sides */
    --ms-book-radius:clamp(20px, 3.4vmin, 38px);
    --ms-book-spine:clamp(22px, 3.4vmin, 46px);
  }
  body.ms-skin.play[data-text-spot] .book{
    width:calc(100vw - var(--ms-book-gap) * 2); max-width:none;
    height:calc(100vh - var(--ms-book-gap) * 2);
    height:calc(100dvh - var(--ms-book-gap) * 2);
    border-radius:var(--ms-book-radius);
    /* The edge is a STACK OF PAGES seen edge-on, and it is NOT uniform. A
     * book's leaves show deep down the outer vertical edges and are nearly
     * flush along the top and bottom, which is what the design frames draw; a
     * ring of one thickness put the same block on all four sides and read as a
     * photo mount (owner, 2026-08-16: "the pages seem to be all around and not
     * just on the sides").
     *
     * Per-side thickness out of box-shadow, which has only a uniform spread:
     * each layer is a PAIR of rings offset -D and +D. The left ring reaches
     * D+T past the left edge and T-D past the right; its mirror does the
     * reverse; together they lay down D+T on both sides and T top and bottom.
     * Three layers — the top sheet, the hairline of shadow it casts on the
     * next, and the block beneath — then the book's own shadow on the black.
     *
     * box-shadow rather than a ::before frame layer, deliberately: .book takes
     * overflow:hidden for the length of a page turn (the rule below), which
     * clips CHILDREN but never the element's own shadow. A pseudo-element
     * would blink the whole page block out for 820ms of every turn.
     * Rings paint first-listed on top, so these run inner to outer. */
    box-shadow:
      calc(var(--ms-book-block) * -.40) 0 0 calc(var(--ms-book-edge) * .50) #FFFFFF,
      calc(var(--ms-book-block) *  .40) 0 0 calc(var(--ms-book-edge) * .50) #FFFFFF,
      calc(var(--ms-book-block) * -.70) 0 0 calc(var(--ms-book-edge) * .74) rgba(105,92,70,.34),
      calc(var(--ms-book-block) *  .70) 0 0 calc(var(--ms-book-edge) * .74) rgba(105,92,70,.34),
      calc(var(--ms-book-block) * -1) 0 0 var(--ms-book-edge) #FBF7EE,
      calc(var(--ms-book-block) *  1) 0 0 var(--ms-book-edge) #FBF7EE,
      /* spread past the WIDEST part of the block (the sides), or the block
       * paints over the shadow everywhere except above and below */
      0 18px 44px calc(var(--ms-book-block) + var(--ms-book-edge) - 4px) rgba(0,0,0,.72);
    background:#FFFDF8;   /* no black hairline if the art rounds a pixel short */
  }
  /* The spine crease is back, and it is not the spread's 2px seam: that is a
   * hairline sized for two cream pages and it disappears over a picture. Here
   * it is the soft shading a real gutter puts on the paper either side of the
   * fold. z-index:1, not the seam's 3 — the text panel is z-index:2 and a
   * top/bottom spot centres it across the middle of the frame, so at 3 the
   * crease would paint down the middle of the words. */
  body.ms-skin.play[data-text-spot] .book::after{
    display:block; width:var(--ms-book-spine); transform:translateX(-50%);
    z-index:1; border-radius:0;
    background:linear-gradient(90deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.14) 32%, rgba(0,0,0,.32) 50%,
      rgba(0,0,0,.14) 68%, rgba(0,0,0,0) 100%);
  }
  body.ms-skin.play[data-text-spot] .right-page{
    position:absolute; inset:0; height:100%; width:100%;
    /* the art is the page, so it is cut to the page's corners — .right-page
     * already carries overflow:hidden from the base rule */
    aspect-ratio:auto; border-radius:var(--ms-book-radius); flex:none;
  }
  body.ms-skin.play[data-text-spot] .right-page img{
    width:100%; height:100%; object-fit:cover; align-self:auto;
  }
  /* the text page becomes a floating panel on the reserved region — no card
   * face: the art behind it IS background by construction, and the words
   * carry their own shadow */
  body.ms-skin.play[data-text-spot] .left-page{
    position:absolute; z-index:2; flex:none; height:auto; aspect-ratio:auto;
    background:none; box-shadow:none; border-radius:0; overflow:visible;
    padding:0; margin:0; width:min(42vw, 620px);
  }
  /* Centred WITHOUT transforms, deliberately: a transformed ancestor becomes
   * the containing block for position:fixed, so a translate here would drag
   * the fixed mic below to the panel's corner — the exact #629 bug. */
  body.ms-skin.play[data-text-spot="right"]  .left-page,
  body.ms-skin.play[data-text-spot="left"]   .left-page{
    top:0; bottom:0; height:fit-content; margin-block:auto;
  }
  body.ms-skin.play[data-text-spot="right"]  .left-page{ right:5vw; }
  body.ms-skin.play[data-text-spot="left"]   .left-page{ left:5vw; }
  body.ms-skin.play[data-text-spot="bottom"] .left-page,
  body.ms-skin.play[data-text-spot="top"]    .left-page{
    left:0; right:0; margin-inline:auto; width:min(72vw, 980px);
  }
  body.ms-skin.play[data-text-spot="bottom"] .left-page{ bottom:7vh; }
  body.ms-skin.play[data-text-spot="top"]    .left-page{ top:7vh; }
  /* Turning a page here is still a BOOK turn. The sheet is the RIGHT HALF of
   * the frame, hinged on the middle of the screen: its left edge (transform-
   * origin:left, perspective-origin:0%) is the spine, and the half the child
   * keeps looking at never moves. It was the whole frame (the phone's
   * geometry, inset:0), which pivoted on the viewport's OUTER edge - the
   * entire picture swinging shut like a door rather than a page turning at the
   * spine (owner, 2026-08-13: "it should look like an actual book folding in
   * the middle"). The value is the SPREAD's own (the rule above), restated so
   * this block owns the geometry its crop below is derived from - the bug was
   * this block silently overriding it, and a `--ms-leaf-art` crop that assumes
   * a half-width sheet must not be one edit away from a full-width one.
   *
   * It also DISSOLVES as it lands. On the spread the sheet comes to rest over
   * the art page, which the child has finished with; here the page it settles
   * on is the left half of the picture it just revealed, so holding a slab of
   * paper over half the new page until the sheet is yanked at 820ms read as a
   * pop. The fade starts once it is past upright (~490ms), so the whole first
   * half of the turn is opaque paper and only the landing softens.          */
  body.ms-skin.play[data-text-spot] .ms-leaf{
    inset:0 0 0 50%;
    animation:msLeafLand .82s linear forwards;
  }
  /* ...and the clip the turn lifts stays ON here. It has to come off on the
   * spread, or the sheet's far edge is sliced off mid-sweep and the paper reads
   * as flat - but that spread is a book the child sees the WHOLE of, sitting in
   * a 24px-padded body, and the sheet swings out over the paper around it. Here
   * the frame is the book, inset from the viewport by --ms-book-gap alone, and
   * with the clip off a perspective-projected sheet (taller than the frame it
   * pivots in) grew the DOCUMENT: 193px of scrollable overflow measured
   * mid-sweep, 675px before the sheet became a half. Clipping to the frame is
   * also what the frame MEANS once it is a book again - a page cannot swing
   * outside its own covers. Pinned by check-story-mobile-layout.py, which
   * measures scrollHeight mid-fold. */
  body.ms-skin.play[data-text-spot].ms-turning .book{ overflow:hidden; }
  /* ...and the sheet carries the PICTURE it is a page of. 200% wide, anchored
   * right, is exactly the box the full-bleed <img> is painted into, so the
   * same object-fit:cover crop lands on the leaf and `b`'s overflow:hidden
   * clips it to the half that lifts - the art on the sheet is the pixels that
   * were under it. Without this the front face was the bare page colour the
   * spread's TEXT page wears (--ms-page-face, from the block above) and the
   * turn read as "the screen goes blue, then folds". That colour stays as the
   * base underneath: with no outgoing picture (a page whose draw failed) the
   * sheet must still be opaque paper, not a window onto the live page.       */
  body.ms-skin.play[data-text-spot] .ms-leaf b::after{
    content:""; position:absolute; top:0; right:0; bottom:0; width:200%;
    background-image:var(--ms-leaf-art,none);
    background-size:cover; background-position:center;
  }
  /* ...cut to the page's corners on its OUTER edge, like the page under it.
   * The sheet at rest has to be invisible against that page (the pixel
   * assertion in check-story-mobile-layout.py), and now that the frame is
   * rounded again a square sheet paints art over the two corners where the
   * real page shows the black behind the book. The spine edge stays square:
   * that is the fold, not an edge of the book. `b` already has
   * overflow:hidden — it is what crops the 200% art layer to this half. */
  body.ms-skin.play[data-text-spot] .ms-leaf b{
    border-radius:0 var(--ms-book-radius) var(--ms-book-radius) 0;
  }
  /* The panel of words clears the sheet instead of being sliced by it. On a
   * top/bottom spot the panel is centred and min(72vw,980px) wide, so it
   * STRADDLES the spine: below the sheet (z-index 2 against 40) the new
   * sentence was cut in half down the middle of the screen for most of the
   * fold, and TEXT_SPOTS puts two of every four pages there. Raising it while
   * the page turns also keeps the mic — position:fixed inside this panel, so
   * trapped in its stacking context — on screen through the turn. Scoped to
   * .ms-turning: at rest the panel must stay under the full-screen layers
   * (the style picker at 40, the grown-up sheet at 50). */
  body.ms-skin.play[data-text-spot].ms-turning .left-page{ z-index:41; }
  /* The words are in a floating panel over the art here, not on the page's own
   * paper, so the leaf's copy of them painted as a bare unstyled band across
   * the top of the sheet. The picture carries the turn. */
  body.ms-skin.play[data-text-spot] .ms-leaf b span{ display:none; }
  /* the host stands clear in a corner, not over the words */
  body.ms-skin.play[data-text-spot] #pup{ left:24px; bottom:12px; top:auto; }
  /* the mic lives INSIDE .left-page, which is now the floating text panel —
   * absolute coords would put it at the panel's corner, riding wherever the
   * panel goes (macs-review #629). Fixed pins it to the VIEWPORT's corner. */
  body.ms-skin.play[data-text-spot] #mic-wrap{
    position:fixed; left:auto; right:5vw; top:auto; bottom:24px; transform:none;
  }
}
/* The reduced-motion cut above reaches `.ms-leaf i` — the element the rotation
 * lives on. The landing fade lives on `.ms-leaf` ITSELF, which that selector
 * never matched, so it needs its own cut. It has to sit here, after the rule
 * that sets the animation: the two selectors have the same specificity, so
 * source order is what decides. */
@media (prefers-reduced-motion: reduce){
  body.ms-skin.play[data-text-spot] .ms-leaf{ animation-duration:.01s; }
}

/* ── The closed cover: telling the pup, and waiting ───────────────────────
 * Design frame 9. Two screens are the child looking at a book that has not
 * been opened yet — the conversation about what the story is about, and the
 * wait while it is written (the one with the music bed) — so both wear the
 * same standing cover on the paw ground with the pup in front of it, rather
 * than the open two-page spread they inherited from the play shell.
 *
 * The cover IS .book, not a fourth sibling screen, for the reason the play
 * state gives for restyling .book too (story-together.html, screen 4): the
 * host chat, the status pill, the mic and the pup are already its children
 * and ride it without being reparented. The face is the design system's own
 * portrait-book face — the same three declarations #setup-screen's cover and
 * the keepsake cover wear — restated rather than added to that selector list
 * because those are content boxes sized by their own screens and this one is
 * sized by the viewport.
 *
 * Wide only, like the frames it comes from. The phone is a single column with
 * its own planning treatment — the night sky, which is itself the fix for a
 * blank pane during the wait and is pinned at 390px by
 * check-story-mobile-layout.py. Taking the sky off a screen this has no
 * design for would be trading one designed layout for no layout.           */
@media (min-width:820px){
  body.ms-skin.play.hosting,
  body.ms-skin.play.planning{
    /* the design's patterned field, which body.play paints over with the OPEN
     * book's white ground (and the full-bleed page with black) */
    background-color:var(--m-app);
    background-image:var(--ms-glow), var(--ms-paw);
    background-attachment:fixed, fixed;
    background-size:auto, cover;
    justify-content:center; align-items:center; padding:24px;
  }
  body.ms-skin.play.hosting .book,
  body.ms-skin.play.planning .book{
    flex-direction:column; justify-content:center; align-items:center;
    width:min(560px, 84vw); max-width:none; height:min(760px, 78vh);
    border-radius:26px; overflow:visible;
    background:linear-gradient(158deg, #2C2662 0%, #221D4E 55%, #1C1843 100%);
    box-shadow:inset 0 0 0 1px rgba(255,255,255,.16),
               inset 14px 0 0 -12px rgba(255,255,255,.22),
               0 26px 60px rgba(0,0,0,.5);
  }
  /* no spine seam: that is the OPEN book's gutter, and this book is shut */
  body.ms-skin.play.hosting .book::after,
  body.ms-skin.play.planning .book::after{ display:none; }
  /* The art pane stays in the layout but goes transparent and empty — every
   * picture layer is hidden and it keeps only the status pill, which is the
   * wait's whole feedback and happens to live in here. Dropping the pane
   * outright would have taken "Writing your page…" with it. */
  body.ms-skin.play.hosting .right-page,
  body.ms-skin.play.planning .right-page{
    position:absolute; inset:0; width:auto; height:auto; flex:none;
    aspect-ratio:auto; border-radius:0; background:none; pointer-events:none;
  }
  body.ms-skin.play.hosting .right-page > *:not(.loading),
  body.ms-skin.play.planning .right-page > *:not(.loading){ display:none; }
  /* ...and the two children whose own display rules carry an id, which the
   * class-only rule above loses to however many classes it stacks: the night
   * sky (`body.planning #planning-sky`) and the title art (`#cover-art`). Same
   * id count, more classes, so these win. The pane's remaining layers are
   * class-only and the rule above already has them — except #grownup-open,
   * which escapes on purpose (next rule). */
  body.ms-skin.play.planning #planning-sky,
  body.ms-skin.play.hosting #cover-art,
  body.ms-skin.play.planning #cover-art,
  /* no page dots on a book that has no pages yet */
  body.ms-skin.play.hosting #page-meta,
  body.ms-skin.play.planning #page-meta{ display:none; }
  /* The grown-up ··· entry's display rule also carries an id
   * (`body.play #grownup-open`), so it too escapes the class-only hide — and
   * should: the sheet it opens lives outside .book, and the entry was
   * reachable through onboarding on the old open spread. What it must not
   * inherit is the pane's pointer-events:none, which left it painted but
   * dead. */
  body.ms-skin.play.hosting #grownup-open,
  body.ms-skin.play.planning #grownup-open{ pointer-events:auto; }
  /* the conversation, and the wait, are written ON the cover */
  body.ms-skin.play.hosting .left-page,
  body.ms-skin.play.planning .left-page{
    position:static; flex:none; width:100%; height:100%; aspect-ratio:auto;
    background:none; box-shadow:none; border-radius:0; margin:0;
    padding:34px 30px 38px; overflow:auto; justify-content:center;
  }
  /* The pup stands in FRONT of the cover, at its foot. Centred with `left`
   * arithmetic rather than a translate: #pup is what the drag gesture moves,
   * and a transform here is a second one to fight with. */
  body.ms-skin.play.hosting #pup,
  body.ms-skin.play.planning #pup{
    left:calc(50% - 125px); bottom:-34px; top:auto;
  }
  /* The pill sits at the TOP of the cover frame, above the host text. Pinned
   * to the top rather than bottom:16% because the pup performs centre-stage at
   * the foot of the cover for the whole wait, and a bottom-anchored pill landed
   * squarely on him — the label read as "behind the character" (STU-99). The
   * top of the frame is the one spot on this cover the pup never reaches. */
  body.ms-skin.play.hosting .right-page .loading,
  body.ms-skin.play.planning .right-page .loading{
    left:50%; top:24px; bottom:auto; transform:translateX(-50%);
  }
  /* The pup's line is what the cover carries while the story is still being
   * decided — the design sets a title there, and until the child has said what
   * the story is about, the question IS the title. The storybook face and a
   * measure rather than the cover's uppercase display: that face is cut for
   * DANNY'S STORY, and a spoken sentence set 52px in caps is a poster, not a
   * question. */
  body.ms-skin.play.hosting #host-chat-page{
    height:100%; justify-content:center; align-items:center; gap:22px;
  }
  body.ms-skin.play.hosting #host-text{
    font-family:var(--ms-story); font-weight:500;
    font-size:clamp(19px, 2.5vmin, 26px); line-height:1.36;
    text-align:center; color:var(--m-ink); max-width:24ch; margin-inline:auto;
  }
  body.ms-skin.play.hosting #host-input-row{ width:84%; margin-inline:auto; }
}

/* ── Per-style story typography ───────────────────────────────────────────
 * A new feature from the "Layout and Typography Variation" spec: each art
 * style narrates in its own face, ink colour and panel treatment. The style the
 * child taps lands on body[data-art-style] (story-front.js), so one attribute
 * drives face, colour and panel together.
 *
 * The karaoke read is part of the spec, not a separate effect: the words already
 * spoken sit at the full ink colour and the rest at 20% OF THAT SAME COLOUR -
 * which is what "Dark Blue + 20% Black" and "…+ 20 % of the same color" mean. So
 * --ms-ink is the one value each style has to set.                          */
body.ms-skin #page-text{ color:var(--ms-ink,#fff); }
body.ms-skin #page-text .ms-word{
  color:color-mix(in srgb, var(--ms-ink,#fff) 20%, transparent);
  /* The dim ARRIVES; it is not the state a page opens in. Only meaningful on
   * the held page below (a new page's words are new elements, which do not
   * transition), where it turns the handoff from "the page is here" to "the
   * voice is reading it" into a fade rather than a flash. */
  transition:color .16s linear;
}
/* A page whose words are HELD — waiting for a narration that has not made a
 * sound yet — is a page the child is looking at, so it reads at full ink and
 * the 20% dim arrives with the voice it describes. Held at 20% instead, the
 * reader showed a nearly-blank page for the length of a /v1/tts round trip on
 * every single turn: the owner asked for the words not to MOVE before the
 * voice, which is a different thing from the page not being READABLE (brutus,
 * 2026-08-14). `inherit` rather than a colour of its own: each art style sets
 * #page-text's ink, and an unlit word before the voice is just page text. */
body.ms-skin #page-text[data-karaoke="held"] .ms-word{ color:inherit; }
body.ms-skin #page-text .ms-word.ms-lit{
  color:var(--ms-ink,#fff);
  filter:none;
  transition:color .16s linear;
}
/* The text panel sits over the illustration for every style except 3d, which
 * gives the text its own page. */
body.ms-skin[data-art-style] #page-text{
  border-radius:22px;
  padding:16px 18px;
  background:var(--ms-panel, transparent);
  box-shadow:var(--ms-panel-shadow, none);
  backdrop-filter:var(--ms-panel-blur, none);
  text-shadow:none;
}
/* No words, no paper. The style is chosen BEFORE the first page exists, so
 * this panel used to paint the moment data-art-style landed — a bare white
 * slab sitting through the whole planning wait and the first draw with
 * nothing in it (owner, 2026-08-13: "the white background for the text but
 * nothing appears ... feels very weird"). enterPlanning empties #page-text,
 * and the karaoke fills it with word spans, so :empty is exactly the "there
 * is nothing to read yet" state. */
body.ms-skin[data-art-style] #page-text:empty{
  background:none; box-shadow:none; backdrop-filter:none; padding:0;
}

/* Water color - white blurred rounded panel, dark blue ink, Mali. */
body.ms-skin[data-art-style="watercolor"] #page-text{
  font-family:"Mali",cursive,sans-serif; font-weight:600;
  --ms-ink:#241C86;
  /* The selection blend's second hue: the wash drifts --ms-ink -> this and back (see
     @keyframes ms-sel-blend in story-together.html). Chosen by measurement, not taste
     — this note is the canonical one; the other styles' hues point back here:
      - the floor is 3:1 at EVERY point along the oklab path, not just the endpoints
        (a blend spends its life between them; the 4.5-era cinematic3d hue bottomed
        at t=0.55). 3:1 and not 4.5:1 because fitNarration() clamps story text to
        24-46px — all WCAG large-scale text, whose AA floor IS 3:1 — and the owner
        ratified the large-text floor (2026-08-19, "loud") after 4.5:1 capped every
        blend under the 8.0/255 visibility bar (MIN_SELECTION_TINT);
      - within the floor, the hue that maximises travel (mix x mean channel distance),
        so selecting reads as a colour CHANGE rather than a flicker. The binding
        contrast point is the static end over a translucent tablet panel on dark art,
        so a louder second hue costs nothing further.
     indigo -> gold: worst 3.56:1 along the path, travel 33.5/255 (the 4.5-era pine
     hue managed 12.2). */
  --ms-sel-to:#BFA70A;
  /* .62 is right HERE, and the reason is the device rather than the number. Over the
     darkest art this game draws it puts UNSELECTED story text at 8.4:1, nearly twice
     the 4.5:1 floor, because white paper over near-black is the easiest pairing of
     the four. Selected it would be 3.58:1 and fail — but selection lives behind
     (hover:hover) and (pointer:fine), so a phone or tablet reader cannot produce it,
     and the raise that pays for it belongs in that query, not here.
     It was briefly .73 unconditionally, which charged every touch device 11 points of
     opacity for a highlight it cannot receive — in the same change whose whole point
     was letting the picture through. The raise now lives in the pointer block below.
     (An earlier version of this comment claimed .62 passes washed at 5.4:1. It does
     not; 5.4 is the .80 figure. The harness caught it the moment the raise came off,
     which is the argument for measuring rather than quoting yourself.) */
  --ms-panel:rgba(255,255,255,.62);
  --ms-panel-blur:blur(7px);
  --ms-panel-shadow:0 6px 22px rgba(0,0,0,.12);
}
/* Drawing (the ink-line style) - cream panel off the page's brightest colour,
 * sprayed edge, Pangolin. */
body.ms-skin[data-art-style="inkline"] #page-text{
  font-family:"Pangolin",cursive,sans-serif; font-weight:400;
  --ms-ink:#5B4A22;
  /* brown -> teal. Same measured constraints as watercolor's canonical note above:
     worst 3.70:1 along the path, travel 26.2/255 (the 4.5-era slate managed 7.8,
     under the visibility bar). */
  --ms-sel-to:#26BFBF;
  --ms-panel:rgba(253,248,228,.9);
  --ms-panel-shadow:0 0 0 7px rgba(253,248,228,.5), 0 8px 22px rgba(0,0,0,.14);
  border-radius:26px;
}
/* 3d - the text does NOT overlay the art: it gets its own page. */
body.ms-skin[data-art-style="cinematic3d"] #page-text{
  font-family:"Lora",Georgia,serif; font-weight:400;
  --ms-ink:#00479E;
  /* blue -> amber. Same measured constraints as watercolor's canonical note above:
     worst 3.73:1 along the path, travel 33.9/255 (the 4.5-era crimson managed 22.6). */
  --ms-sel-to:#BF9B0A;
  --ms-panel:#FBFAF7;
  --ms-panel-shadow:inset 0 0 0 1px rgba(0,0,0,.06);
  border-radius:10px;
}
/* Cut-paper - bright tint of the palette, noise and shadow, Atma. */
body.ms-skin[data-art-style="papercollage"] #page-text{
  font-family:"Atma",cursive,sans-serif; font-weight:500;
  --ms-ink:#3E5227;
  /* 16% where the others take 24%: pale green paper against mid-green ink has the
     least headroom of the four. The 16% ceiling was measured against the old 4.5:1
     floor (24% sat at 4.09:1 over dark art); under the ratified 3:1 large-text floor
     (watercolor's canonical note above) 24% would now pass, but the static wash's
     strength is #755's shipped decision and nobody asked to raise it — the blend
     below is what supplies the visibility here.

     green -> soft magenta. Same measured constraints as watercolor's note: worst
     3.84:1 along the path, travel 17.3/255. Under the 4.5-era floor this style could
     not blend at all — every safe hue travelled 3.6/255, under the 8.0 visibility
     bar — which is why it was the one static style until the floor decision. */
  --ms-sel-mix:16%;
  --ms-sel-to:#BF26BF;
  --ms-panel:rgba(214,230,183,.94);
  --ms-panel-shadow:0 6px 16px rgba(0,0,0,.2);
  border-radius:20px;
}
/* the paper "noise" the cut-paper style asks for */
body.ms-skin[data-art-style="papercollage"] #page-text::before{
  content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  opacity:.5; mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='90' height='90' filter='url(%23n)' opacity='.35'/%3E%3C/svg%3E");
}
body.ms-skin[data-art-style="papercollage"] #page-text{ position:relative; }

/* WIDE LAYOUTS: the text panel lets a little of the picture through (owner,
 * 2026-08-18, "so you can see a bit behind it").
 *
 * Per style, not one blanket alpha, because the four palettes have very different
 * headroom over a dark picture — see each rule for its measured number. A blur rides
 * along wherever transparency is introduced without one: a crisp illustration
 * reading through body text is the difference between "you can see a bit behind it"
 * and "the words are hard to read", and this is the surface a 5-year-old is being
 * read from.
 *
 * Two scoping facts worth stating precisely, because earlier versions of this comment
 * got both wrong:
 *  - min-width:820px is not "tablet only" — it is every wide viewport, desktop
 *    included. It also means a base iPad (768px portrait) and an iPad Mini (744px)
 *    are BELOW it and never see any of this; they render the phone column.
 *  - phones are NOT "left opaque". Only cinematic3d is opaque there. watercolor is
 *    .62 with a 7px blur and inkline is .90, both from their base rules, and they
 *    look that way on every viewport. What this block changes is the wide layout. */
@media (min-width:820px){
  /* .8 — the number the owner asked for — and it holds here, because on a tablet
     there is no selection wash to survive.
     That distinction is the whole reason this block was wrong for a while. The
     readability floor was measured with the 24% wash composited on top, which forced
     every alpha to .91; but the wash is scoped to (hover:hover) and (pointer:fine)
     in story-together.html, so a touch tablet cannot produce it. Applying a
     mouse-only worst case to a touch-only surface is how "add some opacity so you
     can see a bit behind it" turned into nine points of it.
     Measured over the darkest picture this game draws (near-black), UNSELECTED,
     which is the only state a tablet has:
        watercolor 8.4:1   cinematic3d 5.4:1   inkline 5.1:1   papercollage 4.2:1
     The first three clear 4.5:1 at .8 with room. papercollage does not, at any
     alpha below .83 — see its own rule below.
     Where a mouse DOES exist these go back up, in the pointer block after this one.
     scripts/check-story-mobile-layout.py --checks styles asserts both cases, over
     dark art rather than over white, which is what let the first .8 through
     unmeasured. */
  body.ms-skin.play[data-text-spot][data-art-style="cinematic3d"] #page-text{
    --ms-panel:rgba(251,250,247,.8);
    --ms-panel-blur:blur(6px);
  }
  body.ms-skin.play[data-text-spot][data-art-style="inkline"] #page-text{
    --ms-panel:rgba(253,248,228,.8);
    --ms-panel-blur:blur(5px);
  }
  /* papercollage gets .85, not .8. Pale green paper against mid-green ink is the
     tightest pair of the four: .83 is the lowest that clears 4.5:1 unselected, so .8
     would actually be unreadable (4.2:1) rather than merely subtle. .85 is the
     owner's ask honoured as closely as this palette allows, and it is a real change
     — the panel was fully opaque at .94 before, so this style showed nothing of the
     picture at all. */
  body.ms-skin.play[data-text-spot][data-art-style="papercollage"] #page-text{
    --ms-panel:rgba(214,230,183,.85);
    --ms-panel-blur:blur(5px);
  }
  /* watercolor is deliberately absent: its base is already .62, more open than the .8
     asked for here, and lowering it further goes milky over pale art. */
}

/* A MOUSE changes the arithmetic, because only a mouse can select. The wash puts the
   art style's own ink over the words at 24% (16% papercollage), and that costs about
   1.5 points of contrast — enough that .8 fails (cinematic3d 3.7:1, inkline 3.7:1).
   So on a pointer device the panels go back up to where selected text clears 4.5:1.
   This is the ONLY place an alpha answers to the wash, which is the correction: the
   floor now applies exactly where the thing it measures can happen. */
@media (min-width:820px) and (hover:hover) and (pointer:fine){
  body.ms-skin.play[data-text-spot][data-art-style="cinematic3d"] #page-text{
    --ms-panel:rgba(251,250,247,.91);
  }
  body.ms-skin.play[data-text-spot][data-art-style="inkline"] #page-text{
    --ms-panel:rgba(253,248,228,.91);
  }
  /* papercollage cannot clear the floor washed at ANY alpha below opaque — .94 gives
     4.59:1 only with its wash already turned down to 16% (--ms-sel-mix below). So on
     a pointer device it keeps the opaque panel and shows no picture; that is the
     honest cost of this palette, not something a point of alpha fixes. */
  body.ms-skin.play[data-text-spot][data-art-style="papercollage"] #page-text{
    --ms-panel:rgba(214,230,183,.94);
    --ms-panel-blur:none;
  }
  /* watercolor's raise, which is the one that was mis-scoped: washed at .62 selected
     text is 3.58:1, and .73 is the lowest alpha that clears 4.5:1 (4.63:1). Same
     eleven points as before — now paid only by the devices that can select. */
  body.ms-skin.play[data-text-spot][data-art-style="watercolor"] #page-text{
    --ms-panel:rgba(255,255,255,.73);
  }
}
