/**
 * Moonwheel TV receiver — static shell.
 *
 * A single fixed 1920x1080 plate. Nothing here animates or responds to input:
 * the phone is the remote, and receiver.js drives every visible state by
 * setting #stage[data-mode] and moving text and artwork into the ids below.
 *
 * Every colour literal in this file lives once in :root. Everything else
 * refers to a custom property, so the palette is a five-line change and a
 * stray hard-coded colour shows up in review instead of hiding in a rule.
 */

:root{
  --bg:#0B1220;
  --ink:#F4F1EA;
  --ink-dim:#F4F1EAaa;
  --gold:#E8B86D;
  --scrim:rgba(8,12,22,.55);
  /* Sits directly under a glyph rather than behind a panel, so it is nearly
     opaque where the scrim is deliberately not. */
  --ink-shadow:rgba(6,9,16,.92);
  /* Sits behind the caption's words only. 60% — dark enough to hold white
     text over the brightest part of a plate, light enough that the picture
     still reads through it. */
  --caption-ground:rgba(0,0,0,.6);

  /* 5% overscan safe area on a 1920x1080 plate: no text or artwork crosses
     it, because a cheap TV panel crops the outer edge of the picture. */
  --safe-x:96px;
  --safe-y:54px;
}

*{box-sizing:border-box;margin:0;padding:0}

/* The page fills whatever the device gives us; the PLATE inside it stays
   1920x1080 and is scaled to fit. Every measurement in this file is
   therefore a real 1920x1080 measurement and needs no arithmetic — which
   is the point, since the alternative is authoring twice.

   html/body used to be 1920x1080 themselves. A Cast receiver commonly lays
   out at 1280x720 CSS pixels, so that plate was 1.5x too big for its own
   window and `overflow:hidden` simply cut the right and bottom off. */
html,body{
  width:100%;
  height:100%;
  overflow:hidden;
  background:var(--bg);
  color:var(--ink);
  font-family:Georgia,'Times New Roman',serif;
}

/* fitStage() in receiver.js writes the transform. The origin is the top-left
   corner because the transform carries its own centring offset — with a
   centred origin the two would compound and the plate would drift. */
#stage{
  position:absolute;
  top:0;
  left:0;
  width:1920px;
  height:1080px;
  transform-origin:0 0;
}

/* ---------------------------------------------------------------- backgrounds
   Two layers, not two symmetric layers: #bg is the settled plate and
   #bg-next is the incoming one. receiver.js fades #bg-next in over #bg,
   copies the image down into #bg, then clears #bg-next — so #bg is up from
   the first paint and only ever needs the modifier for a hard cut. */

.bg-layer{
  position:absolute;
  inset:0;
  /* receiver.js overrides background-size per plate: `cover` for one cut to
     the screen's shape, `contain` for one too far off it to crop honestly.
     no-repeat is not optional once `contain` is possible — without it a
     letterboxed plate tiles into its own bars. */
  background-size:cover;
  background-repeat:no-repeat;
  background-position:center;
  opacity:0;
  transition:opacity 900ms ease;
}
.bg-layer.is-visible{opacity:1}
#bg{opacity:1}

/* Sleep uses the same portrait dreamscape as the phone. The receiver crops
   from its upper portion so the moon survives the 16:9 cut, then gives the
   plate one extremely slow breath while the track plays. Transforming the
   already-decoded layer is inexpensive on Cast hardware and needs no video,
   canvas or timer. */
#stage[data-mode="moonwheel-harmonies"] .bg-layer.is-sleep-dreamscape{
  background-position:center 7%;
  transform-origin:50% 24%;
  will-change:transform;
  animation:sleep-dream 44s ease-in-out infinite;
}
#stage[data-mode="moonwheel-harmonies"]:not(.is-playing) .bg-layer.is-sleep-dreamscape{
  animation-play-state:paused;
}
@keyframes sleep-dream{
  0%,100%{
    transform:translate3d(-8px,0,0) scale(1.045);
  }
  50%{
    transform:translate3d(10px,-8px,0) scale(1.075);
  }
}

/* A TV/browser that asks for reduced motion keeps the new artwork but not the
   drift. This also avoids leaving a half-applied keyframe transform behind. */
