/* Melo's Music Grove, GAME PAGES ONLY (melody-visual-spec.md, web-nova 2026-08-08).
   Linked by MelodyGame.astro AFTER kf-games.css, never by the lobby: the lobby
   renders only the menu cards and must not start paying for game art. This
   sheet owns everything the visual upgrade added (painted worlds, scrim panel,
   Melo, pad faces, stardust) plus the pad grid, which MOVED here from
   kf-games.css instead of being overridden there: the game pages are the pad
   grid's only consumers (the engine's own menu never draws on live routes,
   both game pages set data-mode), so a shared sheet was carrying rules only
   this surface reads.

   Load order matters for the few rules that override kf-games.css values
   (.kfmel-say size, .kfmel-dot shape, stage and end padding); those are also
   written one level more specific, so they win even if the links ever swap.

   Every world switch keys off ONE attribute: .kf-melody[data-world='...'],
   mirrored by the engine's setWorld(). Background, pad face, strings vs
   shadows and stagger all follow it, so the world pill's tap finally lands
   visibly. */

/* Component values, not tokens: nothing else on the site consumes these.
   Scene band = painted world visible between the stage top and the scrim.
   Frame = painted border the scrim leaves on the left and right.
   Feathers = how far the scrim's horizontal edges take to dissolve into the
   art (Deniz, 2026-08-08: the layer must melt away unnoticed, never end in a
   cut). Top rides INSIDE the scene band, so it stays smaller than every band
   and always leaves clear art above; bottom runs from the garden strip out
   through the stage's bottom edge, where alpha reaches zero, so the stage
   radius never clips visible navy. Both feathers sit clear of text and
   controls: the full-opacity core starts exactly where the panel's box
   starts and ends 28px under the action row's clearance (measured, see the
   scrim comment below). */
.kf-melody {
  --kfmel-scene-band: 56px;
  --kfmel-melo-h: 84px;
  --kfmel-melo-lift: 22px; /* how far Melo's head pokes above the scrim */
  --kfmel-frame: 12px;
  --kfmel-feather-top: 40px;
  --kfmel-feather-btm: 40px;
}
@media (min-width: 481px) {
  .kf-melody {
    --kfmel-scene-band: 96px;
    --kfmel-melo-h: 120px;
    --kfmel-feather-top: 56px;
    --kfmel-feather-btm: 48px;
  }
}
@media (min-width: 1024px) {
  .kf-melody {
    --kfmel-scene-band: 128px;
    --kfmel-melo-h: 160px;
  }
}

/* ============ the painted world ============
   The base gradient on .kfmel-stage/.kfmel-end (kf-games.css) stays put: it is
   the loading state, the image-failure state and the between-worlds state. The
   scene is progressive enhancement painted over it, absolutely positioned so
   it can never shift layout, clipped by the stage's own 26px radius. */
.kfmel-stage,
.kfmel-end {
  position: relative;
  overflow: hidden;
}
.kfmel-stage::before,
.kfmel-end::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center 38%;
  /* Invisible until the page script has SEEN the master finish downloading
     (.kfmel-scene-in); a half-decoded jpeg-style top-down reveal is exactly
     the jank the gradient placeholder exists to prevent. The global
     reduced-motion cap (tokens.css) makes this fade an instant step. */
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
.kf-melody[data-world='bells'] .kfmel-stage::before,
.kf-melody[data-world='bells'] .kfmel-end::before {
  background-image: url('/games/melody/bg-bells.webp');
}
.kf-melody[data-world='chimes'] .kfmel-stage::before,
.kf-melody[data-world='chimes'] .kfmel-end::before {
  background-image: url('/games/melody/bg-chimes.webp');
}
.kf-melody[data-world='drums'] .kfmel-stage::before,
.kf-melody[data-world='drums'] .kfmel-end::before {
  background-image: url('/games/melody/bg-drums.webp');
}
.kf-melody.kfmel-scene-in .kfmel-stage::before,
.kf-melody.kfmel-scene-in .kfmel-end::before {
  opacity: 1;
}

/* The stage stops padding its content directly; the scene band on top and a
   thin frame around keep the world visible, the scrim below holds everything
   interactive. One level more specific than kf-games' .kfmel-stage padding. */
.kf-melody .kfmel-stage {
  padding: var(--kfmel-scene-band) var(--kfmel-frame) var(--kfmel-frame);
}

