/* "Play a Story" branching-story engine styles (D-025 prototype).
   Kept in its own file rather than kf-games.css so the 11 released game pages
   do not pay for a prototype's bytes. Fold into kf-games.css at release.
   Tokens only: no magic colors.

   LAYOUT (Deniz review 2026-07-18, binding):
   - Rule 42: the surface spans the header's logo-to-icons width. That is
     var(--content-max) minus the header's own var(--space-4) side padding,
     which is exactly what .kfps-shell reproduces. No narrow column.
   - LANDSCAPE FIRST: on tablet and desktop the art cell is a landscape frame
     sitting BESIDE the text, so a screen is wide and short, not tall. The
     stacked portrait-ish arrangement is the PHONE variant only.
   - NO SCROLL TO READ: the card is height-budgeted against the viewport, so a
     screen's art + text + controls always fit. Screens are short by authoring;
     this is the belt to that braces. */

.kf-playstory {
  margin: var(--space-4) 0 0;
}

.kfps-stage {
  transition: opacity 0.18s ease;
}
.kfps-fading {
  opacity: 0;
  /* The outgoing screen must not be tappable while it fades out, or a child
     mashing the screen can advance twice on one beat. Pairs with the engine's
     `busy` flag. */
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .kfps-stage {
    transition: none;
  }
  .kfps-fading {
    opacity: 1;
  }
}

.kfps-card {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: var(--space-3);
  width: 100%;
  box-sizing: border-box;
  display: grid;
  gap: var(--space-3);
}
.kfps-card:focus {
  outline: none;
}
.kfps-card:focus-visible {
  outline: 3px solid var(--color-link);
  outline-offset: 2px;
}

/* ---------- art ---------- */

.kfps-art {
  margin: 0;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.kfps-art img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  /* Every plate is landscape 3:2, but the art cell's own ratio moves with the
     viewport height budget. `contain` keeps the whole frame visible at every
     size. That is deliberate, not lazy: the shot list requires all faces fully
     visible and never cropped, and `cover` would quietly crop them on short
     windows. */
  object-fit: contain;
  object-position: center;
}
.kfps-art-note {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin: var(--space-2) 0 0;
  text-align: center;
}

/* Honest "not drawn yet" panel. Deliberately reads as a production note, not
   as artwork: no illustration is faked here (D-019). */
.kfps-art--pending {
  border: 2px dashed var(--kf-lilac);
  background: var(--kf-lilac-soft);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  text-align: center;
  /* Matches the 3:2 shape of a real plate, so a screen does not jump size when
     its illustration lands. */
  aspect-ratio: 3 / 2;
  align-items: center;
  gap: var(--space-2);
}
.kfps-pending-tag {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--kf-lilac-deep);
  margin: 0;
}
.kfps-pending-shot {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1rem;
  color: var(--color-heading);
  margin: 0;
}
.kfps-pending-desc {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin: 0;
  max-width: 34ch;
}

/* ---------- story text ---------- */

