:root {
  color-scheme: light;
  background: transparent;
}

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: transparent;
  -webkit-user-select: none;
  user-select: none;
}

body {
  display: grid;
  place-items: center;
}

/* The device's drop shadow, on its own underlay so its tail can be faded to
   zero before the transparent window's rectangular edge (the halo: the
   shadow's alpha 1-4 tail met the window boundary as a straight-line chop).
   Masking the CONTENT is off the table — every arrangement that masked html,
   body, a full-window wrapper, or the WKWebView's own layer blanked this
   transparent web view at Large — but masking a subtree that holds only
   shadow paint composites fine. Two nested single-gradient masks compose the
   horizontal × vertical fades without mask-composite (which WKWebView also
   refused). 12px of fade at Large (2.553191vw / 1.558442vh), well inside the
   40px side and 63px vertical gutters. */
.shadowRim {
  position: fixed;
  inset: 0;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 2.553191vw,
    #000 calc(100% - 2.553191vw),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 2.553191vw,
    #000 calc(100% - 2.553191vw),
    transparent 100%
  );
}

.shadowRimY {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 1.558442vh,
    #000 calc(100% - 1.558442vh),
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 1.558442vh,
    #000 calc(100% - 1.558442vh),
    transparent 100%
  );
}

/* The caster: .frame's exact box (same width/aspect/centering rules), with
   the device body's own corner radius (SVG rx=24 on the 272 mock — smaller
   and the contact layer would fringe past the art's rounder corners). An
   outer box-shadow never paints under its border box, so the fill stays
   transparent for free. app.js mirrors .frame's resize transform here so
   the shadow scales in the same frame as the device. */
.deviceShadow {
  width: min(390px, 82.978723vw);
  max-height: 83.634286vh;
  aspect-ratio: 272 / 449.143;
  container-type: inline-size;
  border-radius: 8.8236cqw;
  /* The native Shadow slider runs 0…2 with the designed look notched at 1
     (Geoff, Aug 29: "bigger, darker and more smooth at the top end. put a
     notch in the slider"). app.js turns the one slider scalar into these
     three factors: below the notch only alpha dims (lighter room light,
     never a different shadow); above it blur grows fastest (smoother),
     offset next (bigger), alpha gently (darker). The ×1.8 blur / ×1.35
     offset ceiling is sized to the window's transparent gutters — ~8.5vw
     sideways, ~9vh below — so the top end fills the shadow room without
     clipping at the window edge (verified by edge scan at max). */
  box-shadow:
    0 calc(3.404255vw * var(--shadow-off, 1))
      calc(4.255319vw * var(--shadow-blur, 1))
      rgb(9 15 24 / calc(22% * var(--shadow-alpha, 1))),
    0 calc(1.276596vw * var(--shadow-off, 1))
      calc(2.12766vw * var(--shadow-blur, 1))
      rgb(9 15 24 / calc(24% * var(--shadow-alpha, 1))),
    0 calc(0.319149vw * var(--shadow-off, 1))
      calc(0.531915vw * var(--shadow-blur, 1))
      rgb(9 15 24 / calc(31% * var(--shadow-alpha, 1)));
  /* Stay composited between size changes — see the .frame note. */
  will-change: transform;
}