@media (prefers-reduced-motion:reduce){
  #stage[data-mode="moonwheel-harmonies"] .bg-layer.is-sleep-dreamscape{
    animation:none;
    transform:scale(1.045);
  }
}

/* ---------------------------------------------------------------- panorama
   Music Box's scrolling plate. The row holds two copies of the same image and
   is translated by exactly one copy's width before looping, so the picture
   passes continuously and the join never shows — provided the artwork's left
   and right edges meet, which these are drawn to do.

   Width comes from receiver.js as --pan-width, measured from the plate itself
   rather than assumed: the ten panoramas are not all the same width. The
   duration is set from that width too, so every section scrolls at the same
   speed rather than the widest one crawling. */

#panorama{
  position:absolute;
  inset:0;
  display:none;
  /* The row is two plates wide — more than twice the stage. Without this it
     would spill past the stage and into the letterbox bars. */
  overflow:hidden;
}
#pan-row{
  position:absolute;
  top:0;
  left:0;
  height:100%;
  display:flex;
  will-change:transform;
  animation:pan var(--pan-duration,100s) linear infinite;
}
.pan-tile{
  flex:0 0 auto;
  width:var(--pan-width,4271px);
  height:100%;
  background-size:100% 100%;
  background-repeat:no-repeat;
}
@keyframes pan{
  from{transform:translateX(0)}
  to{transform:translateX(calc(-1 * var(--pan-width,4271px)))}
}
/* Stopped when the music is. The scene moving on under a paused track reads
   as the app having carried on without you — the phone gates its own scroll
   on playback for the same reason. */
#stage:not(.is-playing) #pan-row{animation-play-state:paused}

/* ------------------------------------------------------------------- chrome
   Each floating group carries its own soft dark ground rather than sitting on
   a flat bar — the same treatment the phone app gives its chrome over the
   full-bleed panoramas. Two stacked ramps of the one scrim token, the second
   much shorter than the first, so the alpha eases off unevenly and the eye
   never finds the line where the gradient stops. The darkest end compounds to
   roughly four-fifths opaque, deliberately short of punching the artwork out. */

#topbar{
  position:absolute;
  top:0;
  left:0;
  right:0;
  /* 64px down, but the full safe-area inset across: the wordmark is text, and
     text at the horizontal edge is what overscan eats first. The deep bottom
     padding is empty space for the gradient to fade out in — it takes the
     scrim to about 22% of the plate, the same reach the phone app's top scrim
     has, and the fade is spent before the element's edge so there is no line
     where it stops. */
  padding:64px var(--safe-x) 96px;
  background:
    linear-gradient(to bottom,var(--scrim),transparent 88%),
    linear-gradient(to bottom,var(--scrim),transparent 42%);
}
#brand{
  color:var(--gold);
  font-size:28px;
  font-variant:small-caps;
  letter-spacing:.42em;
}
#mode-title{
  margin-top:14px;
  font-size:56px;
  font-weight:400;
  line-height:1.2;
}

/* ------------------------------------------------------------- centre panel
   The one-card mode: Ambient Sounds (a fixed plate and a "use your phone"
   line). */

#center-panel{
  position:absolute;
  inset:0;
  display:none;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  padding:var(--safe-y) var(--safe-x);
  text-align:center;
  /* These two modes now paint their own plate as the background as well as
     on the card, so the card would otherwise sit on a full-brightness copy
     of itself. A centred radial ground darkens the middle and leaves the
     outer picture legible — the same treatment the lyrics panel gets, and
     the reason neither mode needs a blur (a 1920px filter on a TV GPU is
     the one thing here that could cost frames). */
  background:radial-gradient(120% 120% at 50% 50%,var(--scrim),transparent 78%);
}
#center-art{
  flex:0 0 auto;
  width:520px;
  height:520px;
  border-radius:18px;
  object-fit:cover;
  box-shadow:0 8px 28px var(--scrim),0 28px 90px var(--scrim);
}
#center-title{
  margin-top:44px;
  max-width:1100px;
  font-size:64px;
  font-weight:400;
  line-height:1.15;
}
#center-message{
  margin-top:22px;
  max-width:1100px;
  font-size:34px;
  line-height:1.4;
  color:var(--ink-dim);
}