.kfps-panel {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.kfps-text {
  min-height: 0;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* PARAGRAPH SEPARATION (Deniz review 2026-07-19: "sentences are running
     together"). This gap must beat the spacing between two lines INSIDE a
     paragraph, or a paragraph break reads as an ordinary line wrap and two
     paragraphs merge into one wall of text. It used to be --space-3 (1rem)
     while the text ran at 1.35rem x 1.65 leading, so lines inside a paragraph
     sat 2.23rem apart and the paragraph break was less than half that: the
     break was the tightest gap on the screen instead of the loosest.
     Expressed in em against the panel's own font size, it is always one clear
     blank line whatever the viewport does to the clamp. */
  font-size: clamp(1.1rem, 1.5vw, 1.35rem);
  gap: calc(var(--leading-body) * 1em);
  /* Safety net only. Screens are authored short enough that this never
     engages; it exists so an unexpected long screen degrades to a scrollable
     panel instead of pushing the controls off the viewport. */
  overflow-y: auto;
}

/* THE CENTRING ABOVE MUST BE `safe`, AND THIS IS NOT A STYLE PREFERENCE.
   `justify-content: center` on a flex column that overflows pushes the first
   lines ABOVE the scroll container's own start, and overflow cannot scroll
   backwards past its start edge, so the top of the text becomes physically
   unreachable: it is not clipped-but-scrollable, it is gone. Deniz hit exactly
   this on 2026-07-27 ("bazi sayfalarda scroll etmek gerekiyor en basini
   goremiyorum"), on screens whose text needed 315px in a 186px box.
   `safe center` centres while the text fits and falls back to start-aligned the
   moment it does not, so the child always sees the first word.
   Declared as its own rule rather than edited into the block above so that a
   browser without `safe` support keeps the plain `center` it already had. */
.kfps-text {
  justify-content: safe center;
}
.kfps-text p {
  font-size: inherit;
  line-height: var(--leading-body);
  color: var(--color-text);
  margin: 0;
  /* A readable measure inside a wide shell: the surface is wide, the line
     length is not. */
  max-width: 46ch;
}
/* All three of these are <p> inside .kfps-text, which sets `color` at 0-1-1.
   Written unqualified they lost the colour half of their own rule and rendered
   in plain body colour: the narrator's aside was never actually muted and the
   voiced line was never actually in heading purple. Found while adding
   `.kfps-say` on 2026-07-27; the other two had been wrong since #01 shipped.
   Qualifying with `p` fixes all three and changes nothing else. */
.kfps-text p.kfps-aside {
  font-style: italic;
  color: var(--color-text-muted);
  border-left: 3px solid var(--kf-lilac);
  padding-left: var(--space-3);
}
.kfps-text p.kfps-voiced {
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--color-heading);
}

/* SOMEBODY IS SPEAKING. Deniz's rule for every story on the site, 2026-07-27:
   dialogue gets its own line and its own colour so a child can see that it is
   speech. A pre-reader being read to cannot hear quotation marks, and an early
   reader cannot pick a spoken sentence out of a block of narration.

   Deep teal rather than the brand gold, because gold does not clear AA on the
   cream ground at body size. It is `--color-link-strong`, which is the darkened
   teal already used for links, and reusing it here is safe: story text contains
   no links, so there is nothing for it to be confused with.

   The indent is the second, non-colour cue. Colour alone would carry nothing
   for a colour-blind child or in high contrast mode, and the rule is that the
   line reads as speech, not that it is teal. */
/* `.kfps-text p` sets `color` at specificity 0-1-1, so a bare `.kfps-say`
   (0-1-0) loses and the line renders in ordinary body colour. Verified in the
   browser: the first attempt at this rule produced rgb(15,10,36) for both the
   dialogue and the narration, which is exactly no rule at all. Qualify it. */
.kfps-text p.kfps-say {
  padding-left: var(--space-3);
  border-left: 3px solid var(--kf-teal-soft);
}

/* THE COLOUR IS ON THE QUOTE, NOT THE PARAGRAPH. `"DOOOO," he sang.` is one sentence with two
   jobs: the quoted part is speech, `he sang` is narration. Colouring the paragraph made the
   attribution look spoken, which is precisely what the colour exists to distinguish.
   The left rule stays on the paragraph, so a colour-blind child (or high-contrast mode) still
   gets the "somebody is talking" signal without depending on hue at all. */
.kfps-text p.kfps-say .kfps-said {
  color: var(--color-link-strong);
}

/* Page number. Quiet by design: muted, small, tabular so the digits do not
   jitter as the number grows, and sitting with the controls rather than with
   the story text so it never competes with the line the child is reading. */
.kfps-page {
  margin: 0;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  text-align: right;
  flex: 0 0 auto;
}
.kfps-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.kfps-onboard {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: clamp(1.2rem, 2.2vw, 1.6rem);
  color: var(--color-heading);
  margin: 0;
}

.kfps-prompt {
  font-size: clamp(1.3rem, 2.4vw, 1.7rem);
  color: var(--color-heading);
  margin: 0;
}
.kfps-youchoose {
  font-size: 0.8rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--color-text-muted);
  margin: 0;
}

/* ---------- controls ---------- */

/* All three choices carry identical weight: the backfire is never styled as
   wrong (no red, no lock, no dimming). Elias binding. */
.kfps-choices {
  display: grid;
  gap: var(--space-3);
  width: 100%;
}
.kfps-choice {
  min-height: 4rem; /* 64px, per art spec */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  text-align: center;
  box-sizing: border-box;
  /* Tighter vertical padding than horizontal so the token fits INSIDE the
     existing 4rem minimum instead of pushing the button taller. Measured: the
     button stays exactly the height it was before the tokens landed, so the
     phone height budget is unchanged. */
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--kf-purple-deep);
  background: var(--kf-lilac-soft);
  border: 2px solid var(--kf-lilac);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background 0.25s ease, transform 0.25s ease;
}
.kfps-choice:hover {
  background: var(--color-surface);
  transform: translateY(-2px);
}

/* Illustrated choice token.
 *
 * EQUAL WEIGHT IS BINDING (Elias): one rule styles all three tokens at a
 * decision. There is no `.kfps-choice--backfire` and there must never be one:
 * no red, no X, no lock, no dimming, no desaturation, no reduced size on the
 * choice that comically backfires. If you are about to add a per-choice
 * modifier here, that is the defect.
 *
 * Stacked layout (phone and tablet) puts the token beside the label so the
 * button costs no extra height against Nova's height budget: 2.5rem of art
 * plus the existing padding still comes to the same 64px minimum.
 */
.kfps-choice-token {
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  display: block;
}
.kfps-choice-label {
  min-width: 0;
}

.kfps-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
}

/* Back keeps Next's size class (>=64px) and its affordance; only the colour
   weight differs, so Next still reads as the forward move. */
.kfps-back {
  min-height: 4rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  box-sizing: border-box;
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--kf-purple-deep);
  background: var(--kf-lilac-soft);
  border: 2px solid var(--kf-lilac);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background 0.25s ease, transform 0.25s ease;
}
.kfps-back:hover {
  background: var(--color-surface);
  transform: translateY(-2px);
}