.screenReaderStatus {
  position: fixed;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.frame {
  /* 390×643.984 inside the 470×770 Large window. The 40px side and 63px
     vertical gutters are transparent shadow room; the visible iPod stays the
     same size. Percentage gutters keep Medium and Small at exact 75%/50%. */
  width: min(390px, 82.978723vw);
  max-height: 83.634286vh;
  margin: 0;
  --wheel-turn: 0deg;
  /* The shadow lives on .deviceShadow above, where it can be rim-faded.
     `none` is load-bearing: device.css also declares a .frame filter (the
     blog article's static 30/12/3px shadow) and removing this declaration
     would let that one cascade back in. */
  filter: none;
  /* Permanently composited. Without this, the size-change choreography
     promotes the layer when its transform lands and DE-promotes it when the
     settle clears the transform — and that de-promotion re-rasterizes the
     whole device one frame after the glide ends, every antialiased edge
     landing on slightly different pixels (measured: a full-device shimmer of
     up to 10/255, ~300ms after visual rest). Held composited, the rest state
     and the animated state share one rasterization and the pop cannot
     happen. One ~6MB texture at Large; static content, so no ongoing cost. */
  will-change: transform;
}

.screenPanel,
.screen {
  transition: none;
}

/* The firmware blits the highlight to its next row — there is no glide on the
   hardware, and the 90ms ease read as input lag stacked on every detent. */
.listTrack {
  transition: none;
}

.screenPanel {
  background:
    linear-gradient(145deg, rgb(255 255 255 / 12%) 18%, transparent 72%),
    #c4c4ba;
  opacity: 1;
}

.screen {
  filter: contrast(var(--lcd-contrast, 1));
}

/* The backlit look lives on an overlay, not a background swap: gradients
   cannot transition, so toggling data-backlight repainted the panel in one
   frame — a visible flash when the timer expires while the user is only
   watching. The real EL panel lights instantly but DECAYS over a beat, so
   the overlay fades: fast in, slow out. */
.screenPanel::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    linear-gradient(145deg, rgb(255 255 255 / 0%) 23%, rgb(255 255 255 / 56%) 98%),
    #f3f1e8;
  box-shadow:
    0 0 calc(8 * var(--px)) rgb(255 255 255 / 70%),
    inset 0 calc(-1 * var(--px)) calc(3 * var(--px)) rgb(0 0 0 / 25%),
    inset 0 calc(1 * var(--px)) calc(7 * var(--px)) rgb(255 255 255 / 85%);
  opacity: 0;
  transition: opacity 320ms ease-out;
}

.frame[data-backlight] .screenPanel::before {
  opacity: 1;
  transition-duration: 70ms;
}

.frame[data-backlight] .screen {
  filter: contrast(var(--lcd-contrast, 1)) brightness(1.03);
}

.statusPlayback {
  position: absolute;
  left: 3.2%;
  top: 5.1%;
  z-index: 1;
  width: 5%;
  height: 5.5%;
}

/* Sleep decays the LCD over a beat — a passive-matrix panel never cuts to
   blank in one frame. visibility flips only after the fade so the stale DOM
   stays untabbable. Wake stays instant (no base transition): pixels forming
   on a keypress read right, and sleep() leaves the DOM in place to fade. */
.frame[data-sleeping] .screen > * {
  visibility: hidden;
  opacity: 0;
  transition:
    opacity 360ms ease-out,
    visibility 0s 360ms;
}

.statusPlayback[data-state="playing"]::before {
  content: "";
  position: absolute;
  left: 10%;
  top: 4%;
  border-top: calc(4 * var(--px)) solid transparent;
  border-bottom: calc(4 * var(--px)) solid transparent;
  border-left: calc(6 * var(--px)) solid #282425;
}

.statusPlayback[data-state="paused"]::before,
.statusPlayback[data-state="paused"]::after {
  content: "";
  position: absolute;
  top: 5%;
  width: calc(2 * var(--px));
  height: calc(8 * var(--px));
  background: #282425;
}

.statusPlayback[data-state="paused"]::before {
  left: 14%;
}

.statusPlayback[data-state="paused"]::after {
  left: 48%;
}

.statusLock {
  position: absolute;
  left: 10.5%;
  top: 5.4%;
  z-index: 1;
  display: none;
  width: calc(7 * var(--px));
  height: calc(6 * var(--px));
  box-sizing: border-box;
  border: calc(1.2 * var(--px)) solid #282425;
}

.statusLock::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: calc(4 * var(--px));
  height: calc(4 * var(--px));
  transform: translateX(-50%);
  box-sizing: border-box;
  border: calc(1.2 * var(--px)) solid #282425;
  border-bottom: 0;
  border-radius: calc(3 * var(--px)) calc(3 * var(--px)) 0 0;
}

.frame[data-hold] .statusLock {
  display: block;
}

