{
  "version": 3,
  "sources": ["/cdn/shop/t/14/assets/eva-radio.js?cb=1787694683"],
  "sourcesContent": ["/*\n  Eva Radio: floating draggable tape-deck widget. Vanilla JS, no\n  dependencies. Persists open/closed, position, and playback state in\n  localStorage under \"evaRadioState\" so it survives hard reloads and\n  PJAX navigation, since this whole widget lives outside the swapped\n  #MainContent region.\n*/\n(function () {\n  function ready(fn) {\n    if (document.readyState !== 'loading') {\n      fn();\n    } else {\n      document.addEventListener('DOMContentLoaded', fn);\n    }\n  }\n\n  // Same ?tapedev=1 convention eva-radio-tape.js already uses for its own\n  // dev-only logging -- gates the FF/REW sound-trigger console.debug lines\n  // below so verify can prove both buttons actually fire the same sound\n  // without leaving noise in the console for normal visitors.\n  function isTapeDevMode() {\n    try { return new URLSearchParams(window.location.search).get('tapedev') === '1'; } catch (e) { return false; }\n  }\n\n  var STATE_KEY = 'evaRadioState';\n  var MARGIN = 24; // default spawn distance from the document edge\n  // Room reserved on top of MARGIN so the tape rack (eva-radio-tape.js,\n  // anchored left:100% + margin-left:16px off the widget's own right\n  // edge) doesn't overflow the document at the widget's DEFAULT spawn\n  // position. Found 2026-08-05: defaultPosition() below only ever\n  // accounted for the widget's own 340px box, not the rack that sits\n  // beside it -- confirmed live via Playwright at 1440/1600/1920px that\n  // the rack overflowed the document edge by ~113px at every one of\n  // them (rack footprint ~137px vs MARGIN's 24px, and the gap didn't\n  // shrink on a wider viewport since the widget's own default position\n  // is ALSO computed off viewportWidth() (was documentWidth() before\n  // the 2026-08-09 fixed-position rework), not a fixed offset -- it\n  // keeps the same ~24px right margin regardless of window width). A\n  // first-time visitor got an unreachable rack until they happened to\n  // drag the whole widget left first, with nothing prompting that.\n  // 137px measured; a little headroom rather than the bare minimum.\n  var RACK_RESERVE = 150;\n  var DRAG_THRESHOLD = 4; // px of movement before a pointerdown counts as a drag, not a click\n  var MIN_WIDTH = 200;\n  var MAX_WIDTH = 560;\n  var SEEK_SECONDS = 10; // tap step\n  var SAVE_INTERVAL_MS = 4000; // throttle position persistence while playing\n  var PLAY_SOUND_LEAD_MS = 150; // PLAY hotspot: clack starts, music enters this far into it\n  var SEEK_HOLD_THRESHOLD_MS = 180; // press duration before a tap becomes a hold\n  var FFW_PLAYBACK_RATE = 4; // within spec's 3-6 range\n  var FFW_DUCK_VOLUME = 0.4;\n  var REW_DUCK_VOLUME = FFW_DUCK_VOLUME; // Per: REW should sound the same as FFW while held, not near-silent -- was 0.10\n  var KEYBOARD_DUCK_VOLUME = 0.65; // keys+tape play at once now -- lower \"just a tad\" per Per, not the near-silent FFW/REW duck\n  var REW_SCRUB_STEP_SECONDS = 2;\n  var REW_SCRUB_INTERVAL_MS = 250;\n\n  // Tapes: zero tape data lives in this file. The manifest (assets/eva-\n  // tapes.json, schema v1) is fetched once at init -- see loadTapesManifest()\n  // below -- and populates this array. Each side is an ARRAY OF TRACK\n  // OBJECTS ({title, url, gainDb}) in play order, not a lookup into a\n  // separate number-keyed map (that indirection was a v1 artifact of how\n  // Per originally supplied the files; the JSON schema flattens it away).\n  var tapes = [];\n\n  var DEFAULTS = {\n    minimized: true, // was \"open: false\" -- inverted sense, renamed to match the new minimize/restore terminology\n    x: null,\n    y: null,\n    width: null, // resized width in px; null = use the CSS default (responsive, see eva-radio.css)\n    userMoved: false,\n    tapeId: null,\n    sideId: null,\n    trackIndex: 0,\n    positionSec: 0,\n    playing: false,\n    angle: 'front' // 'front' | 'above' -- decides which hotspot layer (tape/keyboard) is visible; keys + tape both work regardless of which angle is showing, no separate mode\n  };\n\n  function loadState() {\n    try {\n      var raw = localStorage.getItem(STATE_KEY);\n      if (!raw) { return Object.assign({}, DEFAULTS); }\n      var parsed = JSON.parse(raw);\n      return Object.assign({}, DEFAULTS, parsed);\n    } catch (e) {\n      return Object.assign({}, DEFAULTS);\n    }\n  }\n\n  function saveState() {\n    try {\n      localStorage.setItem(STATE_KEY, JSON.stringify(state));\n    } catch (e) {}\n  }\n\n  var state = loadState();\n\n  // Resolves eva-tapes.json's image slots (2026-08-03) -- the manifest\n  // stores bare filenames now, not full URLs (a hardcoded URL baked in a\n  // fixed Shopify theme-asset-slot number, e.g. \".../t/14/assets/...\",\n  // silently went stale the moment this draft theme's own slot turned\n  // out to be t/10 instead -- see the window.EvaTapesImageAssets comment\n  // in theme.liquid for the full incident). window.EvaTapesImageAssets\n  // (theme.liquid, asset_url-resolved fresh on every page load) is the\n  // only source of truth for the real URL. A filename with no matching\n  // entry is left as-is rather than dropped -- shouldn't happen since\n  // theme.liquid's map is meant to list every filename any tape uses,\n  // but failing open here (falling through to renderFace()'s existing\n  // silent-404 handling) is safer than a tape rendering with a face\n  // missing entirely over one unlisted filename.\n  function resolveTapeImages(list) {\n    var map = window.EvaTapesImageAssets || {};\n    list.forEach(function (tape) {\n      if (!tape.images) { return; }\n      Object.keys(tape.images).forEach(function (key) {\n        var filename = tape.images[key];\n        if (filename && map[filename]) { tape.images[key] = map[filename]; }\n      });\n    });\n    return list;\n  }\n\n  // Fetched once here (window.EvaRadioTapesManifestUrl comes from\n  // theme.liquid, since this plain .js asset can't call asset_url itself)\n  // and reused for the life of the page -- the browser's own HTTP cache\n  // (the URL is fingerprinted, ?v=...) makes repeat PJAX navigations free,\n  // no extra caching layer needed. Sorted by dateAdded ASC on arrival --\n  // oldest/first-added tape first, per Per's rack ordering spec -- array\n  // position in the JSON itself carries no meaning.\n  var tapesReady = fetch(window.EvaRadioTapesManifestUrl || '', { credentials: 'omit' })\n    .then(function (res) {\n      if (!res.ok) { throw new Error('HTTP ' + res.status); }\n      return res.json();\n    })\n    .then(function (data) {\n      var list = (data && data.tapes) || [];\n      list.sort(function (a, b) { return (a.dateAdded || '').localeCompare(b.dateAdded || ''); });\n      return resolveTapeImages(list);\n    })\n    .catch(function (err) {\n      console.error('[eva-radio] failed to load tape manifest', err);\n      return [];\n    })\n    .then(function (list) {\n      tapes = list;\n      document.dispatchEvent(new CustomEvent('eva:radio:tapes:ready', { detail: { tapes: tapes } }));\n      return tapes;\n    });\n\n  var widget = null;\n  var dragHandle = null;\n  var stage = null; // locked-aspect box every deck layer (img, overlay, hubs, hotspots) positions against -- see eva-radio.css's .eva-radio-widget__stage\n  var img = null;\n  var bubble = null;\n  var bubbleText = null;\n  var bubbleAction = null;\n  var bubbleClose = null;\n  var audio = null;\n  var toggleBtns = [];\n  var minimizeBtn = null;\n  var resizeHandle = null;\n  // Mobile-overflow fix (2026-08-09) -- these two, plus resizeHandle\n  // above, are the \"satellite\" elements that stick out beyond the\n  // widget's own frame box (radioToggleEl: the vertical RADIO-mode tab\n  // on the right edge; tapeRackEl: the tape rack, which sits beside the\n  // widget on wide viewports and below it on narrow ones). clampPosition()\n  // below measures their live overflow rather than a hardcoded px/percent\n  // guess, so the clamp bounds can't drift out of sync with the art the\n  // way a hardcoded reserve value already once did (see RACK_RESERVE's\n  // own history above).\n  var radioToggleEl = null;\n  var tapeRackEl = null;\n  var onAirDots = [];\n  var angleBtn = null;\n  var angleImageCache = {}; // preloaded Image() objects, keyed by angle -- avoids a flash of blank/broken img on first cycle to each angle\n  var keyboardHotspotsSvg = null;\n  // Transport hotspots (door/play/rew/ff/stop/pause), one SVG per angle,\n  // queried into this map in ready(). Each is calibrated to its own photo\n  // (see eva-radio.liquid), so applyAngle() just shows the one matching\n  // state.angle and hides the rest -- the actions/sound/press-animation\n  // code is entirely angle-agnostic already (keyed by data-eva-radio-\n  // action + the .eva-radio-hotspot class, not by which SVG the element\n  // lives in).\n  var angleTransportHotspots = {};\n\n  // --- Angle changer ---------------------------------------------------\n  // 'front' shows the tape transport hotspots, 'above' the keyboard --\n  // left/right are retired (Per: reduce to two angles), photos kept in\n  // _source/radio/ but no longer shipped/cycled. Switching angle never\n  // touches playback -- audio keeps going if it was already playing,\n  // only the hotspot layer and speaker-viz canvas hide/show.\n\n  var ANGLES = ['front', 'above']; // left/right retired (Per: reduce to two angles) -- photos kept in _source/radio/, no longer shipped\n\n  function preloadAngleImages() {\n    var assets = window.EvaRadioAngleAssets || {};\n    ANGLES.forEach(function (a) {\n      if (!assets[a] || angleImageCache[a]) { return; }\n      var im = new Image();\n      im.src = assets[a];\n      angleImageCache[a] = im;\n    });\n  }\n\n  // Keys + tape both work at once now -- there's no separate \"mode\" to\n  // switch between (removed per Per). The angle-changer is the only thing\n  // that decides which hotspot layer is visible/interactive. Every angle\n  // (front/above/left/right) has its own transport hotspot SVG (door +\n  // play/rew/ff/stop/pause), calibrated to that angle's own photo -- see\n  // angleTransportHotspots. 'above' additionally shows the keyboard\n  // layer. Desktop keyboard shortcuts (see eva-radio-keyboard.js) are NOT\n  // gated by angle at all -- they work regardless of which photo is\n  // currently showing, since they don't need the on-image hotspots to be\n  // visible.\n  // Keeps .eva-radio-widget__stage's aspect-ratio (and therefore every\n  // overlay/hub percentage that resolves against it) matched to whichever\n  // angle photo is actually loaded -- front and above are different\n  // intrinsic ratios (1901x1057 vs. 2117x955), so this has to be re-read\n  // from the img every time applyAngle() runs, not set once. Safe to call\n  // unconditionally (idempotent) even when the src didn't change.\n  function syncStageAspect() {\n    if (!img || !stage) { return; }\n    function setFromImg() {\n      if (img.naturalWidth && img.naturalHeight) {\n        stage.style.setProperty('--eva-radio-stage-aspect', img.naturalWidth + ' / ' + img.naturalHeight);\n      }\n    }\n    if (img.complete) {\n      setFromImg();\n    } else {\n      img.addEventListener('load', setFromImg, { once: true });\n    }\n  }\n\n  function applyAngle() {\n    // A localStorage value from before left/right were retired would\n    // otherwise match no angle at all -- neither hotspot layer's display\n    // check would pass, leaving the widget uninteractive until the user\n    // happened to click the angle button once to cycle away from it.\n    if (ANGLES.indexOf(state.angle) === -1) { state.angle = 'front'; }\n    var assets = window.EvaRadioAngleAssets || {};\n    var url = assets[state.angle];\n    if (url && img && img.src !== url) { img.src = url; }\n    syncStageAspect();\n    // Both remaining angles have their own calibrated door hotspot (see\n    // eva-radio.liquid) -- this used to be hardcoded to the front angle\n    // only, which silently blocked drag/tap-insert on 'above' even after\n    // its door polygon existed. True for any angle with hotspots at all.\n    var interactive = ANGLES.indexOf(state.angle) !== -1;\n    Object.keys(angleTransportHotspots).forEach(function (angleName) {\n      var svg = angleTransportHotspots[angleName];\n      if (svg) { svg.style.display = state.angle === angleName ? '' : 'none'; }\n    });\n    if (keyboardHotspotsSvg) { keyboardHotspotsSvg.style.display = state.angle === 'above' ? '' : 'none'; }\n    document.dispatchEvent(new CustomEvent('eva:radio:angle', { detail: { angle: state.angle, interactive: interactive } }));\n  }\n\n  function cycleAngle() {\n    var idx = ANGLES.indexOf(state.angle);\n    state.angle = ANGLES[(idx + 1) % ANGLES.length];\n    applyAngle();\n    saveState();\n  }\n\n  function getTape(id) {\n    for (var i = 0; i < tapes.length; i++) {\n      if (tapes[i].id === id) { return tapes[i]; }\n    }\n    return null;\n  }\n\n  function currentTape() {\n    return state.tapeId ? getTape(state.tapeId) : null;\n  }\n\n  // Phantom-tape fix (2026-08-07): state.tapeId/sideId PERSIST in\n  // localStorage across reloads (see loadState() above) -- they really\n  // mean \"the last side ever deliberately selected,\" not \"there is\n  // physically a tape in the deck right now.\" A visitor who played a\n  // tape days ago, then closed the tab, gets a fresh page load where the\n  // RACK visually shows nothing inserted (eva-radio-tape.js's own\n  // deckTapeId is in-memory only, never persisted, always starts null)\n  // -- but state.tapeId/sideId are still sitting there from that old\n  // session, so playWithSound() (the transport PLAY hotspot) would\n  // happily resume/start that stale tape's audio. Per's rule: \"empty\n  // deck = PLAY produces no music, full stop\" -- and per the rack's own\n  // deckTapeId, the deck genuinely IS empty until a real insert happens\n  // THIS session. deckLoaded is that missing signal: false at every\n  // fresh module load regardless of what's persisted, flipped true only\n  // by an actual insert (selectTapeSideAndPlay(), reached via the rack's\n  // drag/PLAY-button/Enter through the public playTape() API) or by the\n  // hard-reload resume arming itself (armHardLoadResume() below) --\n  // which is the one legitimate case of \"a tape really was left playing,\n  // resume it,\" not a stale leftover. See playWithSound()'s own gate.\n  var deckLoaded = false;\n\n  function sideTracks(tape, sideId) {\n    return (tape && tape.sides && tape.sides[sideId]) || [];\n  }\n\n  function trackAt(tape, sideId, index) {\n    return sideTracks(tape, sideId)[index] || null;\n  }\n\n  // --- Sound-engine ducking ---------------------------------------------\n  // This turns the UI hover/click sound VOLUME down while a tape is\n  // audibly playing, via a small API sound-engine.js exposes on\n  // window.EvaSoundEngine for exactly this purpose. Separate concern from\n  // the global mute (see EvaMute.applyToElement(audio) in init() below) --\n  // ducking is \"quieter while something else plays,\" muting is \"off\n  // entirely,\" same distinction sound-engine.js's own duckFactor comment\n  // draws.\n  //\n  // Also dispatches eva:radio:playstate: the radio widget lives outside\n  // #MainContent and keeps playing straight through a PJAX navigation\n  // (nothing about the swap ever touches it), which means it can end up\n  // playing at the same time as a page-specific ambience loop -- the\n  // K\u00E4llare basement hum, currently the only other real-audio loop in\n  // the theme -- with nothing coordinating the two. Broadcasting play/\n  // stop as an event (rather than reaching into that block's internals\n  // directly) keeps the radio widget from needing to know that page's\n  // ambience even exists; the page listens in, not the other way round.\n  function duckUiSound() {\n    if (window.EvaSoundEngine && typeof window.EvaSoundEngine.duck === 'function') {\n      window.EvaSoundEngine.duck(0.3);\n    }\n    document.dispatchEvent(new CustomEvent('eva:radio:playstate', { detail: { playing: true } }));\n    document.dispatchEvent(new CustomEvent('eva:radio-play')); // Bolty's vibe-state hook (assets/bolty.js) -- separate from playstate above, kept minimal on purpose\n    updateOnAirDot();\n  }\n\n  // reason ('pause' | 'stop') lets listeners (eva-radio-tape.js's deck\n  // overlay) tell the two apart -- Per: pause freezes the inside-deck art\n  // in place, stop fades it out. Both look identical from here (audio.\n  // pause() either way) so the caller has to say which one this is.\n  function restoreUiSound(reason) {\n    if (window.EvaSoundEngine && typeof window.EvaSoundEngine.restore === 'function') {\n      window.EvaSoundEngine.restore();\n    }\n    document.dispatchEvent(new CustomEvent('eva:radio:playstate', { detail: { playing: false, reason: reason } }));\n    document.dispatchEvent(new CustomEvent('eva:radio-stop')); // Bolty's vibe-state hook (assets/bolty.js)\n    updateOnAirDot();\n  }\n\n  // --- Media Session (lock-screen / control-center now-playing) ---------\n  // Added 2026-08-09 -- navigator.mediaSession didn't exist ANYWHERE in\n  // this codebase before this pass (confirmed by a full-repo search), so\n  // the lock screen was falling back to document.title, which Shopify's\n  // own seo-title.liquid renders as \"Eva \u2013 Eva Hardware\" on the\n  // homepage -- not a mediaSession bug to fix, a mediaSession to add.\n  // Mirrors eva-radio-station.js's own copy of this same pattern for\n  // stations -- the two never fight over navigator.mediaSession because\n  // they're already mutually exclusive by the SAME existing architecture\n  // that keeps their audio exclusive (see that file's header comment):\n  // whichever one is actually playing is always the one that most\n  // recently wrote the metadata, no shared coordinator needed.\n  //\n  // Hooked from the audio element's own 'play' event (below), which\n  // ALREADY fires for every case playback (re)starts -- a fresh\n  // selectTapeSideAndPlay(), nextTrack() advancing within a side, and\n  // flipToSide() all end in a call to play(), which fires this natively\n  // -- so one hook point covers \"metadata must update on tape change and\n  // side flip\" for free, no separate hooks needed at each call site.\n  var mediaSessionSupported = 'mediaSession' in navigator;\n\n  function updateTapeMediaSession() {\n    if (!mediaSessionSupported) { return; }\n    var tape = currentTape();\n    if (!tape) { return; }\n    // \"include the side (A/B) if it fits naturally\" -- appended to the\n    // artist field since MediaMetadata has no dedicated side/episode\n    // field, and artist is where a station's own name goes on the other\n    // path, so putting the tape's own identity there too keeps the two\n    // consistent (title always says \"Eva Radio\", artist always says\n    // \"what's actually playing\").\n    var sideLabel = state.sideId ? (' \u2014 Side ' + state.sideId.toUpperCase()) : '';\n    try {\n      navigator.mediaSession.metadata = new MediaMetadata({\n        title: 'Eva Radio',\n        artist: (tape.title || 'Eva Radio') + sideLabel,\n        // ITEM 3 AMENDMENT (2026-08-09): a dedicated square Eva Radio\n        // artwork now, NOT the tape's own cover (tape.images.front) --\n        // per-tape cover art is explicitly NOT wired here yet (tape\n        // covers are portrait, the lock-screen slot is square, and the\n        // crop/pad treatment is Per's own call to make later, not\n        // assumed here). Same array used on the station side -- see\n        // window.EvaRadioArtwork's own comment in theme.liquid.\n        artwork: window.EvaRadioArtwork || []\n      });\n      navigator.mediaSession.playbackState = 'playing';\n    } catch (e) {}\n  }\n\n  function setTapeMediaSessionPlaybackState(playbackState) {\n    if (!mediaSessionSupported) { return; }\n    try { navigator.mediaSession.playbackState = playbackState; } catch (e) {}\n  }\n\n  // Only a genuine STOP clears the lock-screen card entirely -- a PAUSE\n  // (or a side simply ending) leaves the metadata in place and just\n  // flips playbackState, same as any real media app's lock-screen\n  // behavior (pausing a song doesn't make its card disappear).\n  function clearTapeMediaSession() {\n    if (!mediaSessionSupported) { return; }\n    try {\n      navigator.mediaSession.metadata = null;\n      navigator.mediaSession.playbackState = 'none';\n    } catch (e) {}\n  }\n\n  // --- Mechanical tape sounds (radio's own fixed-gain channel) -----------\n  // Deliberately NOT routed through sound-engine.js's playSound()/\n  // duckFactor -- these are the radio's own foreground sound effects,\n  // not ambient UI blips, and don't get ducked by anything (they ARE\n  // what's in the foreground when they play). They DO respect the global\n  // mute now (EvaMute.applyToElement, see initMechSounds() below) --\n  // \"Eva Radio: music playback AND all mechanical sounds\" per the\n  // 2026-08-02 global-mute unification, superseding the exemption this\n  // comment used to describe.\n  //\n  // RADIO_MECH_GAIN calibrated 2026-07-18 against the site's two\n  // existing UI blip assets (sound-engine.js's hover/click, both played\n  // at code-level volume 0.12 there): raw-file peak levels measured via\n  // ffmpeg astats --\n  //   ui-hover.mp3 peak -26.1dB, ui-click.mp3 peak -40.9dB\n  //   eva_ui_tape_play/stop.wav peak -1.4dB, autostop -3.0dB, flip -3.8dB\n  //     (average ~-2.4dB)\n  // ~24-30dB louder than the existing assets at raw file level -- far\n  // past the 4dB comfort threshold, so per spec the CHANNEL gain (not\n  // the files) is scaled down, not re-encoded. Targeted the louder of\n  // the two references (hover, -26dB) rather than click's near-silent\n  // -41dB, since these are explicitly foreground mechanical cues, not\n  // ambient blips -- matching hover's raw peak keeps them clearly\n  // audible as intended while sitting in the same loudness neighborhood\n  // as the loudest thing already on the page. 0.07 brings the four new\n  // sounds to a peak of about -24.5dB to -26.9dB, within ~1.5dB of\n  // hover's own -26.1dB.\n  //\n  // Note this compares raw FILE peaks, not \"effective in-practice\"\n  // loudness after hover/click's own 0.12 code-level volume is applied\n  // (which would put their in-practice peak near -44dB to -59dB --\n  // matching THAT would make the tape sounds nearly inaudible, at odds\n  // with the spec's own framing of them as \"the foreground\"). Flagged\n  // here in case Per wants the alternate, much-quieter target instead.\n  var RADIO_MECH_GAIN = 0.07;\n  var RADIO_MECH_FF_REW_GAIN = RADIO_MECH_GAIN * 0.5; // \"reuse the play clack at lower gain (0.5)\" -- half of the tape channel's own base gain, not an absolute 0.5\n\n  var mechPlayAudio = null;\n  var mechStopAudio = null;\n  var mechAutostopAudio = null;\n  var mechFlipAudio = null;\n  // Hold-seek sounds (FFW/REW wind while held, dedicated tap clacks) --\n  // ONE pair of sounds covers BOTH directions: the reversed-audio\n  // rew-specific files played noticeably quieter/duller than the\n  // forward ones, so direction is communicated by the music itself\n  // scrubbing backwards, not by a different wind sound. Per is still\n  // preparing these two files; keys resolve to a 404 URL until they\n  // land in assets/, which just means these no-op silently (play()\n  // promise rejects, caught below) rather than break anything.\n  var mechFfwWindAudio = null;\n  var mechFfwTapAudio = null;\n\n  function initMechSounds() {\n    var assets = window.EvaRadioAssets || {};\n    try {\n      if (assets.mechPlay) { mechPlayAudio = new Audio(assets.mechPlay); mechPlayAudio.preload = 'auto'; }\n      if (assets.mechStop) { mechStopAudio = new Audio(assets.mechStop); mechStopAudio.preload = 'auto'; }\n      if (assets.mechAutostop) { mechAutostopAudio = new Audio(assets.mechAutostop); mechAutostopAudio.preload = 'auto'; }\n      if (assets.mechFlip) { mechFlipAudio = new Audio(assets.mechFlip); mechFlipAudio.preload = 'auto'; }\n      // mechFfwTap's own fallback (mechFfwTapAudio || mechPlayAudio, see\n      // onSeekPointerUp) only works if a load failure actually nulls the\n      // reference out -- a `new Audio(url)` for a URL that 404s is still\n      // a truthy object right up until the browser fires its own 'error'\n      // event, so without this listener the `||` never sees a falsy left\n      // side and REW/FF's tap both silently try to play a broken file\n      // forever instead of ever reaching the working play-clack fallback.\n      if (assets.mechFfwWind) {\n        mechFfwWindAudio = new Audio(assets.mechFfwWind);\n        mechFfwWindAudio.preload = 'auto';\n        mechFfwWindAudio.addEventListener('error', function () { mechFfwWindAudio = null; });\n      }\n      if (assets.mechFfwTap) {\n        mechFfwTapAudio = new Audio(assets.mechFfwTap);\n        mechFfwTapAudio.preload = 'auto';\n        mechFfwTapAudio.addEventListener('error', function () { mechFfwTapAudio = null; });\n      }\n      if (window.EvaMute) {\n        [mechPlayAudio, mechStopAudio, mechAutostopAudio, mechFlipAudio, mechFfwWindAudio, mechFfwTapAudio].forEach(function (a) {\n          window.EvaMute.applyToElement(a);\n        });\n      }\n    } catch (e) {}\n  }\n\n  // The 'error' listeners above null out mechFfwWindAudio/mechFfwTapAudio\n  // once a load failure actually happens, but that's async -- an element\n  // can still be a truthy, not-yet-failed Audio() in the brief window\n  // before its own network request resolves. This is the synchronous\n  // half of the same check: MediaError.prototype.error is set the moment\n  // a failure is known, no event-loop turn required, so a tap/hold that\n  // lands in that early window still gets the right answer instead of a\n  // silent failed play() attempt.\n  function workingAudio(mechAudio) {\n    return (mechAudio && !mechAudio.error) ? mechAudio : null;\n  }\n\n  function playMech(mechAudio, gain) {\n    if (!mechAudio) { return; }\n    try {\n      mechAudio.loop = false;\n      mechAudio.pause();\n      mechAudio.currentTime = 0;\n      mechAudio.volume = gain;\n      var p = mechAudio.play();\n      if (p && typeof p.catch === 'function') { p.catch(function () {}); }\n    } catch (e) {}\n  }\n\n  // Continuous wind sound while FFW/REW is held -- loops until stopMechLoop.\n  function startMechLoop(mechAudio, gain) {\n    if (!mechAudio) { return; }\n    try {\n      mechAudio.loop = true;\n      mechAudio.currentTime = 0;\n      mechAudio.volume = gain;\n      var p = mechAudio.play();\n      if (p && typeof p.catch === 'function') { p.catch(function () {}); }\n    } catch (e) {}\n  }\n\n  function stopMechLoop(mechAudio) {\n    if (!mechAudio) { return; }\n    try {\n      mechAudio.loop = false;\n      mechAudio.pause();\n      mechAudio.currentTime = 0;\n    } catch (e) {}\n  }\n\n  // --- Positioning / drag -------------------------------------------------\n\n  // Viewport dimensions, not document (2026-08-09) -- position:fixed means\n  // x/y are viewport coordinates. This is also what actually fixes the\n  // mobile overflow bug: window.innerWidth/innerHeight are properties of\n  // the SCREEN, not of the page's own content, so they can't be inflated\n  // by the very element being clamped against them the way\n  // document.documentElement.scrollWidth could (see eva-radio.css's own\n  // comment on .eva-radio-widget for the full incident writeup -- that\n  // circularity, not merely \"absolute vs fixed\" in the abstract, was the\n  // actual mechanism of the bug).\n  function viewportWidth() {\n    return window.innerWidth;\n  }\n  function viewportHeight() {\n    return window.innerHeight;\n  }\n\n  // How far the resize handle, the RADIO mode-toggle tab, and (when\n  // rendered) the tape rack stick out beyond the widget's OWN frame box,\n  // on each side -- measured live via getBoundingClientRect() rather than\n  // hardcoded px/percent guesses, so a future art/layout change can't\n  // silently desync the clamp bounds the way a hardcoded reserve value\n  // already once did for the rack (see RACK_RESERVE's own history/\n  // comment below). Each of these three elements is CSS-positioned\n  // relative to the widget's own box (percentage/anchor offsets, not\n  // page coordinates), so the delta between its rect and the widget's is\n  // the same regardless of where the widget currently sits on screen --\n  // safe to measure at the widget's CURRENT position and reuse for a\n  // CANDIDATE position in clampPosition() below. An element that isn't\n  // actually rendered (hidden, display:none, or the rack tucked away at\n  // a breakpoint where it needs no separate reserve) reports an all-zero\n  // rect and is skipped automatically -- no separate visibility check\n  // needed.\n  function measureOverflow() {\n    var wRect = widget.getBoundingClientRect();\n    var overflow = { left: 0, right: 0, top: 0, bottom: 0 };\n    [resizeHandle, radioToggleEl, tapeRackEl].forEach(function (el) {\n      if (!el) { return; }\n      var r = el.getBoundingClientRect();\n      if (!r.width || !r.height) { return; }\n      overflow.left = Math.max(overflow.left, wRect.left - r.left);\n      overflow.right = Math.max(overflow.right, r.right - wRect.right);\n      overflow.top = Math.max(overflow.top, wRect.top - r.top);\n      overflow.bottom = Math.max(overflow.bottom, r.bottom - wRect.bottom);\n    });\n    return overflow;\n  }\n\n  // Keeps the FULL assembly (widget + resize handle + RADIO tab + open\n  // tape rack) fully inside the viewport on all four sides -- not just\n  // \"MIN_VISIBLE px of the widget itself,\" which is what let the old\n  // document-relative version drift the widget (and the page along with\n  // it) past the screen edge. Called on every drag/resize pointermove\n  // tick (not just on release) -- overflow during the drag, even for a\n  // single frame, is enough to trigger iOS's shrink-to-fit zoom, so the\n  // clamp has to hold continuously, not just at the end.\n  function clampPosition(x, y) {\n    var w = widget.offsetWidth;\n    var h = widget.offsetHeight;\n    var overflow = measureOverflow();\n    var minX = overflow.left;\n    var maxX = viewportWidth() - w - overflow.right;\n    var minY = overflow.top;\n    var maxY = viewportHeight() - h - overflow.bottom;\n    return {\n      // Math.max(minX, ...) wins if the assembly is wider than the\n      // viewport itself (e.g. an extreme resize on a tiny screen) --\n      // pins to the left/top-most valid edge rather than oscillating.\n      // Resize clamping below keeps this a defensive fallback, not the\n      // normal case.\n      x: Math.max(minX, Math.min(maxX, x)),\n      y: Math.max(minY, Math.min(maxY, y))\n    };\n  }\n\n  // Fresh-visitor spawn: right side, below the header, clear of hero\n  // CTAs -- anchored to the header's own bottom edge. viewport-relative\n  // now (no more `+ window.scrollY`, 2026-08-09) to match position:fixed\n  // -- a spawn computed in page coordinates would place the widget\n  // wherever the header happened to be in the DOCUMENT, but a fixed\n  // element's top/left are resolved against the VIEWPORT, so adding the\n  // scroll offset would double-count it (this only ever mattered at\n  // scrollY > 0, which first paint never is in practice, but the old\n  // formula was wrong for the new coordinate system either way).\n  function headerBottom() {\n    var header = document.querySelector('.site-header, header[role=\"banner\"], #shopify-section-header-group');\n    if (!header) { return 140; }\n    var rect = header.getBoundingClientRect();\n    return rect.bottom;\n  }\n\n  function defaultPosition() {\n    var w = widget.offsetWidth || 340;\n    // Only reserve room for the rack above the same 700px breakpoint\n    // eva-radio.css uses to move the rack BELOW the widget instead of\n    // beside it (see RACK_RESERVE's comment) -- below that breakpoint\n    // the rack already needs no extra horizontal room, so reserving it\n    // there would just push the widget needlessly further left on\n    // small/narrow viewports for no reason.\n    var reserve = window.innerWidth > 700 ? RACK_RESERVE : 0;\n    return {\n      x: viewportWidth() - w - MARGIN - reserve,\n      y: headerBottom() + MARGIN\n    };\n  }\n\n  function applyPosition(x, y) {\n    widget.style.left = x + 'px';\n    widget.style.top = y + 'px';\n    widget.style.right = 'auto';\n    widget.style.bottom = 'auto';\n  }\n\n  function applyWidth() {\n    if (state.width) {\n      widget.style.width = state.width + 'px';\n    }\n    // else: leave the CSS default (responsive) in place.\n  }\n\n  function positionWidget() {\n    applyWidth();\n    var pos;\n    if (state.userMoved && state.x !== null && state.y !== null) {\n      pos = clampPosition(state.x, state.y);\n    } else {\n      pos = defaultPosition();\n    }\n    state.x = pos.x;\n    state.y = pos.y;\n    applyPosition(pos.x, pos.y);\n  }\n\n  var dragging = false;\n  var dragMoved = false;\n  var dragStartX = 0;\n  var dragStartY = 0;\n  var widgetStartX = 0;\n  var widgetStartY = 0;\n  var activePointerId = null;\n\n  function onPointerDown(e) {\n    // Hotspots and the bubble/minimize/resize controls handle their own\n    // clicks -- don't let a drag start swallow those taps.\n    if (e.target.closest && e.target.closest('.eva-radio-hotspot, .eva-radio-widget__bubble-close, .eva-radio-widget__bubble-action, .eva-radio-widget__minimize, .eva-radio-widget__resize-handle')) {\n      return;\n    }\n    dragging = true;\n    dragMoved = false;\n    activePointerId = e.pointerId;\n    dragStartX = e.clientX;\n    dragStartY = e.clientY;\n    widgetStartX = state.x;\n    widgetStartY = state.y;\n    try { dragHandle.setPointerCapture(e.pointerId); } catch (err) {}\n  }\n\n  function onPointerMove(e) {\n    if (!dragging || e.pointerId !== activePointerId) { return; }\n    var dx = e.clientX - dragStartX;\n    var dy = e.clientY - dragStartY;\n    if (!dragMoved && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {\n      dragMoved = true;\n    }\n    if (!dragMoved) { return; }\n    var pos = clampPosition(widgetStartX + dx, widgetStartY + dy);\n    state.x = pos.x;\n    state.y = pos.y;\n    applyPosition(pos.x, pos.y);\n  }\n\n  function onPointerUp(e) {\n    if (!dragging || e.pointerId !== activePointerId) { return; }\n    dragging = false;\n    activePointerId = null;\n    try { dragHandle.releasePointerCapture(e.pointerId); } catch (err) {}\n    if (dragMoved) {\n      state.userMoved = true;\n      saveState();\n    }\n  }\n\n  // --- Resize -------------------------------------------------------\n\n  var resizing = false;\n  var resizeStartX = 0;\n  var resizeStartWidth = 0;\n  var resizeActivePointerId = null;\n\n  function onResizePointerDown(e) {\n    resizing = true;\n    resizeActivePointerId = e.pointerId;\n    resizeStartX = e.clientX;\n    resizeStartWidth = widget.offsetWidth;\n    try { resizeHandle.setPointerCapture(e.pointerId); } catch (err) {}\n    // Rack-desync fix (2026-08-06): tells eva-radio-tape.js a continuous\n    // scale gesture is starting, so it can suppress its own CSS\n    // transitions and drive tape sizing/alignment off a live rAF loop\n    // instead -- see that file's own eva:radio:resize listener for why\n    // (transitionend alone only fires on settle, never mid-drag).\n    document.dispatchEvent(new CustomEvent('eva:radio:resize', { detail: { active: true } }));\n  }\n\n  function onResizePointerMove(e) {\n    if (!resizing || e.pointerId !== resizeActivePointerId) { return; }\n    var dx = e.clientX - resizeStartX;\n    var newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStartWidth + dx));\n    // Spec: \"the widget cannot be resized larger than the viewport\" --\n    // caps width so that, even fully repositioned against the nearest\n    // edge, the assembly (widget + resize handle/RADIO tab overflow)\n    // still fits. Reuses the PRE-resize overflow measurement as a close\n    // approximation -- these satellites' offsets are small relative to\n    // the widget's own width and don't meaningfully shift across a\n    // single resize gesture, so re-deriving them from a not-yet-applied\n    // width isn't worth the extra complexity.\n    var overflow = measureOverflow();\n    var maxWidthForViewport = Math.max(MIN_WIDTH, viewportWidth() - overflow.left - overflow.right);\n    newWidth = Math.min(newWidth, maxWidthForViewport);\n    widget.style.width = newWidth + 'px';\n    // Hotspots are %-based against the image's own aspect ratio, which\n    // cascades from this width automatically (img width:100% -> height\n    // auto via intrinsic ratio -> hotspot SVG height:100% of that) --\n    // no separate height/hotspot recalculation needed here. Tape rack\n    // sizing/alignment tracks this via its own rAF loop (see\n    // eva-radio-tape.js's eva:radio:resize listener) rather than being\n    // driven from here directly -- keeps this module ignorant of the\n    // rack subsystem, same separation as every other cross-file event.\n\n    // Clamp on every tick, not only on release (spec) -- growing the box\n    // can push its right/bottom edge past the viewport before MAX_WIDTH\n    // (or the cap above) is even reached, if the widget was already\n    // parked near an edge. Reuses the same clampPosition() drag uses.\n    var pos = clampPosition(state.x, state.y);\n    state.x = pos.x;\n    state.y = pos.y;\n    applyPosition(pos.x, pos.y);\n  }\n\n  function onResizePointerUp(e) {\n    if (!resizing || e.pointerId !== resizeActivePointerId) { return; }\n    resizing = false;\n    resizeActivePointerId = null;\n    try { resizeHandle.releasePointerCapture(e.pointerId); } catch (err) {}\n    document.dispatchEvent(new CustomEvent('eva:radio:resize', { detail: { active: false } }));\n    state.width = widget.offsetWidth;\n    // Already clamped on every tick above -- this is just the final\n    // state-persistence snapshot, not load-bearing for overflow.\n    var pos = clampPosition(state.x, state.y);\n    state.x = pos.x;\n    state.y = pos.y;\n    applyPosition(pos.x, pos.y);\n    saveState();\n  }\n\n  // Re-clamps both size and position against the CURRENT viewport --\n  // shared by the window resize listener and the orientationchange\n  // listener below (spec: \"so a widget parked at the right edge in\n  // landscape does not sit offscreen in portrait\"). Width can genuinely\n  // need to SHRINK here (not just reposition) -- a widget resized to\n  // 560px wide on a landscape viewport can no longer fit at all on a\n  // narrower portrait one, however far left it's pushed.\n  function reclampWidgetToViewport() {\n    if (!widget || widget.hidden) { return; }\n    var overflow = measureOverflow();\n    var maxWidthForViewport = Math.max(MIN_WIDTH, viewportWidth() - overflow.left - overflow.right);\n    if (widget.offsetWidth > maxWidthForViewport) {\n      widget.style.width = maxWidthForViewport + 'px';\n      state.width = widget.offsetWidth;\n    }\n    var pos = clampPosition(state.x, state.y);\n    state.x = pos.x;\n    state.y = pos.y;\n    applyPosition(pos.x, pos.y);\n    saveState();\n  }\n\n  window.addEventListener('resize', reclampWidgetToViewport);\n  // Some browsers fire 'resize' late/inconsistently around an\n  // orientation flip (the visual viewport can settle a frame or two\n  // after 'resize' already fired with stale dimensions) -- a dedicated\n  // listener plus a short follow-up timeout covers both without\n  // depending on either firing in a particular order.\n  window.addEventListener('orientationchange', function () {\n    reclampWidgetToViewport();\n    setTimeout(reclampWidgetToViewport, 300);\n  });\n\n  // --- Open / close --------------------------------------------------\n\n  function updateToggleButtonsUI() {\n    toggleBtns.forEach(function (btn) {\n      btn.setAttribute('aria-pressed', state.minimized ? 'false' : 'true');\n    });\n  }\n\n  // On-air dot: the only header-visible sign a tape is still playing\n  // once the widget chassis itself is minimized. Never shown while the\n  // widget is visible (restored) -- the widget itself is evidence\n  // enough there, this is specifically for the minimized+playing case.\n  function updateOnAirDot() {\n    var showDot = state.playing && state.minimized;\n    onAirDots.forEach(function (dot) {\n      dot.hidden = !showDot;\n    });\n  }\n\n  function restoreWidget() {\n    state.minimized = false;\n    widget.hidden = false;\n    positionWidget();\n    updateToggleButtonsUI();\n    updateOnAirDot();\n    saveState();\n    // Phase 2 visibility model (2026-08-04): \"tapes shown on open\" --\n    // eva-radio-tape.js owns the tape rack directly and listens for this\n    // to reset any hidden-while-playing state, same cross-file event\n    // pattern eva:radio:playstate/eva:radio:angle already use.\n    document.dispatchEvent(new CustomEvent('eva:radio:opened'));\n  }\n\n  function minimizeWidget() {\n    state.minimized = true;\n    widget.hidden = true;\n    updateToggleButtonsUI();\n    updateOnAirDot();\n    saveState();\n  }\n\n  function toggleMinimized() {\n    if (state.minimized) { restoreWidget(); } else { minimizeWidget(); }\n  }\n\n  // --- Speech bubble: end-of-side prompt only ---------------------------\n  // The old persistent first-open \"info\" hint is gone -- all control\n  // hints are hover tooltips now (see the tooltip chip below). This\n  // bubble is purely event-driven: it only ever shows the end-of-side\n  // flip prompt.\n\n  function hideBubble() {\n    bubble.hidden = true;\n    bubbleAction.hidden = true;\n    bubbleAction.onclick = null;\n  }\n\n  function dismissBubble() {\n    hideBubble();\n  }\n\n  // End-of-side: the physical gesture (eject -> flip -> reinsert, see\n  // eva-radio-tape.js) is the PRIMARY path now, so this is informational\n  // rather than a question -- the button is a small accessible fallback\n  // for anyone who can't do the drag/tap gesture, not the main call to\n  // action.\n  function showSidePrompt(sideId) {\n    if (sideId === 'a') {\n      bubbleText.innerHTML = 'End of Side A &mdash; flip the tape.';\n      bubbleAction.textContent = '[ flip & play Side B ]';\n      bubbleAction.onclick = function () { flipToSide('b'); };\n    } else {\n      bubbleText.innerHTML = 'End of Side B &mdash; flip the tape.';\n      bubbleAction.textContent = '[ flip & play Side A ]';\n      bubbleAction.onclick = function () { flipToSide('a'); };\n    }\n    bubbleAction.hidden = false;\n    bubble.hidden = false;\n  }\n\n  // --- Tooltip chip -------------------------------------------------------\n  // One reusable element for every control hint (deck buttons, door, tone\n  // toggle, rhythm presets, the tape object) -- shown/positioned against\n  // whichever element is currently hovered/touched, driven by a single\n  // delegated listener pair below rather than per-control wiring. Exposed\n  // on window.EvaRadioWidget so eva-radio-tape.js's tape object (a\n  // different file, outside dragHandle) can reuse the same chip.\n  var tooltipEl = null;\n  var tooltipTextEl = null;\n  var TOOLTIP_OFFSET = 8;\n  var tooltipTouchedOnce = null; // Set of elements a touch tooltip already fired for this page load\n\n  function positionTooltip(targetRect) {\n    var tw = tooltipEl.offsetWidth;\n    var th = tooltipEl.offsetHeight;\n    var left = targetRect.left + targetRect.width / 2 - tw / 2;\n    left = Math.max(4, Math.min(left, window.innerWidth - tw - 4));\n    var top = targetRect.bottom + TOOLTIP_OFFSET;\n    if (top + th > window.innerHeight - 4) {\n      top = targetRect.top - th - TOOLTIP_OFFSET; // flip above -- only when there's not enough viewport room below\n    }\n    tooltipEl.style.left = left + 'px';\n    tooltipEl.style.top = top + 'px';\n  }\n\n  function showTooltip(targetEl, text) {\n    if (!tooltipEl || !text) { return; }\n    tooltipTextEl.textContent = text;\n    tooltipEl.hidden = false;\n    positionTooltip(targetEl.getBoundingClientRect());\n  }\n\n  function hideTooltip() {\n    if (tooltipEl) { tooltipEl.hidden = true; }\n  }\n\n  // --- Cassette door -----------------------------------------------------\n  // Per: pressing the door must never pop up a list -- it only opens or\n  // closes the tape rack (2026-08-09: a real toggle, same target both\n  // ways; previously only ever opened, same as the rack's own hatch\n  // tab), nothing else. The rack itself lives in eva-radio-tape.js\n  // (see its own toggleRackFromDoor()), so this just broadcasts the\n  // request rather than reaching into that module directly.\n  function requestShowRack() {\n    hideBubble(); // pressing the door counts as engaging with the widget\n    document.dispatchEvent(new CustomEvent('eva:radio:door:pressed'));\n  }\n\n  // --- Playback ----------------------------------------------------------\n\n  function loadTrack(tape, sideId, index, resumeSeconds) {\n    var track = trackAt(tape, sideId, index);\n    if (!track || !track.url) { return false; }\n    audio.src = track.url;\n    audio.currentTime = resumeSeconds || 0;\n    currentTrackGainDb = typeof track.gainDb === 'number' ? track.gainDb : 0;\n    updateAudioVolume(); // apply the new track's loudness-normalization gain immediately\n    return true;\n  }\n\n  // Selects a side and starts it playing immediately, with NO mechanical\n  // play-clack of its own -- used both for a fresh rack insert (which DOES\n  // get the clack, via selectTapeSide() below calling this after the\n  // clack) and for the flip sequence (which must NOT double up on a\n  // clack, since eva_ui_tape_flip.wav already contains its own play-press\n  // sound per spec).\n  function selectTapeSideAndPlay(tapeId, sideId) {\n    var tape = getTape(tapeId);\n    if (!tape) { return; }\n    deckLoaded = true; // a genuine insert THIS session -- see deckLoaded's own comment\n    state.tapeId = tapeId;\n    state.sideId = sideId;\n    state.trackIndex = 0;\n    state.positionSec = 0;\n    // The tape object (eva-radio-tape.js) tracks its own flipped-face state\n    // locally, but the accessible flip-and-play bubble fallback changes\n    // the side through THIS function too, not through the tape object's\n    // own insert gesture -- broadcast so the physical tape stays visually\n    // in sync with whichever side is actually playing.\n    document.dispatchEvent(new CustomEvent('eva:radio:tape:side', { detail: { tapeId: tapeId, sideId: sideId } }));\n    var trackList = sideTracks(tape, sideId);\n    if (!trackList.length) {\n      // Empty/not-yet-populated side: remember the selection, nothing to play.\n      saveState();\n      return;\n    }\n    loadTrack(tape, sideId, 0, 0);\n    play();\n  }\n\n  function selectTapeSide(tapeId, sideId) {\n    dismissBubble();\n    playMech(mechPlayAudio, RADIO_MECH_GAIN);\n    setTimeout(function () {\n      selectTapeSideAndPlay(tapeId, sideId);\n    }, PLAY_SOUND_LEAD_MS);\n  }\n\n  function play() {\n    var tape = currentTape();\n    if (!tape || !state.sideId) { return; } // nothing selected -- graceful no-op\n    if (!sideTracks(tape, state.sideId).length) { return; }\n    if (!audio.src) {\n      loadTrack(tape, state.sideId, state.trackIndex, state.positionSec);\n    }\n    var p = audio.play();\n    if (p && typeof p.catch === 'function') { p.catch(function () {}); }\n  }\n\n  // PLAY hotspot specifically (resuming an already-selected side): clack\n  // first, music enters ~150ms into it. A fresh rack insert gets the\n  // same clack via selectTapeSide() above instead.\n  function playWithSound() {\n    var tape = currentTape();\n    if (!tape || !state.sideId) { return; }\n    playMech(mechPlayAudio, RADIO_MECH_GAIN);\n    // Phantom-tape fix (2026-08-07): `tape` above only proves state.tapeId\n    // is set, which can be a stale leftover from a PAST session with\n    // nothing genuinely inserted THIS one -- see deckLoaded's own\n    // comment. Per's rule: empty deck = PLAY produces no music, at MOST\n    // a mechanical click -- the clack above still plays either way (the\n    // deck acknowledges the press, same as a real one would), but the\n    // actual music is gated here.\n    if (!deckLoaded) { return; }\n    setTimeout(play, PLAY_SOUND_LEAD_MS);\n  }\n\n  function pause() {\n    cancelSeekHold(); // never leave the deck stuck sped-up/ducked if pause lands mid-hold\n    audio.pause();\n    state.playing = false;\n    state.positionSec = audio.currentTime || 0;\n    restoreUiSound('pause');\n    saveState();\n  }\n\n  function pauseWithSound() {\n    playMech(mechStopAudio, RADIO_MECH_GAIN);\n    pause();\n  }\n\n  function stop() {\n    cancelSeekHold();\n    audio.pause();\n    audio.currentTime = 0;\n    state.playing = false;\n    state.positionSec = 0;\n    restoreUiSound('stop');\n    clearTapeMediaSession();\n    saveState();\n  }\n\n  function stopWithSound() {\n    playMech(mechStopAudio, RADIO_MECH_GAIN);\n    stop();\n  }\n\n  function seek(deltaSeconds) {\n    var tape = currentTape();\n    if (!tape || !state.sideId || !audio.src) { return; }\n    var next = audio.currentTime + deltaSeconds;\n    if (next < 0) { next = 0; } // REW at/near the start of a track just restarts it\n    if (audio.duration && next > audio.duration) {\n      nextTrack(); // stays within the side -- nextTrack() itself won't cross a side boundary\n      return;\n    }\n    audio.currentTime = next;\n  }\n\n  // --- FF / REW: tap (fixed step) vs hold (authentic tape scrub) --------\n  // A tap is a single fixed jump. A HOLD keeps the tape \"moving\": FFW\n  // keeps the music audible sped up (pitch rising, like a real deck\n  // freewheeling), REW effectively lifts the head (ducked near-silent)\n  // while scrubbing backwards in visible chunks. Distinguished purely by\n  // how long the hotspot stays pressed -- SEEK_HOLD_THRESHOLD_MS decides\n  // which one a given press turns out to be, decided at release, not at\n  // press time.\n  var seekHoldTimer = null;\n  var seekHoldAction = null; // null | 'ff' | 'rew'\n  var seekHoldPointerId = null;\n  var rewScrubInterval = null;\n\n  // Composed tape volume: FFW/REW's near-silent duck always wins while\n  // held (matches prior behavior exactly); otherwise the light keyboard\n  // duck applies if any key is currently held, else full volume. Keeping\n  // this as one function (rather than each caller setting audio.volume\n  // directly) avoids the two ducks racing to stomp on each other's\n  // restore -- e.g. releasing a key mid-FFW-hold must not un-duck FFW.\n  // The per-track gainDb (EBU R128 loudness-normalization, set by\n  // loadTrack()) is applied multiplicatively on top of whichever duck\n  // wins, so a quiet-mastered track isn't left buried under a duck sized\n  // for full-volume playback.\n  var keyboardDucking = false;\n  var currentTrackGainDb = 0;\n  function dbToLinear(db) { return Math.pow(10, db / 20); }\n  function updateAudioVolume() {\n    var trackGain = dbToLinear(currentTrackGainDb || 0);\n    var base;\n    if (seekHoldAction === 'ff') { base = FFW_DUCK_VOLUME; }\n    else if (seekHoldAction === 'rew') { base = REW_DUCK_VOLUME; }\n    else { base = keyboardDucking ? KEYBOARD_DUCK_VOLUME : 1; }\n    audio.volume = Math.max(0, Math.min(1, base * trackGain));\n  }\n\n  // Lets eva-radio-tape.js sync the spool hubs to the transport's actual\n  // state (fast-forward/rewind spin during a hold, back to normal\n  // play/pause/stop once released) -- see that file's updateDeckWindowEffects().\n  function dispatchSeekState(action) {\n    document.dispatchEvent(new CustomEvent('eva:radio:seekstate', { detail: { action: action } }));\n  }\n\n  function beginSeekHold(action) {\n    seekHoldAction = action;\n    dispatchSeekState(action);\n    if (isTapeDevMode()) { console.debug('[eva-radio] ' + action + ' hold start: duck volume', action === 'ff' ? FFW_DUCK_VOLUME : REW_DUCK_VOLUME, 'windSound', workingAudio(mechFfwWindAudio) ? workingAudio(mechFfwWindAudio).src : '(missing, silent)'); }\n    if (action === 'ff') {\n      audio.playbackRate = FFW_PLAYBACK_RATE;\n      audio.preservesPitch = false;\n      audio.webkitPreservesPitch = false;\n      audio.mozPreservesPitch = false;\n      updateAudioVolume();\n      startMechLoop(workingAudio(mechFfwWindAudio), RADIO_MECH_GAIN); // no fallback loop sound exists yet -- silent (not a broken play() attempt) until Per supplies the file, same for both directions\n    } else if (action === 'rew') {\n      updateAudioVolume();\n      startMechLoop(workingAudio(mechFfwWindAudio), RADIO_MECH_GAIN); // same wind sound as FFW -- direction reads from the music scrubbing backwards, not the sound\n      rewScrubInterval = setInterval(function () {\n        seek(-REW_SCRUB_STEP_SECONDS);\n      }, REW_SCRUB_INTERVAL_MS);\n    }\n  }\n\n  // Cleanup shared by a normal release and an abnormal one (pointercancel,\n  // or a stop/pause landing mid-hold) -- always restores rate/volume and\n  // silences the wind loop, never leaves the deck stuck sped-up or ducked.\n  function endSeekHold() {\n    if (!seekHoldAction) { return; }\n    if (seekHoldAction === 'ff') {\n      audio.playbackRate = 1;\n      audio.preservesPitch = true;\n      audio.webkitPreservesPitch = true;\n      audio.mozPreservesPitch = true;\n      stopMechLoop(mechFfwWindAudio);\n    } else if (seekHoldAction === 'rew') {\n      if (rewScrubInterval) { clearInterval(rewScrubInterval); rewScrubInterval = null; }\n      stopMechLoop(mechFfwWindAudio);\n    }\n    seekHoldAction = null;\n    dispatchSeekState(null);\n    updateAudioVolume();\n  }\n\n  function cancelSeekHold() {\n    seekHoldPointerId = null;\n    if (seekHoldTimer) { clearTimeout(seekHoldTimer); seekHoldTimer = null; }\n    endSeekHold();\n  }\n\n  function onSeekPointerDown(action) {\n    return function (e) {\n      dismissBubble();\n      seekHoldPointerId = e.pointerId;\n      try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}\n      seekHoldTimer = setTimeout(function () {\n        seekHoldTimer = null;\n        beginSeekHold(action);\n      }, SEEK_HOLD_THRESHOLD_MS);\n    };\n  }\n\n  function onSeekPointerUp(action) {\n    return function (e) {\n      if (e.pointerId !== seekHoldPointerId) { return; }\n      seekHoldPointerId = null;\n      if (seekHoldTimer) {\n        // Released before the hold threshold fired -- a tap, not a hold.\n        clearTimeout(seekHoldTimer);\n        seekHoldTimer = null;\n        var tapAudio = workingAudio(mechFfwTapAudio) || mechPlayAudio; // same tap sound for both directions; falls back to the play clack until it lands\n        if (isTapeDevMode()) { console.debug('[eva-radio] ' + action + ' tap sound:', tapAudio && tapAudio.src); }\n        playMech(tapAudio, RADIO_MECH_FF_REW_GAIN);\n        seek(action === 'ff' ? SEEK_SECONDS : -SEEK_SECONDS);\n      } else {\n        endSeekHold();\n      }\n    };\n  }\n\n  function onSeekPointerCancel(e) {\n    if (e.pointerId !== seekHoldPointerId) { return; }\n    cancelSeekHold();\n  }\n\n  function nextTrack() {\n    var tape = currentTape();\n    if (!tape || !state.sideId) { return; }\n    var next = state.trackIndex + 1;\n    if (next >= sideTracks(tape, state.sideId).length) {\n      endOfSide();\n      return;\n    }\n    state.trackIndex = next;\n    state.positionSec = 0;\n    loadTrack(tape, state.sideId, next, 0);\n    play();\n  }\n\n  // Side runs out on its own: audio has already naturally stopped (the\n  // browser's own 'ended' event got us here) -- sync state, play the\n  // autostop clunk, and show the flip/rewind prompt. No auto-flip: the\n  // whole point is the ritual of a visitor choosing to flip the tape.\n  function endOfSide() {\n    state.playing = false;\n    state.positionSec = 0;\n    restoreUiSound();\n    setTapeMediaSessionPlaybackState('paused'); // 'pause' event listener is skipped for this path (audio.ended is true), so this needs its own explicit update -- the tape's still \"there\" (flip prompt showing), just not playing, same as a real pause\n    saveState();\n    playMech(mechAutostopAudio, RADIO_MECH_GAIN);\n    showSidePrompt(state.sideId);\n  }\n\n  // Flip sequence (used for both A->B \"flip\" and B->A \"rewind\" -- same\n  // mechanical sound either way per spec). eva_ui_tape_flip.wav already\n  // contains its own play-press sound, so the new side starts the\n  // instant the flip sound ends, with no separate PLAY clack layered on\n  // top of it.\n  function flipToSide(sideId) {\n    var tape = currentTape();\n    if (!tape) { return; }\n    hideBubble();\n    if (!mechFlipAudio) {\n      selectTapeSideAndPlay(tape.id, sideId);\n      return;\n    }\n    var started = false;\n    function startSide() {\n      if (started) { return; } // 'ended' and a failed play() promise could both fire\n      started = true;\n      mechFlipAudio.removeEventListener('ended', startSide);\n      selectTapeSideAndPlay(tape.id, sideId);\n    }\n    mechFlipAudio.pause();\n    mechFlipAudio.currentTime = 0;\n    mechFlipAudio.volume = RADIO_MECH_GAIN;\n    mechFlipAudio.addEventListener('ended', startSide);\n    var p = mechFlipAudio.play();\n    if (p && typeof p.catch === 'function') {\n      p.catch(startSide); // autoplay blocked or failed -- don't strand the ritual on a silent flip sound forever\n    }\n  }\n\n  audio = null; // assigned in init(), listeners wired there once the element exists\n\n  // --- Hard-load resume ---------------------------------------------------\n  // Same pattern as the store bell in sound-engine.js: browsers block\n  // audio.play() before a real user gesture, so a \"was playing\" state\n  // from before a hard reload can't just resume immediately -- arm it on\n  // the first gesture instead.\n  var resumeArmed = false;\n  function armHardLoadResume() {\n    if (resumeArmed || !state.playing) { return; }\n    resumeArmed = true;\n    function attempt() {\n      document.removeEventListener('pointerdown', attempt, true);\n      document.removeEventListener('keydown', attempt, true);\n      document.removeEventListener('touchstart', attempt, true);\n      // The gesture can fire before the manifest fetch resolves on a fast\n      // hard-reload -- wait for it rather than silently dropping the resume\n      // (tapesReady is a no-op microtask hop once already resolved, which\n      // is the common case).\n      tapesReady.then(function () {\n        var tape = currentTape();\n        if (!tape || !state.sideId) { return; }\n        deckLoaded = true; // a real tape WAS playing when the page last saved -- this resume is the one legitimate exception to \"state.tapeId alone doesn't mean a tape is in the deck,\" see deckLoaded's own comment\n        loadTrack(tape, state.sideId, state.trackIndex, state.positionSec);\n        play();\n      });\n    }\n    document.addEventListener('pointerdown', attempt, true);\n    document.addEventListener('keydown', attempt, true);\n    document.addEventListener('touchstart', attempt, true);\n  }\n\n  // --- Hotspot actions -----------------------------------------------\n\n  // ff/rew are deliberately absent here -- they're driven by dedicated\n  // pointerdown/up handlers (see onSeekPointerDown/Up below) so a press\n  // can be told apart as a tap vs a hold. Leaving them out of ACTIONS\n  // means the native 'click' that still follows their pointerup is a\n  // harmless no-op instead of double-firing a seek.\n  var ACTIONS = {\n    door: function () { requestShowRack(); },\n    play: function () { dismissBubble(); playWithSound(); },\n    pause: function () { pauseWithSound(); },\n    stop: function () { stopWithSound(); }\n  };\n\n  function onHotspotClick(e) {\n    var el = e.target.closest && e.target.closest('.eva-radio-hotspot');\n    if (!el) { return; }\n    var action = el.getAttribute('data-eva-radio-action');\n    if (ACTIONS[action]) { ACTIONS[action](); }\n  }\n\n  // A pointerdown/up pair on a hotspot with no real movement should also\n  // count as the click above (native click still fires in that case in\n  // every real browser -- this pressed-state toggle is purely visual).\n  function onHotspotPointerDown(e) {\n    var el = e.target.closest && e.target.closest('.eva-radio-hotspot');\n    if (!el) { return; }\n    el.classList.add('eva-radio-hotspot--pressed');\n  }\n\n  function clearPressedState() {\n    var pressed = dragHandle.querySelectorAll('.eva-radio-hotspot--pressed');\n    pressed.forEach(function (el) { el.classList.remove('eva-radio-hotspot--pressed'); });\n  }\n\n  // --- Init ----------------------------------------------------------\n\n  var savePositionTimer = null;\n\n  ready(function () {\n    widget = document.getElementById('EvaRadioWidget');\n    dragHandle = document.getElementById('EvaRadioDragHandle');\n    stage = document.getElementById('EvaRadioStage');\n    bubble = document.getElementById('EvaRadioBubble');\n    bubbleText = document.getElementById('EvaRadioBubbleText');\n    bubbleAction = document.getElementById('EvaRadioBubbleAction');\n    bubbleClose = document.getElementById('EvaRadioBubbleClose');\n    tooltipEl = document.getElementById('EvaRadioTooltip');\n    tooltipTextEl = document.getElementById('EvaRadioTooltipText');\n    tooltipTouchedOnce = new Set();\n    audio = document.getElementById('EvaRadioAudio');\n    if (window.EvaMute) { window.EvaMute.applyToElement(audio); }\n    if (mediaSessionSupported) {\n      try {\n        // Same functions the physical PLAY/PAUSE hotspots call -- a\n        // lock-screen button press behaves exactly like pressing the\n        // real deck button, mechanical clack included.\n        navigator.mediaSession.setActionHandler('play', function () { playWithSound(); });\n        navigator.mediaSession.setActionHandler('pause', function () { pauseWithSound(); });\n        navigator.mediaSession.setActionHandler('nexttrack', function () { nextTrack(); });\n        // No 'previoustrack' handler, deliberately -- the deck has no\n        // real \"previous track\" capability (nextTrack() only ever\n        // advances forward within a side; there's no equivalent\n        // rewind-to-earlier-track function anywhere in this file).\n        // Leaving it unset lets the OS grey out/hide that lock-screen\n        // button honestly rather than wiring it to a fabricated\n        // behavior that doesn't exist in the deck itself.\n      } catch (e) {}\n    }\n    toggleBtns = Array.prototype.slice.call(document.querySelectorAll('.js-eva-radio-toggle'));\n    minimizeBtn = document.getElementById('EvaRadioMinimize');\n    resizeHandle = document.getElementById('EvaRadioResizeHandle');\n    radioToggleEl = document.querySelector('.eva-radio-widget__radio-toggle');\n    tapeRackEl = document.getElementById('EvaRadioTapeRack');\n    onAirDots = Array.prototype.slice.call(document.querySelectorAll('.js-eva-radio-onair'));\n    angleBtn = document.getElementById('EvaRadioAngle');\n    angleTransportHotspots = {\n      front: document.getElementById('EvaRadioHotspots'),\n      above: document.getElementById('EvaRadioHotspotsAbove')\n    };\n    img = dragHandle && dragHandle.querySelector('.eva-radio-widget__img');\n    keyboardHotspotsSvg = document.getElementById('EvaRadioKeyboardHotspots');\n\n    if (!widget || !dragHandle || !audio) { return; }\n\n    // Transport (play/stop/ff/rew/door/drag) binds FIRST and on its own --\n    // this is the core function of the widget and must never be taken down\n    // by a throw in a newer/less-critical subsystem below it.\n    try {\n      toggleBtns.forEach(function (btn) {\n        btn.addEventListener('click', toggleMinimized);\n      });\n      updateToggleButtonsUI();\n\n      if (minimizeBtn) { minimizeBtn.addEventListener('click', minimizeWidget); }\n\n      if (resizeHandle) {\n        resizeHandle.addEventListener('pointerdown', onResizePointerDown);\n        resizeHandle.addEventListener('pointermove', onResizePointerMove);\n        resizeHandle.addEventListener('pointerup', onResizePointerUp);\n        resizeHandle.addEventListener('pointercancel', onResizePointerUp);\n      }\n\n      bubbleClose.addEventListener('click', dismissBubble);\n\n      dragHandle.addEventListener('pointerdown', onPointerDown);\n      dragHandle.addEventListener('pointermove', onPointerMove);\n      dragHandle.addEventListener('pointerup', onPointerUp);\n      dragHandle.addEventListener('pointercancel', onPointerUp);\n\n      dragHandle.addEventListener('pointerdown', onHotspotPointerDown);\n      dragHandle.addEventListener('pointerup', clearPressedState);\n      dragHandle.addEventListener('pointercancel', clearPressedState);\n      dragHandle.addEventListener('click', onHotspotClick);\n\n      // Every angle has its own hotspot SVG (only one visible/interactive\n      // at a time via display:none, see applyAngle()), each with its own\n      // ff/rew polygon -- bind all of them, not just the front one, or\n      // FF/REW would silently do nothing on the other 3 angles.\n      var ffHotspots = dragHandle.querySelectorAll('[data-eva-radio-action=\"ff\"]');\n      var rewHotspots = dragHandle.querySelectorAll('[data-eva-radio-action=\"rew\"]');\n      ffHotspots.forEach(function (el) {\n        el.addEventListener('pointerdown', onSeekPointerDown('ff'));\n        el.addEventListener('pointerup', onSeekPointerUp('ff'));\n        el.addEventListener('pointercancel', onSeekPointerCancel);\n      });\n      rewHotspots.forEach(function (el) {\n        el.addEventListener('pointerdown', onSeekPointerDown('rew'));\n        el.addEventListener('pointerup', onSeekPointerUp('rew'));\n        el.addEventListener('pointercancel', onSeekPointerCancel);\n      });\n\n      // Tooltip chip: one delegated pair covers every hotspot with a\n      // data-eva-tooltip attribute (deck buttons, door, tone toggle,\n      // rhythm presets across all 4 angles) rather than per-control\n      // wiring. pointerover/out (filtered to real mice) rather than\n      // mouseover/out -- touch devices fire synthetic mouse events right\n      // after a real touch, and an unfiltered mouseout was hiding the\n      // touch-triggered tooltip within milliseconds instead of leaving it\n      // up for its full 1.5s.\n      dragHandle.addEventListener('pointerover', function (e) {\n        if (e.pointerType !== 'mouse') { return; }\n        var el = e.target.closest && e.target.closest('[data-eva-tooltip]');\n        if (el) { showTooltip(el, el.getAttribute('data-eva-tooltip')); }\n      });\n      dragHandle.addEventListener('pointerout', function (e) {\n        if (e.pointerType !== 'mouse') { return; }\n        var el = e.target.closest && e.target.closest('[data-eva-tooltip]');\n        if (el && !el.contains(e.relatedTarget)) { hideTooltip(); }\n      });\n      // Touch has no hover -- show the chip briefly on a control's FIRST\n      // touch this page load only, per Per's spec, rather than on every tap.\n      dragHandle.addEventListener('touchstart', function (e) {\n        var el = e.target.closest && e.target.closest('[data-eva-tooltip]');\n        if (!el || tooltipTouchedOnce.has(el)) { return; }\n        tooltipTouchedOnce.add(el);\n        showTooltip(el, el.getAttribute('data-eva-tooltip'));\n        setTimeout(hideTooltip, 1500);\n      }, { passive: true });\n\n      audio.addEventListener('play', function () {\n        state.playing = true;\n        duckUiSound();\n        updateTapeMediaSession();\n        saveState();\n      });\n      audio.addEventListener('pause', function () {\n        // 'pause' also fires right before 'ended' -- ended's own handler\n        // (nextTrack/endOfSide) is authoritative for that case, this just\n        // covers an explicit user pause/stop.\n        if (audio.ended) { return; }\n        state.playing = false;\n        restoreUiSound();\n        setTapeMediaSessionPlaybackState('paused');\n      });\n      audio.addEventListener('ended', function () {\n        nextTrack();\n      });\n      audio.addEventListener('timeupdate', function () {\n        if (!state.playing) { return; }\n        if (savePositionTimer) { return; }\n        savePositionTimer = setTimeout(function () {\n          savePositionTimer = null;\n          state.positionSec = audio.currentTime || 0;\n          saveState();\n        }, SAVE_INTERVAL_MS);\n      });\n\n      // Small public hook for eva-radio-tape.js (tape browser) -- the one\n      // and only path that ever starts playback, driven by a real\n      // drag/tap-insert in the rack.\n      window.EvaRadioWidget = {\n        playTape: function (tapeId, sideId) {\n          selectTapeSide(tapeId, sideId || 'a');\n        },\n        // [] until tapesReady resolves -- eva-radio-tape.js checks this\n        // synchronously first (covers a fetch that already resolved by the\n        // time its own ready() runs), falling back to the\n        // 'eva:radio:tapes:ready' event for the other ordering.\n        getTapes: function () { return tapes; },\n        // Reused by eva-radio-tape.js for the tape object's own hover/\n        // touch tooltip -- one chip, one positioning implementation.\n        showTooltip: showTooltip,\n        hideTooltip: hideTooltip,\n        // Reused by eva-radio-tape.js when the rack scroller switches\n        // which tape is centered -- whatever was playing shouldn't keep\n        // going once the visible tape object no longer matches it.\n        stop: stop,\n        // Side-effect-free read (2026-08-07) -- lets eva-radio-station.js\n        // check \"is a tape genuinely playing right now\" before calling\n        // stop(), rather than calling it unconditionally on every SCAN\n        // attempt/retry. stop() always dispatches eva:radio:playstate\n        // with reason:'stop', which eva-radio-tape.js's own listener\n        // turns into a visible cassette-door pop flash -- harmless to\n        // call when nothing's playing in terms of AUDIO state, but very\n        // much not harmless visually: it was firing that flash on every\n        // single SCAN retry, animating the TAPE DECK for something\n        // happening in the tuner instead. See attemptPlay()'s own gate.\n        isPlaying: function () { return !!state.playing; }\n      };\n    } catch (transportErr) {\n      console.error('[eva-radio] transport init failed', transportErr);\n    }\n\n    // Angle changer + keyboard -- separate subsystem, own try/catch so\n    // a failure here can never prevent transport (above) from working.\n    try {\n      initMechSounds();\n      preloadAngleImages();\n      applyAngle(); // restores the persisted angle photo + hotspot visibility\n\n      // Keys duck the tape a tad instead of pausing it -- both play at\n      // once now (eva-radio-keyboard.js dispatches this on every active/\n      // idle transition, not per keypress).\n      document.addEventListener('eva:radio:keyboard:active', function (e) {\n        keyboardDucking = !!(e.detail && e.detail.active);\n        updateAudioVolume();\n      });\n\n      if (angleBtn) { angleBtn.addEventListener('click', cycleAngle); }\n    } catch (modeErr) {\n      console.error('[eva-radio] angle/mode init failed', modeErr);\n    }\n\n    // Widget chrome: initial show/position + PJAX/lifecycle hooks.\n    try {\n      if (!state.minimized) {\n        widget.hidden = false;\n        positionWidget();\n      }\n      updateOnAirDot();\n\n      document.addEventListener('eva:pjax:loaded', function () {\n        if (!widget || widget.hidden) { return; }\n        var pos = clampPosition(state.x, state.y);\n        state.x = pos.x;\n        state.y = pos.y;\n        applyPosition(pos.x, pos.y);\n        saveState();\n      });\n\n      armHardLoadResume();\n\n      window.addEventListener('beforeunload', function () {\n        if (state.playing) {\n          state.positionSec = audio.currentTime || 0;\n        }\n        saveState();\n      });\n    } catch (chromeErr) {\n      console.error('[eva-radio] widget chrome init failed', chromeErr);\n    }\n  });\n})();\n"],
  "mappings": "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",
  "names": []
}