/* Listen is .kf-speak (src/styles/speak.css). Everything it looks like -- pill,
   disc, glyph, hover, focus, reduced motion -- comes from there, and .kfps-listen
   survives as a POSITIONING HOOK ONLY. Adding a visual property back here is the
   bug: it is how one speaker became four.

   It used to carry this card's lilac chrome, and a 2px --kf-lilac border on the
   white card measures 2.33:1. The border is the only thing saying "tappable", so
   that was a live 1.4.11 failure. --speak-face is --color-link: 4.05:1. */
.kfps-listen {
  flex: 0 0 auto;
}
.kfps-back[hidden],
.kfps-listen[hidden] {
  display: none;
}

/* 4rem square, box-sizing included, so nothing here is smaller than Next or
   Back. It was 3rem plus 2px borders and measured 51px: the smallest thing on
   screen for the smallest hands.

   4rem is 68px on this site, NOT 64. The root font is 17px
   (--text-base: 1.0625rem on html in BaseLayout), which is also why the old
   3rem measured 51. Both numbers come off the same root, so a comment claiming
   3rem=51 and 4rem=64 could never have been right. Measured live: 68.
   Rule: px for anything that is a FLOOR, rem for type and scaling space.

   This is a BELL, not a speaker, so it keeps its own two-tone drawing and does
   NOT take .kf-speak. Only the boundary colour is shared: 2px --kf-lilac on
   white measured 2.33:1 and the border is the only thing saying "tappable"
   (1.4.11). --color-link is 4.05:1 and matches the Listen pill beside it. */
.kfps-mute {
  width: 4rem;
  height: 4rem;
  box-sizing: border-box;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--color-link);
  background: var(--kf-lilac-soft);
  border-radius: 50%;
  cursor: pointer;
  transition: background 0.25s ease;
}
.kfps-mute:hover {
  background: var(--color-surface);
}

.kfps-cta {
  min-height: 4rem;
  min-width: 12rem;
  margin-left: auto;
  box-sizing: border-box;
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.2rem;
  color: var(--kf-navy-dark);
  background: var(--kf-gold);
  border: 2px solid var(--kf-gold);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.kfps-cta:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lift);
}

/* .kfps-listen is NOT in this list: it is .kf-speak now and inherits the global
   :focus-visible from tokens.css. Re-declaring it here is what made this page's
   focus ring differ from the rest of the site. */
.kfps-choice:focus-visible,
.kfps-cta:focus-visible,
.kfps-back:focus-visible,
.kfps-mute:focus-visible {
  outline: 3px solid var(--color-link);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  /* .kfps-listen omitted on purpose: speak.css carries its reduced-motion
     behaviour, including the static halo that replaces the pulse. */
  .kfps-choice,
  .kfps-cta,
  .kfps-back,
  .kfps-mute {
    transition: none;
  }
  .kfps-choice:hover,
  .kfps-cta:hover,
  .kfps-back:hover {
    transform: none;
  }
}

/* ---------- phone: stacked, portrait-friendly art ---------- */

@media (max-width: 47.99rem) {
  /* Same no-scroll contract as desktop, just stacked. The card is height
     budgeted against the viewport and the ART is the flexible element: text
     and the >=64px controls keep their size, the picture gives up space.
     Measured on the worst real case, 375x667 (iPhone SE), where a decision
     screen carries three 64px choices. */
  .kfps-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-2);
  /* Height comes from the shell's flex column now, not from a guessed constant. See
     "HEIGHT BUDGET, REBUILT" at the end of this file. */
  }
  .kfps-art {
    flex: 1 1 auto;
    min-height: 0;
  }
  .kfps-art img,
  .kfps-art--pending {
    height: 100%;
    min-height: 0;
  }
  .kfps-art--pending {
    aspect-ratio: auto;
    padding: var(--space-2);
  }
  /* On a phone the shot note is production scaffolding, not story: let it
     shrink out of the way rather than push the story text off screen. */
  .kfps-pending-desc {
    display: none;
  }
  .kfps-art-note {
    display: none;
  }
  .kfps-panel {
    flex: 0 0 auto;
    gap: var(--space-2);
  }
  .kfps-text {
    /* Phone type size. The gap stays the em-based one blank line from the base
       rule and is deliberately NOT tightened to --space-2 here: on the smallest
       screen, where the most text wraps, collapsing the paragraph break was
       what made the lines run together worst. */
    font-size: 1.05rem;
  }
  .kfps-text p {
    max-width: none;
  }
  .kfps-prompt {
    font-size: 1.15rem;
  }
  .kfps-youchoose {
    font-size: 0.7rem;
  }
  .kfps-choices {
    grid-template-columns: 1fr;
    gap: var(--space-2);
  }
  .kfps-controls {
    gap: var(--space-2);
  }
  .kfps-cta {
    margin-left: auto;
    min-width: 0;
    flex: 1 1 auto;
  }
  /* .kfps-listen dropped: its padding is asymmetric by design (disc left,
     label right) and belongs to .kf-speak, which also takes it full-width
     under 30rem. Overriding padding here would flatten the pill. */
  .kfps-back {
    padding: var(--space-3);
  }
}

/* ---------- tablet and desktop: LANDSCAPE, art beside text ---------- */