#screen-panels {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.listScrollbar {
  position: absolute;
  top: 1%;
  right: 0.8%;
  bottom: 1%;
  width: calc(2 * var(--px));
  background: rgb(40 36 37 / 18%);
}

.listScrollbar > span {
  position: absolute;
  left: 0;
  width: 100%;
  min-height: calc(7 * var(--px));
  background: #282425;
}

/* --- The boot splash (the web version's intro): the spark in LCD ink over
   a stepped progress bar, a cut to the wordmark, then the first iPod's
   wrap-sticker line cycling its languages. The panel owns the WHOLE glass
   (unlike the list panels, which start below the rule) — the chrome
   retires with it below. Everything is absolutely pinned so the
   boot→wordmark cut cannot shift the spark. Geometry is the blog
   component's, 1:1 — the two share the same --px unit and screen box. --- */

.introPanel {
  position: absolute;
  inset: 0;
}

.introSpark {
  position: absolute;
  left: 50%;
  top: 31%;
  transform: translate(-50%, -50%);
  width: calc(40 * var(--px));
  height: auto;
  fill: #282425;
}

.introBoot {
  position: absolute;
  left: 50%;
  top: 58%;
  transform: translate(-50%, -50%);
  width: calc(64 * var(--px));
  height: calc(6 * var(--px));
  border: calc(1 * var(--px)) solid #282425;
  background: rgb(191 188 181 / 45%);
  overflow: hidden;
}

.introBootFill {
  display: block;
  height: 100%;
  background: #282425;
  transform-origin: left;
  /* Chunky stepped fill — pixels arrive in blocks, the way a 2001 boot bar
     painted. Duration COUPLED to INTRO_BOOT_MS in app.js. */
  animation: introBootFill 1500ms steps(12, end) both;
}