/* ============ the scrim panel ============
   One panel carries EVERY interactive element (Melo's line, dots, pads,
   pills, garden). White text on --scrim-navy over the art brief's dark lower
   half measures 5.6:1+; that pairing is what lets real art and WCAG AA
   coexist. No backdrop-filter: blur is a frame-rate tax on cheap tablets and
   buys nothing over the luminance rule baked into the masters.

   The navy is painted by a pseudo, NOT on the element, for two reasons that
   are the whole of Deniz's 2026-08-08 note ("yedirerek yapmalisin... fark
   edilmeden yok olmali o layer"):
   1. The paint must run PAST the panel's box - up into the scene band and
      down through the bottom frame - so both horizontal edges can dissolve
      over art instead of cutting it. A background cannot leave its box.
   2. The element itself must never fade, or the say line, pads and buttons
      would fade with it; the pseudo sits at z -1, under all of them.
   The feather zones hold no text and no control. Top: full opacity begins
   exactly at the old top edge, so everything from Melo's line down sits on
   the same 0.62 navy it always did (only his bust stands in the ramp, and he
   is art). Bottom: the ramp spans the garden's last rows and the old 12px
   frame (keepsakes are aria-hidden decoration with no contrast floor); the
   action row stays 28px+ above it at every width (measured 830px: buttons
   end 66px above the panel's box bottom, core ends 28px below them; 360px:
   58px and 28px). No border-radius anymore: a feathered edge has no corner
   to round, and the stage's own radius clips the sides where alpha is
   already zero.
   The ramp is eased (smoothstep stops at 40% and 75%, alpha 0.22 and 0.52 of
   the 0.62 core = the --scrim-navy channels), because a linear ramp ends in
   a visible Mach band, which is the cut coming back as a ghost. */
.kfmel-scrim {
  position: relative;
  z-index: 1; /* above the scene layer */
  padding: var(--space-3);
}
.kfmel-scrim::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: calc(-1 * var(--kfmel-feather-top)) 0 calc(-1 * var(--kfmel-frame));
  background: linear-gradient(
    to bottom,
    rgba(15, 10, 36, 0) 0,
    rgba(15, 10, 36, 0.22) calc(var(--kfmel-feather-top) * 0.4),
    rgba(15, 10, 36, 0.52) calc(var(--kfmel-feather-top) * 0.75),
    var(--scrim-navy) var(--kfmel-feather-top),
    var(--scrim-navy) calc(100% - var(--kfmel-feather-btm)),
    rgba(15, 10, 36, 0.52) calc(100% - var(--kfmel-feather-btm) * 0.75),
    rgba(15, 10, 36, 0.22) calc(100% - var(--kfmel-feather-btm) * 0.4),
    rgba(15, 10, 36, 0) 100%
  );
  pointer-events: none;
}

/* ============ header row: Melo + his line ============
   Melo bust left, the say line beside him, vertically centered. The row
   reserves Melo's rendered height so message changes never shift the board
   below (the say element itself stays the aria-live region, untouched). */
.kfmel-head {
  position: relative;
  display: flex;
  align-items: center;
  text-align: left;
  min-height: var(--kfmel-melo-h);
  /* Melo's reserved box: sprite width (461:768, so 0.6 of height) plus a gap.
     Reserved via padding, not a flex slot, because the sprite is absolutely
     positioned to poke past the scrim's top edge. */
  padding-left: calc(var(--kfmel-melo-h) * 0.6 + var(--space-3));
  margin: 0 0 0.9rem;
}
.kfmel-melo-fig {
  position: absolute;
  left: 0;
  /* Lift + the scrim's own padding puts his head --kfmel-melo-lift px above
     the scrim edge, into the scene: he stands in the grove behind the panel.
     The lift is smaller than every scene band, so he never leaves the stage. */
  top: calc(-1 * (var(--kfmel-melo-lift) + var(--space-3)));
  width: calc(var(--kfmel-melo-h) * 0.6);
  height: var(--kfmel-melo-h);
  pointer-events: none;
}
.kfmel-melo-fig img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* Ear glow, canon: his ears shine when a melody approaches. Two soft radial
   layers anchored at ear positions measured from the delivered sprite
   (melo-listen.webp 461x768: cupped ear center ~30%/32%, far ear under the
   hair ~86%/31%), sized ~34% of sprite height. An aura AROUND the ears,
   honest about being light, same gold family as --glow-hint-soft/full.
   Swell 300ms in / 400ms out, never looping; the global reduced-motion cap
   turns both into instant opacity steps with the same meaning. */