/* -------------------------------------------------------------- lyrics panel
   The lower third for the two narrated story modes. A radial ground rather
   than a band: the panel is a column of centred text with no edges of its
   own, so a rectangle behind it would read as a subtitle box. */

#lyrics-panel{
  position:absolute;
  left:var(--safe-x);
  right:var(--safe-x);
  /* Just clear of the transport, which is up in both story modes. The row
     inside it ends 156px off the bottom (60px padding + 96px of art), so this
     leaves a 12px gap above it — the caption reads as the line belonging to
     the player rather than floating in the middle of the picture. */
  bottom:168px;
  display:none;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  max-height:200px;
  overflow:hidden;
  padding:12px 80px;
  text-align:center;
  /* No ground of its own. The panel used to sit on two stacked radial scrims,
     which read as a dark band laid across the artwork — the text is lifted off
     the picture by its own shadow instead. */
}
.lyric{
  max-width:1500px;
  /* Smaller than it was: at 52px a full line crowded the plate and pulled the
     eye off the picture, which is the thing the story is actually told in. */
  font-size:38px;
  line-height:1.35;
  /* Hard shadow, no blur: one pixel of black directly under the glyph. It
     separates the text from a light plate without the halo a blurred shadow
     puts around every letter. */
  text-shadow:0 1px 0 var(--ink-shadow);
  /* The ground goes behind the WORDS, not across the screen. The panel is a
     centred column, so this element is only as wide as its own line and the
     bar ends with the sentence — where the old treatment ran edge to edge and
     read as a band laid over the artwork. An empty line renders no box at
     all, because there is nothing to pad. */
  display:inline-block;
  padding:8px 24px;
  border-radius:12px;
  background:var(--caption-ground);
}
.lyric:empty{padding:0;background:none}
.lyric.is-dim{opacity:.4}

/* ---------------------------------------------------------------- transport
   Bottom strip. The element is taller than the row inside it and the extra
   height is only gradient: the art and the text stop at the safe-area line,
   and the scrim keeps rising above them so the row has something to sit on
   instead of a hard top edge. */

#transport{
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  height:210px;
  display:none;
  align-items:flex-end;
  gap:28px;
  padding:0 var(--safe-x) 60px;
  background:
    linear-gradient(to top,var(--scrim),transparent 92%),
    linear-gradient(to top,var(--scrim),transparent 46%);
}
#transport-art{
  flex:0 0 auto;
  width:96px;
  height:96px;
  border-radius:12px;
  object-fit:cover;
  box-shadow:0 6px 22px var(--scrim);
}
/* A fixed basis, not a growing one: the play glyphs are centred on the screen
   and a title allowed to flex would slide underneath them. */
#transport-meta{
  flex:0 1 620px;
  min-width:0;
  /* Optically centres the two text lines on the 96px art beside them. */
  padding-bottom:12px;
}
#transport-title{
  font-size:30px;
  line-height:1.2;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
#transport-sub{
  margin-top:10px;
  font-size:22px;
  line-height:1.2;
  color:var(--ink-dim);
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
#transport-icons{
  position:absolute;
  left:50%;
  /* Centred on the screen rather than centred in the leftover flex space, and
     lifted to put the 44px glyphs on the same optical centre line as the 96px
     art at the left of the row. Measured from the padding box, so it is the
     60px of row clearance plus half the height difference. */
  bottom:86px;
  transform:translateX(-50%);
  display:flex;
  align-items:center;
  gap:44px;
}

/* Status indicators, not controls — the phone is the remote, so there is
   nothing to hover, press or point at. Each glyph is one box of the dim ink
   token cut by a clip-path; no icon font, no SVG. */
.tp-ic{
  position:relative;
  flex:0 0 auto;
  width:44px;
  height:44px;
  background:var(--ink-dim);
  cursor:default;
}
.tp-prev{clip-path:polygon(88% 6%,88% 94%,16% 50%)}
.tp-next{clip-path:polygon(12% 6%,84% 50%,12% 94%)}
.tp-play{clip-path:polygon(18% 4%,92% 50%,18% 96%)}

/* Playing swaps the play triangle for two bars. The bars are pseudo-elements
   rather than a second clip-path because one polygon cannot describe two
   disconnected rectangles. */