@media (min-width: 48rem) {
  .kfps-card {
    /* Art gets the larger share; the text column keeps a comfortable measure. */
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    align-items: stretch;
    padding: var(--space-4);
    gap: var(--space-4);
    /* Height budget: the whole screen fits between the sticky header and the
       bottom of the viewport, so no child scrolls to finish a screen. Clamped
       so a very short window still gets a usable frame and a very tall one
       does not stretch the art absurdly. */
  /* Height comes from the shell's flex column now, not from a guessed constant. See
     "HEIGHT BUDGET, REBUILT" at the end of this file. */
  }
  /* THE CONTROLS MUST FIT ON ONE ROW HERE, AND IT IS A READABILITY FIX, NOT A
     COSMETIC ONE.
     Measured at 1280x720 before this rule: mute 68 + Back 122 + Listen 157 came
     to 381px in a 529px column, and Next's 12rem min-width (204px) could not
     follow them, so Next wrapped to a second row. The controls block then stood
     170px tall instead of 68px and ate 102px straight out of the text area,
     which was left with 186px. That is about six lines, and it is why screens
     were being clipped: the story was not too long, the buttons were too wide.
     Next keeps its gold weight, its 4rem height and its position at the end of
     the row, so it still reads as the forward move; it just stops demanding a
     row of its own on a column that cannot spare one. */
  .kfps-controls {
    flex-wrap: nowrap;
    gap: var(--space-2);
  }
  .kfps-cta {
    min-width: 8rem;
    padding: var(--space-3);
  }
  /* .kfps-listen dropped: padding belongs to .kf-speak (see above). */
  .kfps-back {
    padding: var(--space-3) var(--space-3);
  }
  .kfps-art {
    height: 100%;
  }
  .kfps-art img {
    height: 100%;
    min-height: 0;
  }
  /* Landscape frame, matching the 3:2 plates it stands in for. */
  .kfps-art--pending {
    aspect-ratio: auto;
    height: 100%;
    justify-content: center;
  }
  .kfps-choices {
    grid-template-columns: 1fr;
  }
  /* The start card has no art-heavy text; let it breathe a little shorter. */
  .kfps-card--start {
  /* Height comes from the shell's flex column now, not from a guessed constant. See
     "HEIGHT BUDGET, REBUILT" at the end of this file. */
  }
}

/* Wide desktop: three choices side by side reads faster than a stack. */
@media (min-width: 68rem) {
  .kfps-choices {
    grid-template-columns: repeat(3, 1fr);
  }
  .kfps-choice {
    font-size: 1rem;
    /* Three equal columns: the token sits ABOVE its word, so all three read as
       one row of equally sized pictures with equally sized words under them. */
    flex-direction: column;
    gap: var(--space-2);
  }
  .kfps-choice-token {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
  }
}

/* ================= PLAY LAYER =================
   Elias vetted, 2026-07-19. Everything in this block obeys the same rules the
   engine does: nothing here is a counter, a bar, a percentage, a visible timer
   or a countdown; nothing pulses faster than 2s (no flashing, project red
   line); and every animation collapses to a static end state under
   prefers-reduced-motion, so the reduced-motion experience is the same
   experience, just still. */

/* The art frame is the positioning context for the veils, the breathing ring
   and the crown. It is the only element they may overlay: the controls, and
   Next in particular, are OUTSIDE it and are never covered by anything in this
   layer. */
.kfps-art {
  position: relative;
}

/* ---------- removed: the ambient spark thread and its tray ---------- */

/* .kfps-sparks, .kfps-spark, .kfps-spark--gone, kfps-spark-breathe, .kfps-tray,
   .kfps-tray-glow, .kfps-tray--bump and kfps-tray-bump used to sit here.

   Deniz removed the whole collectable layer on 2026-08-10: "Play a Story'deki
   tum yildiz toplama fonksiyonunu kaldir." Nothing on the art is tappable any
   more, and nothing glows to say how much was gathered. If a rule for any of
   those class names reappears in this file, something is trying to bring the
   mechanic back. */

/* ---------- invitation lines ---------- */

/* Sits with the story text, in the story's own voice and size. The asking
   state gets a slow warm glow so the pause is VISIBLE, which is what tells a
   child there is room to join in. It is not a progress indicator: it shows no
   elapsed fraction, no remaining time and no steps, because a child who needs
   longer must never watch themselves run out of it. */
.kfps-invite {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--color-heading);
  max-width: 46ch;
}
.kfps-invite--asking {
  animation: kfps-invite-breathe 2.4s ease-in-out infinite;
}
.kfps-invite--on {
  color: var(--color-text);
  font-family: inherit;
  font-weight: inherit;
}
@keyframes kfps-invite-breathe {
  0%, 100% { opacity: 0.62; }
  50%      { opacity: 1; }
}

/* ---------- stay still (NODE B) ---------- */

/* A soft settling of the frame while everyone is being still.
   pointer-events: none is load bearing: a tap during this window must reach
   nothing and do nothing, positive OR negative. There is no handler on this
   element and there must never be one. */