@keyframes introBootFill {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.introWordmark {
  position: absolute;
  left: 50%;
  top: 58%;
  transform: translate(-50%, -50%);
  font-size: calc(17 * var(--px));
  letter-spacing: calc(-0.7 * var(--px));
  white-space: nowrap;
}

.introSticker {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(9 * var(--px));
  text-align: center;
  font-size: calc(9.5 * var(--px));
  letter-spacing: calc(-0.2 * var(--px));
  white-space: nowrap;
  visibility: hidden;
}

.introSticker[data-visible] {
  visibility: visible;
}

/* The splash owns the whole glass, so the shared chrome retires with it —
   and the reboot / nostalgia welcome screens keep the same bare glass.
   data-screen reads the DESTINATION mid-push, so the chrome is back the
   moment a button steps down into the menu. */
[data-screen="splash"] .screenTitle,
[data-screen="splash"] .batteryFrame,
[data-screen="splash"] .batteryBars,
[data-screen="splash"] .screenRule,
[data-screen="splash"] .statusPlayback,
[data-screen="splash"] .statusLock,
[data-screen="welcome"] .screenTitle,
[data-screen="welcome"] .batteryFrame,
[data-screen="welcome"] .batteryBars,
[data-screen="welcome"] .screenRule,
[data-screen="welcome"] .statusPlayback,
[data-screen="welcome"] .statusLock,
[data-screen="boot"] .screenTitle,
[data-screen="boot"] .batteryFrame,
[data-screen="boot"] .batteryBars,
[data-screen="boot"] .screenRule,
[data-screen="boot"] .statusPlayback,
[data-screen="boot"] .statusLock {
  visibility: hidden;
}

.splashPanel,
.welcomePanel,
.messagePanel {
  position: absolute;
  left: 0;
  top: 15.436%;
  width: 100%;
  height: 84.564%;
  box-sizing: border-box;
  display: grid;
  place-items: center;
  padding: 8% 9%;
  text-align: center;
  line-height: 1.18;
}

.welcomePanel {
  align-content: center;
  gap: 0.55em;
  /* Same descender room as list rows — Chicago's g/j/p/y get clipped
     otherwise in the tight LCD measure. */
  line-height: 1.28;
}

.welcomeLine {
  max-width: 100%;
}

.welcomeLine[data-mark] {
  font-size: 1.05em;
  margin-bottom: 0.35em;
}

.bootMark {
  font-size: 1.1em;
}

.messagePanel.aboutPanel {
  place-items: start;
  padding: 2.2% 3%;
  text-align: left;
  font-size: 0.72em;
  line-height: 1.42;
}

.settingValue {
  position: relative;
  margin-left: auto;
  color: inherit;
  opacity: 0.72;
}

.listRow[data-selected] .settingValue {
  color: #bfbcb5;
  opacity: 1;
}

.rowTitle {
  flex: 1 1 auto;
  min-width: 0;
  /* Chicago's g/j/p/q/y extend below its unusually tight line box. Grow the
     element's clip window without changing the six-row firmware geometry. */
  padding-top: 0.2em;
  padding-bottom: 0.2em;
  margin-top: -0.2em;
  margin-bottom: -0.2em;
}

.rowTitleText {
  display: inline-block;
  white-space: nowrap;
}

.listRow[data-selected] .rowTitle[data-overflow] {
  text-overflow: clip;
}

.listRow[data-selected] .rowTitle[data-overflow] .rowTitleText {
  animation: selectedRowMarquee 6s linear infinite;
}

@keyframes selectedRowMarquee {
  0%,
  16% {
    transform: translateX(0);
    margin-left: 0;
  }

  48%,
  62% {
    transform: translateX(-100%);
    margin-left: 100%;
  }

  94%,
  100% {
    transform: translateX(0);
    margin-left: 0;
  }
}

.settingValue {
  flex: none;
  max-width: 44%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Same descender guard as .rowTitle: extend the clip window
     past the tight line box, hand the space back with negative margins.
     Longhands only — a margin shorthand would kill margin-left: auto. */
  padding-top: 0.2em;
  padding-bottom: 0.2em;
  margin-top: -0.2em;
  margin-bottom: -0.2em;
}

.rowSubtitle {
  opacity: 0.7;
}

/* The wheel's fixed lighting stays put; only handling marks rotate. The Figma
   wear spots are #C6C6C6 at 9% — invisible on white plastic, so rotation read
   as a dead disc. What turns instead is finger dirt: three sprite-crops of the
   procedural thumb print, laid tangentially at r ~0.55 of the disc so one is
   always in view whatever angle the wheel rests at. Multiply at ~10% is about
   a 15-level dip on the plastic — legible while moving, reads as smudge, not
   as a drawn graphic. The grain and the wear spots stay off. */
.wheelGrain,
.wheelWearA,
.wheelWearB,
.wheelWearC,
.wheelWearD {
  display: none;
}

.wheelSmudgeA,
.wheelSmudgeB,
.wheelSmudgeC {
  mix-blend-mode: multiply;
  filter: blur(0.4px);
}

.wheelSmudgeA {
  left: 15%;
  top: 59.1%;
  width: 30%;
  height: 17.8%;
  opacity: 0.1;
  transform: rotate(-38deg);
}

.wheelSmudgeB {
  left: 57%;
  top: 27.9%;
  width: 22%;
  height: 13.1%;
  opacity: 0.062;
  transform: rotate(52deg);
}

.wheelSmudgeC {
  left: 38%;
  top: 6%;
  width: 25%;
  height: 14.8%;
  opacity: 0.045;
  transform: rotate(8deg);
}

.wheelLabels,
.pressLabels {
  opacity: 0.58;
  mix-blend-mode: multiply;
}

.wheelControl,
.interactive[data-dragging] .wheelControl,
.interactive[data-dragging] {
  cursor: default;
}

.volumePanel {
  position: absolute;
  left: 0;
  top: 15.436%;
  width: 100%;
  height: 84.564%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 9%;
}

.nowPlayingArtist {
  margin: 3% 0 1%;
}

.nowPlayingContext {
  display: flex;
  justify-content: space-between;
  width: 100%;
  min-height: 0.8em;
  font-size: 0.68em;
  line-height: 1;
}

.modeIcons {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
}

.shuffleIcon,
.repeatIcon {
  position: relative;
  display: inline-block;
  width: 1.15em;
  height: 0.7em;
}

.shuffleIcon::before,
.shuffleIcon::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(1 * var(--px));
  background: #282425;
  transform-origin: center;
}