.kfmel-melo-fig::before,
.kfmel-melo-fig::after {
  content: '';
  position: absolute;
  height: 34%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(212, 168, 67, 0.85), rgba(212, 168, 67, 0) 70%);
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity 0.4s ease; /* the 400ms out */
  pointer-events: none;
}
.kfmel-melo-fig::before {
  left: 30%;
  top: 32%;
}
.kfmel-melo-fig::after {
  left: 86%;
  top: 31%;
}
.kfmel-melo-fig.kfmel-ears-on::before,
.kfmel-melo-fig.kfmel-ears-on::after {
  opacity: 0.8;
  transition-duration: 0.3s; /* the 300ms in */
}
/* Once, brighter, on each completed echo: her melody reached him. */
.kfmel-melo-fig.kfmel-ears-big::before,
.kfmel-melo-fig.kfmel-ears-big::after {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1.12);
  transition-duration: 0.3s;
}

/* Melo speaks at child-label size (the 1.25rem in kf-games.css sat under the
   1.35rem floor); weight, white and the text-shadow arrive from kf-games. */
.kfmel-head .kfmel-say {
  margin: 0;
  font-size: var(--text-child-label);
}

/* ============ dots: done vs to-come, shape plus color ============
   Gold-on-dark by color alone failed the color-blind child; now undone is an
   empty ring and done is a filled ring, so the difference survives grayscale.
   The white ring on the scrim composite measures 5.65:1 (floor 3:1). */
.kf-melody .kfmel-dot {
  box-sizing: border-box;
  width: 14px;
  height: 14px;
  background: transparent;
  border: 2px solid rgba(255, 255, 255, 0.85);
}
.kf-melody .kfmel-dot.done {
  background: var(--kf-gold);
}

/* ============ pad grid: one balanced recipe per count ============
   Replaces the flex-wrap sizing that let the 5-pad board orphan 4+1. Every
   configuration lands balanced (3 / 4 / 2+2 / 5 / 3+2 / 4+4 / 3+3+2) and
   every target stays over the 64px floor at the 360px device (measured in the
   spec's table; re-measured in the browser at integration). */
.kfmel-pads {
  display: grid;
  justify-content: center;
  margin: 0 0 1.1rem;
  /* Constant headroom for the hanging strings (26px) and the stagger's low
     end (14px) on every world, so switching worlds mid-play never reflows the
     board; drums simply leave the space calm. */
  padding: 26px 0 14px;
}
.kfmel-pads[data-count='3'] {
  grid-template-columns: repeat(3, minmax(0, 8rem));
  gap: 16px; /* youngest band board: bigger pads, bigger gaps */
}
.kfmel-pads[data-count='4'] {
  grid-template-columns: repeat(4, minmax(0, 8rem));
  gap: 14px;
}
.kfmel-pads[data-count='5'] {
  grid-template-columns: repeat(5, minmax(0, 7.5rem));
  gap: 14px;
}
.kfmel-pads[data-count='8'] {
  grid-template-columns: repeat(4, minmax(0, 5.8rem));
  gap: 12px; /* rows land 4+4 */
  /* The label board is flat and stagger-free, so it needs no string headroom;
     a symmetric sliver keeps it breathing inside the scrim. */
  padding: 4px 0;
}
@media (max-width: 479px) {
  .kfmel-pads[data-count='4'] {
    grid-template-columns: repeat(2, minmax(0, 8rem)); /* rows 2+2 */
  }
}
/* 6-column span trick: three pads per row, and the short last row centers by
   starting one column in. A plain 3-column grid would left-hang the orphans. */