.kfps-still {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: var(--radius-md);
  background: radial-gradient(
    ellipse at center,
    rgba(45, 27, 78, 0) 45%,
    rgba(45, 27, 78, 0.28) 100%
  );
  animation: kfps-still-settle 3.5s ease-in-out forwards;
}
@keyframes kfps-still-settle {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ---------- peekaboo ---------- */

/* A plain scrim, never a drawn face or drawn paws: no code-drawn stand-in for
   illustration on this page (D-019/D-021). It hides the plate for a moment and
   opens on a tap or by itself after 3.5s, whichever comes first. It covers the
   art only, so Next stays reachable throughout. */
.kfps-peek {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
  background: var(--kf-lilac-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.4s ease;
}
.kfps-peek--open {
  opacity: 0;
  pointer-events: none;
}
.kfps-peek-btn {
  min-width: 8rem;
  min-height: 4.75rem; /* 76px, same floor as the sparks */
  box-sizing: border-box;
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.2rem;
  color: var(--kf-navy-dark);
  background: var(--kf-gold);
  border: 2px solid var(--kf-gold);
  border-radius: var(--radius-pill);
  cursor: pointer;
}
.kfps-peek-btn:focus-visible {
  outline: 3px solid var(--color-link);
  outline-offset: 3px;
}

/* ---------- the crown ---------- */

/* A soft shimmer over the plate that already paints the crown. Nothing is drawn
   here and nothing is conditional: the engine calls this with no arguments. */
.kfps-crown {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: var(--radius-md);
  background: radial-gradient(
    circle at 50% 45%,
    rgba(212, 168, 67, 0.5) 0%,
    rgba(212, 168, 67, 0) 62%
  );
  animation: kfps-crown-rise 2.6s ease-out forwards;
}
@keyframes kfps-crown-rise {
  0%   { opacity: 0; transform: scale(0.94); }
  35%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0.28; transform: scale(1.04); }
}

/* ---------- breathe with Petal (story #02) ---------- */

/* A soft ring of light over the art that swells and settles at a breathing
   pace, so a child who wants to join in has something to follow.

   IT IS A METRONOME, NOT A METER. It never fills, never empties, never closes
   and never shows time running out: it just breathes, on a loop, until the
   screen changes. A child who needs longer must never be able to watch
   themselves run out of time, so there is deliberately no progress to read off
   it and nothing counts the cycles.

   Slow on purpose. 4s in, 4s out is a calm preschool breath and is also well
   under the flashing threshold; nothing on this page cycles faster than 2s
   (epilepsy red line). It never eats a tap, so Next stays live underneath. */
.kfps-breathe {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 34%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(
    circle at center,
    rgba(212, 168, 67, 0.32) 0%,
    rgba(212, 168, 67, 0.16) 55%,
    rgba(212, 168, 67, 0) 72%
  );
  animation: kfps-breathe-cycle 8s ease-in-out infinite;
}
@keyframes kfps-breathe-cycle {
  0%,
  100% {
    transform: translate(-50%, -50%) scale(0.72);
    opacity: 0.55;
  }
  50% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
  }
}

/* ---------- reduced motion ---------- */

/* Same experience, held still. Every element above keeps its FINAL, readable
   state: the invitation is legible at full opacity, the veils resolve
   instantly. Nothing is removed and nothing becomes unavailable. */
@media (prefers-reduced-motion: reduce) {
  .kfps-invite--asking,
  .kfps-still,
  .kfps-crown,
  .kfps-breathe {
    animation: none;
  }
  /* Held open and calm rather than removed: the screen still reads as a
     breathing moment, it simply does not move. */
  .kfps-breathe {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
  }
  .kfps-invite--asking {
    opacity: 1;
  }
  .kfps-peek {
    transition: none;
  }
  .kfps-crown {
    opacity: 0.32;
  }
}

/* ---------- start card intro ---------- */

/* Milo's four lines. The first three read as body copy and the last carries the
   emphasis, so the line that tells a parent their child chooses is the one the
   eye lands on. */
.kfps-intro {
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin: 0;
  max-width: 46ch;
}
.kfps-text--start {
  gap: calc(var(--leading-body) * 0.6em);
}

/* START CARD HEIGHT BUDGET, revisited when the cover landed.
 *
 * The start card used to carry no art, so the phone rule "text and controls
 * keep their size, the ART gives up space" cost nothing there. With a cover
 * plate and a four line intro it cost everything: measured at 375x667 the art
 * cell was squeezed to 137px, and on a 300px-tall window it collapsed to
 * ZERO and the cover simply did not render.
 *
 * So the art gets a floor here, and the intro gives up a little size instead.
 * Start stays above the fold, which matters more than a large cover: a child
 * who cannot reach the button has no story at all. */
@media (max-width: 47.99rem) {
  .kfps-card--start .kfps-art {
    min-height: 8.5rem;
  }
  .kfps-text--start .kfps-intro {
    font-size: 0.98rem;
  }
  .kfps-text--start .kfps-onboard {
    font-size: 1.15rem;
  }
}