.shuffleIcon::before {
  top: 28%;
  transform: rotate(26deg);
}

.shuffleIcon::after {
  bottom: 28%;
  transform: rotate(-26deg);
}

.repeatIcon {
  box-sizing: border-box;
  border: calc(1 * var(--px)) solid #282425;
  border-radius: 0.22em;
}

.repeatIcon[data-one]::after {
  content: "1";
  position: absolute;
  inset: -0.18em 0 0;
  text-align: center;
  font-size: 0.7em;
  line-height: 1;
}

.nowPlayingArtist,
.nowPlayingAlbum {
  margin: 0 0 auto;
  max-width: 100%;
  overflow: hidden;
  font-size: 1em;
  /* Chicago's g/j/p/q/y live below its tight em box, and overflow: hidden
     clips at the line box — 1.05 shaved "Anthology"'s descenders (the
     rowTitle / nowPlayingTitle lesson, Aug 30/31). The column's auto
     margins absorb the extra leading, so nothing below shifts. */
  line-height: 1.3;
  color: #282425;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nowPlayingArtist[data-long],
.nowPlayingAlbum[data-long] {
  width: 100%;
  text-align: left;
}

.nowPlayingArtist[data-long] > span,
.nowPlayingAlbum[data-long] > span {
  display: inline-block;
  white-space: nowrap;
  animation: ipodMarquee 9s linear infinite;
}

.nowPlayingTitle[data-long] {
  text-align: left;
  text-overflow: clip;
}

.volumeLabel {
  margin-bottom: 10%;
}

.volumeTrack {
  position: relative;
  width: 88%;
  height: 12%;
  border: calc(1 * var(--px)) solid #282425;
}

.gaugeRow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4%;
  width: 88%;
  height: 16%;
}

.gaugeRow .volumeTrack {
  flex: 1;
  width: auto;
  height: 72%;
}

.gaugeEndpoint {
  flex: none;
  width: 0.78em;
  aspect-ratio: 1;
  border: calc(1 * var(--px)) solid #282425;
  border-radius: 50%;
  box-sizing: border-box;
}

.gaugeLow {
  background: linear-gradient(90deg, #282425 50%, transparent 50%);
}

.gaugeHigh {
  background: #282425;
}

.volumeFill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--volume);
  background: #282425;
}

.loadingRow {
  animation: loadingBlink 1s steps(1, end) infinite;
}

.breakoutPanel {
  position: absolute;
  left: 0;
  top: 15.436%;
  width: 100%;
  height: 84.564%;
}

.breakoutPanel canvas {
  display: block;
  width: 100%;
  height: 100%;
}

@keyframes loadingBlink {
  50% {
    opacity: 0.45;
  }
}

button:focus {
  outline: none;
}

button:focus-visible {
  outline: calc(2 * var(--px)) solid #007da5;
  outline-offset: calc(2 * var(--px));
  border-radius: 50%;
}

.listViewport:focus {
  /* The selected LCD row is the keyboard focus indicator. */
  outline: none;
}

@media (prefers-reduced-motion: reduce) {
  .wheelControl,
  .btnMenu,
  .btnPrev,
  .btnNext,
  .btnPlay,
  .btnSelect,
  .interactive {
    pointer-events: auto;
  }

  .pushOutLeft,
  .pushInRight,
  .pushOutRight,
  .pushInLeft,
  .loadingRow,
  .listRow[data-selected] .rowTitle[data-overflow] .rowTitleText,
  .nowPlayingTitle[data-long] > span,
  .nowPlayingArtist[data-long] > span,
  .nowPlayingAlbum[data-long] > span {
    animation: none;
  }

  .buttonPressSurface,
  .pressArt,
  .selectPressSurface,
  .selectPressArt,
  .btnMenu,
  .btnPrev,
  .btnNext,
  .btnPlay,
  .btnSelect,
  .btnMenu::before,
  .btnPrev::before,
  .btnNext::before,
  .btnPlay::before,
  .btnSelect::before {
    transition: none;
  }
}