#transport.is-playing #tp-playpause{background:none;clip-path:none}
#transport.is-playing #tp-playpause::before,
#transport.is-playing #tp-playpause::after{
  content:'';
  position:absolute;
  top:12%;
  bottom:12%;
  width:24%;
  background:var(--ink-dim);
}
#transport.is-playing #tp-playpause::before{left:18%}
#transport.is-playing #tp-playpause::after{right:18%}

/* Pinned to the very bottom edge, outside the safe area on purpose: it is a
   4px rule with no detail in it to lose, and lifting it to the safe line
   would leave it floating under the artwork. */
#progress{
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  height:4px;
  background:var(--scrim);
}
#progress-fill{
  width:0;
  height:100%;
  background:var(--gold);
}

/* --------------------------------------------------------------- diagnostics
   Deliberately plain and deliberately small. It is not part of the design —
   it exists because the only instrument available on a TV across the room is
   a photograph, and it must be legible in one. */

/* TOP-LEFT, not the bottom-right corner it started in. That corner is the
   first thing to leave the screen when the plate is scaled too large — so
   the one element whose job is to explain an overflow was being hidden by
   the overflow. The top-left stayed readable through every version of that
   bug, which makes it the only honest place for this. */
#diag{
  position:absolute;
  left:var(--safe-x);
  top:190px;
  max-width:760px;
  padding:10px 18px;
  border-radius:8px;
  background:var(--scrim);
  color:var(--ink-dim);
  font-family:monospace;
  font-size:22px;
  line-height:1.5;
  text-align:left;
  white-space:pre-line;
}

/* ------------------------------------------------------------------- modes
   Visibility is driven entirely off #stage[data-mode], which receiver.js sets
   from the wire mode. The `hidden` attributes in index.html are only the
   pre-JavaScript state and are not consulted here; a mode this file has never
   heard of falls through to them and shows the artwork alone. */

#stage[data-mode="music-box"] #center-panel,
#stage[data-mode="music-box"] #lyrics-panel{display:none}
#stage[data-mode="music-box"] #transport{display:flex}
/* The panorama is the whole background here, so it covers the bg layers
   rather than replacing them — they keep whatever the last section left, and
   get it back on the way out. */
#stage[data-mode="music-box"] #panorama{display:block}

#stage[data-mode="lullaby-stories"] #center-panel,
#stage[data-mode="dream-journey"] #center-panel{display:none}
#stage[data-mode="lullaby-stories"] #lyrics-panel,
#stage[data-mode="dream-journey"] #lyrics-panel{display:flex}
#stage[data-mode="lullaby-stories"] #transport,
#stage[data-mode="dream-journey"] #transport{display:flex}

/* Moonwheel Harmonies: the per-category plate fills the screen (the bg layers
   keep their default opacity:1), the transport strip sits over it, and the
   centre card and lyrics panel stay down — there is no narration and the
   title already rides in the transport strip. */
#stage[data-mode="moonwheel-harmonies"] #center-panel,
#stage[data-mode="moonwheel-harmonies"] #lyrics-panel{display:none}
#stage[data-mode="moonwheel-harmonies"] #transport{display:flex}

/* Ambient Sounds shows no background image. Fading the layers out rather than
   clearing them keeps the crossfade timing identical to every other mode
   change, and leaves whatever was loaded in place for the return trip. The
   selector outranks #bg's own opacity:1, which is what makes it work. */
#stage[data-mode="ambient-sounds"] .bg-layer{opacity:0}

/* TEMPORARY. On Ambient Sounds the readout moves to the middle of the screen,
   under the card, because that is the one screen being asked about and the
   corner has twice failed to be seen from across a room. It goes back to the
   corner as soon as the mixer's state is known once. */
#stage[data-mode="ambient-sounds"] #diag{
  left:50%;
  right:auto;
  top:auto;
  bottom:150px;
  max-width:1500px;
  transform:translateX(-50%);
  text-align:center;
  font-size:30px;
}

#stage[data-mode="ambient-sounds"] #center-panel{display:flex}
#stage[data-mode="ambient-sounds"] #lyrics-panel,
#stage[data-mode="ambient-sounds"] #transport{display:none}