/* ============================================================================
   HEIGHT BUDGET, REBUILT (Nova, 2026-08-01). Replaces the three clamp() card
   heights that were deleted above.

   WHY THEY WERE WRONG, IN ONE SENTENCE: they subtracted a GUESSED constant from
   the viewport and then floored the result with a rem length. The guess was
   7rem (119px) against a real 281px of chrome on #02 and 309px on #01, and one
   constant cannot serve two pages whose titles are different lengths. The rem
   floor is the second half of the bug: a rem floor inside a viewport budget can
   exceed the viewport, and at 200% text size 21rem is 714px, which is how the
   card grew past an 800px-tall screen and pushed all three choice labels out of
   sight.

   THE RULE THIS ESTABLISHES: rem bounds CONTENT (font sizes, target floors);
   viewport units bound WINDOWS. A rem length may never be the lower bound of a
   viewport-derived height. The audit found that trap in exactly three places
   and all three are the lines that were removed.

   THE MECHANISM: the page shell becomes a flex column the height of the window.
   The exit row and the title block take their natural size; the story stage
   takes everything left over. Nothing is guessed, so nothing drifts when a
   title wraps, a language changes, or a user enlarges text. Measured proof: the
   same CSS settles the chrome above the card at 195px on BOTH stories, which
   used to measure 281 and 309.
   ============================================================================ */

/* ---------------- phone: stacked ---------------- */