@media (max-width: 599px) {
  .kfmel-pads[data-count='5'] {
    grid-template-columns: repeat(6, minmax(0, 1fr)); /* rows 3+2 */
  }
  .kfmel-pads[data-count='5'] .kfmel-pad {
    grid-column: span 2;
  }
  .kfmel-pads[data-count='5'] .kfmel-pad:nth-child(4) {
    grid-column: 2 / span 2; /* pad 5 then flows into columns 4-5 */
  }
}
@media (max-width: 419px) {
  .kfmel-pads[data-count='8'] {
    /* A 4-column row here would be 58px pads, under the 64px floor; 3+3+2
       keeps ~79px targets and has no orphan. */
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
  .kfmel-pads[data-count='8'] .kfmel-pad {
    grid-column: span 2;
  }
  .kfmel-pads[data-count='8'] .kfmel-pad:nth-child(7) {
    grid-column: 2 / span 2;
  }
}

/* ============ the pads themselves ============
   Big equal circles, real buttons, exactly as before; the world object is the
   pad's FACE, never its silhouette. The July 31 lesson (halo clickable,
   corners swallowed, dead bands) rules out shaped hit areas for good. */
.kfmel-pad {
  position: relative;
  aspect-ratio: 1; /* the grid track sets the width; this keeps it a circle */
  border-radius: 50%;
  /* Solid white rim: the old 55% white measured under the 3:1 UI floor on the
     scrim composite; solid white clears it at 5.65:1. */
  border: 4px solid #fff;
  /* Depth without a second image: pad color in the center easing toward its
     deep value at the rim. The Do Re Mi board sets no --pad-deep, so it
     stays deliberately flat behind its solfege labels. */
  background: radial-gradient(
    circle at 50% 42%,
    var(--pad-color, #2ec4b6) 0%,
    var(--pad-color, #2ec4b6) 48%,
    var(--pad-deep, var(--pad-color, #2ec4b6)) 130%
  );
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 6px 18px rgba(15, 10, 36, 0.35);
  transition: filter 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  /* Hanging stagger rides one custom property so the lit scale below can
     compose with it instead of stomping it. */
  transform: translateY(var(--kfmel-dy, 0px));
}
/* The world object. A span, not the button's background, so the disc ground
   stays the failure state: if the sprite 404s the colored circle underneath
   is exactly the board that shipped before this upgrade, still operable. */
.kfmel-pad-face {
  width: 72%;
  height: 72%;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
}
/* Each world's object dresses the pad faces AND the garden keepsakes from
   the same three rules: one keepsake blooms per completed round (engine
   bloom()), and because both classes key off [data-world] here, a world
   switch re-stamps the whole shelf the moment it re-dresses the pads. The
   keepsakes are one collection, always the current world's object; the
   Do Re Mi board's keepsake is an inline SVG note in the engine and never
   reads these. */
.kf-melody[data-world='bells'] .kfmel-pad-face,
.kf-melody[data-world='bells'] .kfmel-keep-face {
  background-image: url('/games/melody/pad-bell.webp');
}
.kf-melody[data-world='chimes'] .kfmel-pad-face,
.kf-melody[data-world='chimes'] .kfmel-keep-face {
  background-image: url('/games/melody/pad-chime.webp');
}
.kf-melody[data-world='drums'] .kfmel-pad-face,
.kf-melody[data-world='drums'] .kfmel-keep-face {
  background-image: url('/games/melody/pad-drum.webp');
}
/* The keepsake face fills the flower's old 2rem envelope (kf-games.css
   .kfmel-flower), so the shelf's layout does not move by a pixel; a span
   with a CSS background, like the pad face above, so a missing sprite
   degrades to blank decoration, never a broken-image glyph. */
.kfmel-flower .kfmel-keep-face {
  display: block;
  width: 100%;
  height: 100%;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}

/* Hanging fiction, bells and chimes: a string rising from each pad, fading
   upward into the grove light so it needs no attachment point, plus a pitch
   stagger: the lower the note, the lower it hangs (physically true, and a
   fifth non-color pitch cue). Engine sets --i per pad and --pad-count on the
   grid; 3.5px per step caps the drop at 14px on the 5-pad board. The label
   board (8) is excluded: its labels are its art. */
.kf-melody[data-world='bells'] .kfmel-pads:not([data-count='8']) .kfmel-pad,
.kf-melody[data-world='chimes'] .kfmel-pads:not([data-count='8']) .kfmel-pad {
  --kfmel-dy: calc((var(--pad-count, 3) - 1 - var(--i, 0)) * 3.5px);
}
.kf-melody[data-world='bells'] .kfmel-pads:not([data-count='8']) .kfmel-pad::before,
.kf-melody[data-world='chimes'] .kfmel-pads:not([data-count='8']) .kfmel-pad::before {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 3px;
  height: 26px;
  margin-left: -1.5px;
  background: linear-gradient(to top, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0));
  border-radius: 2px;
  pointer-events: none;
}
/* Drums stand on the meadow: no strings, no stagger, a soft dark ellipse
   under each one instead. It peeks from under the circle, so it never has to
   fight the pad's own paint order. */
.kf-melody[data-world='drums'] .kfmel-pads:not([data-count='8']) .kfmel-pad::after {
  content: '';
  position: absolute;
  left: 10%;
  right: 10%;
  bottom: -13px;
  height: 12px;
  background: radial-gradient(ellipse at center, rgba(15, 10, 36, 0.45), rgba(15, 10, 36, 0) 70%);
  pointer-events: none;
}

.kfmel-pad:hover {
  filter: brightness(1.08); /* tap parity exists: pointerdown is the trigger */
}
/* The glow: a slow swell, never a flash (300ms in, 300ms out), unchanged from
   the shipped engine except that scale now composes with the stagger. */
.kfmel-pad.lit {
  filter: brightness(1.3) saturate(1.15);
  box-shadow:
    0 0 0 8px rgba(255, 255, 255, 0.3),
    0 0 34px 6px var(--pad-color),
    0 6px 18px rgba(15, 10, 36, 0.35);
  transform: translateY(var(--kfmel-dy, 0px)) scale(1.06);
}
.kfmel-pads.listening .kfmel-pad {
  cursor: default;
}
/* Safety net for a stale cached engine that still draws SVG pad faces: keep
   them sized instead of letting an unsized inline SVG blow the circle open. */
.kfmel-pad svg {
  width: 58%;
  height: 58%;
  pointer-events: none;
}

/* ============ end panel: same world, same scrim, no extra markup ============
   The scene ::before is shared above; the scrim here is an ::after because the
   engine's end panel has no wrapper element, and it does not need one: text
   and buttons stack above it, stardust between.

   Same dissolve as the stage scrim, same reasons (see that comment). The only
   geometric difference: this panel has no garden strip under its buttons, so
   the bottom padding grows by the feather to push the buttons clear of the
   ramp - the spec's "extend the core downward, the feather lives where no
   control sits". Buttons end feather+16px above the panel's bottom edge, the
   core reaches to 28px below them, and the ramp dies to zero exactly at the
   stage edge, under nothing but stardust. */
.kf-melody .kfmel-end {
  padding: calc(var(--kfmel-scene-band) + var(--space-3))
    calc(var(--kfmel-frame) + var(--space-3))
    calc(var(--kfmel-feather-btm) + var(--space-3) + var(--kfmel-frame));
}
.kfmel-end::after {
  content: '';
  position: absolute;
  z-index: 1;
  inset: calc(var(--kfmel-scene-band) - var(--kfmel-feather-top)) var(--kfmel-frame) 0;
  background: linear-gradient(
    to bottom,
    rgba(15, 10, 36, 0) 0,
    rgba(15, 10, 36, 0.22) calc(var(--kfmel-feather-top) * 0.4),
    rgba(15, 10, 36, 0.52) calc(var(--kfmel-feather-top) * 0.75),
    var(--scrim-navy) var(--kfmel-feather-top),
    var(--scrim-navy) calc(100% - var(--kfmel-feather-btm)),
    rgba(15, 10, 36, 0.52) calc(100% - var(--kfmel-feather-btm) * 0.75),
    rgba(15, 10, 36, 0.22) calc(100% - var(--kfmel-feather-btm) * 0.4),
    rgba(15, 10, 36, 0) 100%
  );
  pointer-events: none;
}
.kfmel-end-big,
.kfmel-end-small,
.kfmel-end-btns {
  position: relative;
  z-index: 3; /* text above the stardust, stardust above the scrim */
}

/* ============ stardust ============
   Code-drawn BY DESIGN and not the Star Farm defect: the ban is on code-drawn
   SUBJECT art, and the subjects are real sprites now; sparkle on top is house
   practice (D-011). Small, soft-edged, on the dark scrim: nowhere near the
   flash red line. The engine positions, colors and sizes each mote inline and
   removes it when it is done; under reduced motion it skips these classes
   entirely and places static motes instead. */
.kfmel-star {
  position: absolute;
  z-index: 2;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 6px 1px currentColor;
  pointer-events: none;
}
.kfmel-star.is-rise {
  animation: kfmel-star-rise 0.9s ease-out forwards;
}
.kfmel-star.is-drift {
  /* fill-mode both: invisible through its stagger delay, gone after */
  animation: kfmel-star-drift 2.2s ease-out both;
}
.kfmel-star.is-staying {
  opacity: 0.45; /* the faint decoration that stays on the end panel */
}
@keyframes kfmel-star-rise {
  from {
    transform: translateY(0);
    opacity: 0.95;
  }
  to {
    transform: translateY(-48px);
    opacity: 0;
  }
}
@keyframes kfmel-star-drift {
  from {
    transform: translateY(12px);
    opacity: 0;
  }
  25% {
    opacity: 0.85;
  }
  to {
    transform: translateY(-58px);
    opacity: 0;
  }
}

/* ============ reduced motion ============
   tokens.css already caps every animation and transition on the page, which
   makes the scene fade, the ear glow and the dot fill instant steps with the
   same meaning. The two rules here are the ones a duration cap cannot express:
   the lit swell loses its scale but keeps the stagger, which is static
   positioning, not motion. Stardust fallbacks live in the engine (REDUCED). */
@media (prefers-reduced-motion: reduce) {
  .kfmel-pad,
  .kfmel-pad.lit {
    transform: translateY(var(--kfmel-dy, 0px));
  }
}