@media (max-width: 47.99rem) {

  /* THE SHELL IS THE MEASURING DEVICE.
     `main.ps:has(> .kf-playstory)` is 0-2-1, which beats Astro's scoped
     `.ps[data-astro-cid-...]` at 0-2-0 without depending on file order, and it
     scopes the rule to the two story pages rather than to `.ps` generally.
     4rem is the sticky site header, measured at 66.9px on phone. */
  main.ps:has(> .kf-playstory) {
    box-sizing: border-box;
    /* min-height, NOT height: content that needs more room than the window grows
       the PAGE (which scrolls) instead of squeezing the card and clipping a line.
       Fixes text clipping on iPhone-SE-class phones, 375x667 / 360x640 (Sage,
       2026-08-02). */
    min-height: calc(100svh - 4rem);
    display: flex;
    flex-direction: column;
    /* Side padding drops to --space-3, which is what the header's own inner
       uses. The alignment rule was quietly broken on phone: the header logo sat
       at x=17 and the card at x=25.5. Now both are 17, which also returns 17px
       of width to the picture and the text. */
    /* Bottom padding trimmed and the stage's top margin removed: measured, Next sat 11px below
       the fold at 360x640 and these are the cheapest pixels on the page. Deniz's rule is
       absolute: a child who cannot reach the button has no story at all. */
    padding: var(--space-2) var(--space-3) var(--space-1);
  }
  main.ps .game-exit-row { flex: 0 0 auto; margin-bottom: var(--space-1); }
  main.ps .ps-head       { flex: 0 0 auto; }
  main.ps .ps-head .kicker { font-size: 0.75rem; margin: 0 0 var(--space-1); }
  /* The h1 KEEPS its size and stays in the document: the page still has a name
     for a screen reader and for search. Only the tagline goes, and only under
     48rem, where it costs 36.5px of a 640px screen to repeat what the cover
     card says better and with a picture. */
  main.ps .ps-head .ps-sub { display: none; }

  main.ps > .kf-playstory {
    margin-top: 0;
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
  }
  main.ps .kfps-stage {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
  }

  /* THE CARD IS SIZED BY WHAT IS IN IT. Not by the window.
     Deniz, 2026-08-01: the text may never scroll AND the picture may never
     shrink to make room. Both are the product, so neither can be the thing
     that yields. What yields is the PAGE: if a window is too short to hold a
     screen, the page scrolls, which is an ordinary thing a reader does with a
     page and is not the same as losing the end of a sentence inside a box. */
  /* max-height removed: on a short phone the card must be free to grow past the
     window so the PAGE scrolls, rather than capping and clipping the words. */
  .kfps-card { height: auto; flex: 1 1 auto; min-height: 0; max-height: none; }

  /* THE PICTURE GETS A FLOOR AND A CEILING.
     Floor: it used to be the only flexible element with no minimum, so it
     collapsed to 0 on 8 screens and to 11px on another. The rule that art is
     half the experience was already broken on phone before anybody looked at
     the button. Ceiling: the plates are 3:2, so beyond (width / 1.5) the cell
     only adds empty space around a picture that has stopped growing. The svh
     term shrinks the picture on a SHORT phone rather than on a narrow one,
     which is where the pressure actually is. Measured: 206px on a 360x800 (the
     plate at its natural size, no letterbox), 130px on a 360x640. */
  /* The plate keeps its 3:2 shape at the full width it is given. It is not a
     spare tyre for the layout: it had been collapsing to 0 on 8 screens and to
     11px on another, which is how "art is half the experience" got quietly
     broken long before anybody looked at the button. */
  .kfps-card .kfps-art {
    flex: 1 1 auto;
    min-height: 6rem;
    max-height: min(calc((100vw - 3rem) / 1.5), calc(100svh - 30rem));
  }
  /* TEXT NEVER SCROLLS. ART YIELDS.
     Deniz's rule is "no scrolling to read", and it is not a budget to trade
     against. The first version of this block let the text box flex down to
     min-height:0 while the picture kept its share, so on a 742px window 35 of
     59 screens put a scrollbar next to the story and cut the last line off.
     Inverting it is the whole fix: the panel and the text take exactly the
     height their words need, and the picture absorbs whatever is left, down to
     its own 6rem floor. If a screen still cannot fit after the picture has
     given everything, the CARD grows and the PAGE scrolls, which is a
     different thing from a child losing the end of a sentence inside a box. */
  /* flex 0 0 auto, NOT 1 1: the words take exactly the height they need and never
     shrink below it, so the last line is never cut. The plate (flex 1 1 above)
     absorbs the slack down to its 6rem floor; past that the card grows and the
     PAGE scrolls. This is what the "TEXT NEVER SCROLLS. ART YIELDS." note wanted;
     the 1 1 here quietly let the text box shrink and clip (Sage, 2026-08-02). */
  .kfps-card .kfps-panel { flex: 0 0 auto; min-height: 0; }
  .kfps-card .kfps-text  { flex: 0 0 auto; min-height: 0; gap: 1em; }

  /* TOOLBAR: three equal discs, then ONE full-width primary.
     The controls used to wrap into three rows (68 + 64 + 71 = 220px) because
     speak.css takes .kf-speak to width:100% under 30rem. Worse than the height:
     Listen was a 292x64 white pill sitting ABOVE Next, so the first thing a
     child's eye landed on was not the primary action. Now the three secondary
     controls are 68px discs matching the mute bell, one icon register, and Next
     is the full-width gold bar at the bottom of the card, in the thumb zone.
     Measured: 220px down to 147.5px. */
  .kfps-controls .kf-speak.kfps-listen {
    position: relative;
    width: 4rem; height: 4rem; min-height: 4rem;
    padding: 0; justify-content: center; flex: 0 0 auto;
  }
  .kfps-card .kfps-back {
    position: relative;
    width: 4rem; height: 4rem; min-height: 4rem;
    padding: 0; gap: 0; flex: 0 0 auto; border-radius: 50%;
  }
  /* Visually hidden, NOT removed: Back, Listen and the page number stay in the
     accessibility tree as the buttons' accessible names. */
  .kfps-controls .kf-speak.kfps-listen > span:not(.kf-speak-ico),
  .kfps-card .kfps-back > span,
  .kfps-panel > .kfps-page {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .kfps-card .kfps-cta { flex: 1 0 100%; margin-left: 0; min-width: 0; }

  /* DECISION CARD: the three choices ARE the action, so they are never the
     thing that scrolls away. The card is sized by its CONTENT here, not by the
     window, and the page scrolls if the window cannot hold it. This is the
     opposite policy to a scene card on purpose: a scene has ONE action (Next)
     that must stay reachable, so its text yields; a decision has THREE that
     must be readable together, and you cannot scroll to one of three. */
  .kfps-card:has(.kfps-choices) { flex: 0 0 auto; height: auto; max-height: none; }
  .kfps-card .kfps-text--decision { overflow: visible; }
  .kfps-card:has(.kfps-choices) .kfps-art { max-height: 20%; min-height: 4rem; }
  /* Three across, token above the word: the same composition the 68rem rule
     already uses, so a decision reads the same at every size. Stacked, the
     three 64px buttons cost 221px and pushed the question itself out of the box
     (measured: the prompt collapsed from 47px to 0). Across, they cost 135px. */
  .kfps-card .kfps-choices { grid-template-columns: repeat(3, 1fr); gap: var(--space-2); }
  .kfps-card .kfps-choice {
    flex-direction: column; gap: var(--space-1);
    font-size: 0.95rem; padding: var(--space-2) var(--space-1);
  }
  .kfps-card .kfps-choice-token { width: 3.5rem; height: 3.5rem; }
}

/* ---------------- tablet and desktop: landscape ---------------- */

@media (min-width: 48rem) {
  main.ps:has(> .kf-playstory) {
    box-sizing: border-box;
    /* WIDTH IS LOCKED, NOT LEFT TO THE CONTENT. main is a flex item in the body's
       column, and its `margin: 0 auto` (the shell centring) cancels the flex
       stretch, so without this the whole column shrink-wraps to whatever the card
       holds: a wide plate made a wide page and a narrow pending box a narrow one,
       so the page changed width screen to screen (Deniz, 2026-08-01: "her sayfa
       standart olacak"). width:100% fills the shell up to its max-width again. */
    width: 100%;
    /* min-height, NOT height: a page with extra content below the story (the TR
       #01 page has a "Büyükler için not" .ps-refs section the EN pages lack) must
       grow and scroll, not force the story stage to shrink and cover the note
       (Sage, 2026-08-02). EN pages, with nothing after the story, still settle at
       one window height, so the scene card stays uniform. */
    min-height: calc(100svh - 5rem);
    display: flex; flex-direction: column;
  }
  main.ps .game-exit-row, main.ps .ps-head { flex: 0 0 auto; }
  main.ps > .kf-playstory {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
  }
  main.ps .kfps-stage {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
  }

  /* THE WORDS DECIDE THE HEIGHT. THE PICTURE KEEPS ITS SIZE.
     Deniz, 2026-08-01: the text may never scroll, AND the picture may never
     yield. Side by side those two do not actually conflict, and the old code
     invented a conflict: the card was capped against the window, the picture
     filled that height, and the text was squeezed into whatever was left, so
     five-block screens grew a scrollbar and lost their last line.
     Now the card is as tall as its TALLEST COLUMN. The plate sits at its
     natural 3:2 size and stops. If the text needs more room than the plate is
     tall, the card grows and the text gets it. Nothing is cut, nothing scrolls,
     nothing shrinks. */
  .kfps-card,
  .kfps-card--start { height: auto; flex: 0 0 auto; min-height: 0; max-height: none; }
  .kfps-card .kfps-panel { min-height: 0; flex: 1 1 auto; }
  .kfps-card .kfps-text  { min-height: 0; flex: 0 0 auto; overflow: visible; }
  /* The plate holds its shape and its place at the top of its column: not
     stretched to match a taller text column, not squashed by one. */
  .kfps-card .kfps-art   { flex: 0 0 auto; align-self: flex-start; }

  /* EVERY SCENE PAGE IS THE SAME HEIGHT (Deniz, 2026-08-01: "her sayfada boy
     değişemez ... her sayfa standart olacak"), and the placeholder page is exactly
     as tall as the picture page.
     The card FILLS the story stage, which the shell above sized to the window, so
     the height comes from the PAGE, not from what each screen happens to hold: a
     two-line screen, a full-plate screen and an "illustration coming" screen all
     come out identical. The picture (or the pending note) fills its half and is
     contained, never cropped; short text centres in the calm space beside it.
     A min-height floor keeps a short window from squashing the card below the
     words: past that floor the PAGE scrolls (Nova's rule) instead of a line being
     lost. The start card and the decision card keep their own sizing. */
  .kfps-card:not(.kfps-card--start):not(:has(.kfps-choices)) {
    flex: 1 1 auto;
    height: auto;
    min-height: 26rem;
    max-height: 34rem;
    align-items: stretch;
  }
  .kfps-card:not(.kfps-card--start):not(:has(.kfps-choices)) .kfps-art {
    align-self: stretch; aspect-ratio: auto; height: 100%; min-height: 0;
    overflow: hidden;
  }
  .kfps-card:not(.kfps-card--start):not(:has(.kfps-choices)) .kfps-art img {
    width: 100%; height: 100%; object-fit: contain;
  }
  .kfps-card:not(.kfps-card--start):not(:has(.kfps-choices)) .kfps-art--pending {
    aspect-ratio: auto; height: 100%; width: 100%; min-height: 0;
  }
  .kfps-card:not(.kfps-card--start):not(:has(.kfps-choices)) .kfps-panel {
    align-self: stretch; min-height: 0;
  }
  .kfps-card:not(.kfps-card--start):not(:has(.kfps-choices)) .kfps-text {
    flex: 1 1 auto; justify-content: safe center;
  }

  .kfps-card:has(.kfps-choices) { flex: 0 0 auto; height: auto; max-height: none; }
  .kfps-card .kfps-text--decision { overflow: visible; }
}

/* A 170px square picture for a three-word label is out of proportion, and on a
   short desktop window it was what pushed the choices out of their box. */
@media (min-width: 68rem) {
  .kfps-choice-token { max-width: 4.5rem; margin-inline: auto; }
}

/* ---------------- full-screen title strip ---------------- */

/* Only ever visible in full screen: on the page itself the real <h1> is right
   there above the stage, and two headings for one story is one too many. */
.kfps-fshead {
  display: none;
}

.kf-playstory.kf-fs-on > .kfps-fshead {
  display: block;
  width: 100%;
  max-width: var(--kf-fs-maxw, none);
  /* Air above the strip, and air between the strip and the story card. First
     pass was too tight and Deniz sent it back: "baslik ile hikaye arasina biraz
     daha bosluk ver ve baslik bloguna da ustten padding margin ver ki tepeye
     yapisik durmasin". Roughly doubled at the top.
     The gap under it is BOTH padding and a margin on purpose: the card is
     centred with auto margins, and auto margins collapse to zero on a screen
     with no room to spare - the margin here is the part that survives that, so
     the title can never end up welded to the picture. */
  padding: clamp(1.6rem, 4.5vh, 3.2rem) 1rem clamp(1.1rem, 3vh, 2.2rem);
  /* NOTE, measured: with the card centred in what is left, half of anything
     added here goes to the gap and half to the space under the card. So this
     number buys about half its own size in visible separation. Bigger values
     start to un-centre the card, which is the trade Deniz would have to pick. */
  margin-bottom: clamp(1.2rem, 4vh, 2.8rem);
  text-align: center;
  box-sizing: border-box;
}

.kfps-fshead-kicker {
  display: block;
  font-family: var(--font-heading, inherit);
  font-weight: 700;
  font-size: clamp(0.72rem, 1.5vw, 0.85rem);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-kicker, #2ec4b6);
}

.kfps-fshead-title {
  display: block;
  margin-top: 0.15rem;
  font-family: var(--font-heading, inherit);
  font-weight: 800;
  /* clamp written with spaces around the + : CSS maths silently drops the whole
     declaration without them, and the heading simply never grows */
  font-size: clamp(1.15rem, 0.9rem + 1.1vw, 1.8rem);
  line-height: 1.15;
  color: var(--kf-purple-deep, #2d1b4e);
}
