PageSourceSearch

https://www.hamienet.com/assets/SouthparkStageBanner-BCxT9awg.js

js hamienet.com collected 2026-09-24 19:01:24 UTC 95,630 bytes, 2,304 lines download raw bytes

1import { r as reactExports, R as React, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js";
2import { a as defaultBackgroundPresets } from "./StagePuppetEngine-BtPqoUG7.js";
3const SOUTHPARK_STAGE_CALIBRATIONS = {
4  // 1. Pavilion / Outdoor stages with smaller or raised platform
5  anime: { scale: 1.46, offsetX: -1, offsetY: 3, origin: "50% 50%", backY: 435, frontY: 600, backScale: 0.69, frontScale: 1.02, horizonY: 260 },
6  occasions: { scale: 1.84, offsetX: 0, offsetY: -11, origin: "50% 50%", backY: 405, frontY: 555, backScale: 0.55, frontScale: 0.98, horizonY: 250 },
7  reggae: { scale: 1.45, offsetX: -1, offsetY: -5, origin: "50% 50%", backY: 385, frontY: 565, backScale: 0.58, frontScale: 0.98, horizonY: 260 },
8  techno: { scale: 1.5, offsetX: 0, offsetY: -10, origin: "50% 50%", backY: 410, frontY: 550, backScale: 0.52, frontScale: 1, horizonY: 240 },
9  trap: { scale: 1.28, offsetX: -1, offsetY: -11, origin: "50% 50%", backY: 340, frontY: 540, backScale: 0.51, frontScale: 0.98, horizonY: 250 },
10  // 2. Large concert & theater stages with elevated horizons
11  rock: { scale: 2.5, offsetX: -4, offsetY: -28, origin: "50% 50%", backY: 375, frontY: 540, backScale: 0.54, frontScale: 1, horizonY: 250 },
12  classical: { scale: 1.37, offsetX: 0, offsetY: -12, origin: "50% 50%", backY: 435, frontY: 520, backScale: 0.55, frontScale: 0.82, horizonY: 250 },
13  pop: { scale: 1.02, offsetX: -1, offsetY: 3, origin: "50% 50%", backY: 365, frontY: 540, backScale: 0.56, frontScale: 0.98, horizonY: 255 },
14  jazz: { scale: 1.52, offsetX: 0, offsetY: -10, origin: "50% 50%", backY: 345, frontY: 455, backScale: 0.55, frontScale: 0.95, horizonY: 275 },
15  metal: { scale: 1.22, offsetX: -1, offsetY: -11, origin: "50% 50%", backY: 370, frontY: 520, backScale: 0.55, frontScale: 0.98, horizonY: 245 },
16  folk: { scale: 1.3, offsetX: 2, offsetY: -4, origin: "50% 50%", backY: 370, frontY: 565, backScale: 0.57, frontScale: 0.86, horizonY: 260 },
17  game_music: { scale: 1.19, offsetX: 0, offsetY: -8, origin: "50% 50%", backY: 375, frontY: 485, backScale: 0.55, frontScale: 0.98, horizonY: 250 },
18  "sci-fi": { scale: 1.2, offsetX: 1, offsetY: -5, origin: "50% 50%", backY: 380, frontY: 560, backScale: 0.56, frontScale: 0.98, horizonY: 255 },
19  scifi: { scale: 1.2, offsetX: 1, offsetY: -5, origin: "50% 50%", backY: 380, frontY: 560, backScale: 0.56, frontScale: 0.98, horizonY: 255 },
20  country: { scale: 1.29, offsetX: 0, offsetY: -4, origin: "50% 50%", backY: 430, frontY: 520, backScale: 0.58, frontScale: 0.96, horizonY: 265 },
21  world: { scale: 1.22, offsetX: 0, offsetY: -4, origin: "50% 50%", backY: 375, frontY: 560, backScale: 0.54, frontScale: 0.98, horizonY: 250 },
22  indie: { scale: 1.12, offsetX: 0, offsetY: 0, origin: "50% 50%", backY: 380, frontY: 485, backScale: 0.58, frontScale: 0.96, horizonY: 260 },
23  // 3. Flat floor / expansive ground stages (subtle 1.15-1.18x zoom for stage immersion)
24  alt_rock: { scale: 1.15, offsetX: -1, offsetY: 0, origin: "50% 50%", backY: 420, frontY: 480, backScale: 0.6, frontScale: 0.98, horizonY: 275 },
25  national: { scale: 1.44, offsetX: 0, offsetY: -11, origin: "50% 50%", backY: 405, frontY: 545, backScale: 0.56, frontScale: 0.98, horizonY: 255 },
26  religious: { scale: 1.68, offsetX: 0, offsetY: -21, origin: "50% 50%", backY: 385, frontY: 565, backScale: 0.52, frontScale: 0.98, horizonY: 240 },
27  original: { scale: 1, offsetX: 2, offsetY: -11, origin: "50% 50%", backY: 350, frontY: 545, backScale: 0.55, frontScale: 0.94, horizonY: 250 },
28  hip_hop: { scale: 1.24, offsetX: 0, offsetY: -7, origin: "50% 50%", backY: 385, frontY: 525, backScale: 0.6, frontScale: 0.98, horizonY: 270 },
29  karaoke: { scale: 1.15, offsetX: -1, offsetY: -6, origin: "50% 50%", backY: 405, frontY: 545, backScale: 0.64, frontScale: 0.96, horizonY: 280 },
30  latin: { scale: 1.15, offsetX: 0, offsetY: -5, origin: "50% 50%", backY: 390, frontY: 540, backScale: 0.58, frontScale: 0.98, horizonY: 260 },
31  reggaeton: { scale: 1.15, offsetX: -2, offsetY: -6, origin: "50% 50%", backY: 380, frontY: 545, backScale: 0.52, frontScale: 1, horizonY: 260 },
32  rnb: { scale: 1.58, offsetX: 2, offsetY: -5, origin: "50% 50%", backY: 380, frontY: 590, backScale: 0.6, frontScale: 0.96, horizonY: 275 }
33};
34const DEFAULT_ALIGNMENT = {
35  scale: 1.05,
36  offsetX: 0,
37  offsetY: 0,
38  origin: "50% 50%",
39  backY: 380,
40  frontY: 475,
41  backScale: 0.58,
42  frontScale: 0.98,
43  horizonY: 260
44};
45function getSouthparkStageAlignment(imageUrlOrId) {
46  if (!imageUrlOrId) return DEFAULT_ALIGNMENT;
47  const directKey = imageUrlOrId.toLowerCase().trim();
48  if (SOUTHPARK_STAGE_CALIBRATIONS[directKey]) {
49    return SOUTHPARK_STAGE_CALIBRATIONS[directKey];
50  }
51  const match = imageUrlOrId.match(/\/([^/]+)\.(jpg|png|webp|svg)/i);
52  if (match && match[1]) {
53    const fileKey = match[1].toLowerCase();
54    if (SOUTHPARK_STAGE_CALIBRATIONS[fileKey]) {
55      return SOUTHPARK_STAGE_CALIBRATIONS[fileKey];
56    }
57  }
58  for (const key of Object.keys(SOUTHPARK_STAGE_CALIBRATIONS)) {
59    if (directKey.includes(key)) {
60      return SOUTHPARK_STAGE_CALIBRATIONS[key];
61    }
62  }
63  return DEFAULT_ALIGNMENT;
64}
65const defaultSouthparkStageConfig = {
66  type: "image",
67  color: "#0c0c14",
68  gradient: {
69    type: "linear",
70    angle: 135,
71    stops: []
72  },
73  image: {
74    url: "/assets/visualizers/orchestra/bkg/classical.jpg",
75    size: "cover",
76    position: "center center",
77    repeat: "no-repeat",
78    blur: 0
79  },
80  particles: {
81    enabled: false,
82    type: "stars",
83    count: 50,
84    speed: 1,
85    color: "#ffffff",
86    size: 2
87  },
88  overlay: {
89    enabled: true,
90    color: "#000000",
91    opacity: 0.15
92  }
93};
94function useSouthparkStageBackground(track, shuffleSeed) {
95  const [spBackgroundConfig, setSpBackgroundConfig] = reactExports.useState(() => {
96    if (typeof window !== "undefined") {
97      try {
98        const cached = localStorage.getItem("southpark_cached_bg_config");
99        if (cached) return JSON.parse(cached);
100      } catch {
101      }
102    }
103    return null;
104  });
105  const [spBackgroundPresets, setSpBackgroundPresets] = reactExports.useState(() => {
106    if (typeof window !== "undefined") {
107      try {
108        const cached = localStorage.getItem("southpark_cached_bg_presets");
109        if (cached) return JSON.parse(cached);
110      } catch {
111      }
112    }
113    return null;
114  });
115  reactExports.useEffect(() => {
116    let isMounted = true;
117    const loadConfigs = async () => {
118      var _a, _b;
119      try {
120        const [spBgRes, spPresetsRes] = await Promise.all([
121          fetch("/api/settings/southpark_background_config"),
122          fetch("/api/settings/southpark_background_presets")
123        ]);
124        if (isMounted) {
125          if (spBgRes.ok) {
126            const data = await spBgRes.json();
127            if (data == null ? void 0 : data.value) {
128              const val = data.value;
129              if ((_b = (_a = val.image) == null ? void 0 : _a.url) == null ? void 0 : _b.includes("background-forest")) {
130                val.image.url = "/assets/visualizers/orchestra/bkg/classical.jpg";
131              }
132              setSpBackgroundConfig(val);
133              try {
134                localStorage.setItem("southpark_cached_bg_config", JSON.stringify(val));
135              } catch {
136              }
137            }
138          }
139          if (spPresetsRes.ok) {
140            const data = await spPresetsRes.json();
141            if ((data == null ? void 0 : data.value) && Array.isArray(data.value) && data.value.length > 0) {
142              const cleaned = data.value.filter(
143                (p) => {
144                  var _a2, _b2, _c;
145                  return !((_c = (_b2 = (_a2 = p.config) == null ? void 0 : _a2.image) == null ? void 0 : _b2.url) == null ? void 0 : _c.includes("background-forest"));
146                }
147              );
148              setSpBackgroundPresets(cleaned);
149              try {
150                localStorage.setItem("southpark_cached_bg_presets", JSON.stringify(cleaned));
151              } catch {
152              }
153            }
154          }
155        }
156      } catch (e) {
157        console.log("Using default background configs in Southpark visualizer", e);
158      }
159    };
160    loadConfigs();
161    return () => {
162      isMounted = false;
163    };
164  }, []);
165  const resolvedBackgroundConfig = reactExports.useMemo(() => {
166    var _a;
167    if (track.backgroundImageUrl) {
168      return {
169        type: "image",
170        color: "#050505",
171        gradient: {
172          type: "linear",
173          angle: 180,
174          stops: []
175        },
176        image: {
177          url: track.backgroundImageUrl,
178          size: "cover",
179          position: "center center",
180          repeat: "no-repeat",
181          blur: 0
182        },
183        overlay: {
184          enabled: true,
185          color: "#000000",
186          opacity: 0.15
187        },
188        particles: {
189          enabled: false,
190          type: "stars",
191          count: 50,
192          speed: 1,
193          color: "#ffffff",
194          size: 2
195        }
196      };
197    }
198    const enrichPresetConfig = (preset) => {
199      var _a2, _b, _c;
200      const url = (_b = (_a2 = preset.config) == null ? void 0 : _a2.image) == null ? void 0 : _b.url;
201      const calib2 = getSouthparkStageAlignment(url);
202      const userImg = (_c = preset.config) == null ? void 0 : _c.image;
203      return {
204        ...preset,
205        config: {
206          ...preset.config,
207          image: {
208            url: (userImg == null ? void 0 : userImg.url) || url || "",
209            size: (userImg == null ? void 0 : userImg.size) || "cover",
210            position: (userImg == null ? void 0 : userImg.position) || "center center",
211            repeat: (userImg == null ? void 0 : userImg.repeat) || "no-repeat",
212            blur: (userImg == null ? void 0 : userImg.blur) || 0,
213            scale: (userImg == null ? void 0 : userImg.scale) !== void 0 ? userImg.scale : calib2.scale,
214            offsetX: (userImg == null ? void 0 : userImg.offsetX) !== void 0 ? userImg.offsetX : calib2.offsetX || 0,
215            offsetY: (userImg == null ? void 0 : userImg.offsetY) !== void 0 ? userImg.offsetY : calib2.offsetY,
216            backY: (userImg == null ? void 0 : userImg.backY) !== void 0 ? userImg.backY : calib2.backY,
217            frontY: (userImg == null ? void 0 : userImg.frontY) !== void 0 ? userImg.frontY : calib2.frontY,
218            backScale: (userImg == null ? void 0 : userImg.backScale) !== void 0 ? userImg.backScale : calib2.backScale,
219            frontScale: (userImg == null ? void 0 : userImg.frontScale) !== void 0 ? userImg.frontScale : calib2.frontScale,
220            horizonY: (userImg == null ? void 0 : userImg.horizonY) !== void 0 ? userImg.horizonY : calib2.horizonY
221          }
222        }
223      };
224    };
225    const basePool = defaultBackgroundPresets.filter((p) => {
226      var _a2, _b, _c;
227      return !((_c = (_b = (_a2 = p.config) == null ? void 0 : _a2.image) == null ? void 0 : _b.url) == null ? void 0 : _c.includes("background-forest"));
228    });
229    const mergedPresets = basePool.map((base) => {
230      const spOverride = (spBackgroundPresets || []).find((p) => p.id === base.id);
231      const raw = spOverride ? spOverride : base;
232      return enrichPresetConfig(raw);
233    });
234    const baseIds = new Set(basePool.map((b) => b.id));
235    const extraCustom = (spBackgroundPresets || []).filter((p) => {
236      var _a2, _b, _c;
237      return !baseIds.has(p.id) && !((_c = (_b = (_a2 = p.config) == null ? void 0 : _a2.image) == null ? void 0 : _b.url) == null ? void 0 : _c.includes("background-forest"));
238    }).map(enrichPresetConfig);
239    const fullPool = [...mergedPresets, ...extraCustom];
240    if (shuffleSeed !== void 0 && fullPool.length > 0) {
241      const seedIndex = Math.abs(shuffleSeed) % fullPool.length;
242      return fullPool[seedIndex].config;
243    }
244    const matchPresets = (presets) => {
245      var _a2, _b;
246      if (!Array.isArray(track.tags) || track.tags.length === 0) return null;
247      const tagNames = track.tags.map((t) => {
248        if (typeof t === "string") return t.toLowerCase();
249        if (t && typeof t === "object" && "name" in t) return t.name.toLowerCase();
250        return "";
251      }).filter(Boolean);
252      for (const preset of presets) {
253        if (Array.isArray(preset.tags) && preset.tags.length > 0) {
254          const matches = preset.tags.some((tag) => {
255            const cleanTag = tag.toLowerCase().trim();
256            if (!cleanTag) return false;
257            return tagNames.some((tn) => {
258              if (tn.includes(cleanTag) || cleanTag.includes(tn)) return true;
259              const tnWords = tn.split(/[\s,\-_]+/);
260              const tagWords = cleanTag.split(/[\s,\-_]+/);
261              return tagWords.some((tw) => tw.length > 2 && tnWords.includes(tw));
262            });
263          });
264          if (matches && preset.config && !((_b = (_a2 = preset.config.image) == null ? void 0 : _a2.url) == null ? void 0 : _b.includes("background-forest"))) {
265            return preset.config;
266          }
267        }
268      }
269      return null;
270    };
271    const matchedPresetConfig = matchPresets(fullPool);
272    if (matchedPresetConfig) return matchedPresetConfig;
273    if (fullPool.length > 0) {
274      const trackStr = `${track.id || ""}_${track.title || "default"}`;
275      let hash = 0;
276      for (let i = 0; i < trackStr.length; i++) {
277        hash = (hash << 5) - hash + trackStr.charCodeAt(i);
278        hash |= 0;
279      }
280      const index = Math.abs(hash) % fullPool.length;
281      return fullPool[index].config;
282    }
283    const baseDefault = spBackgroundConfig || defaultSouthparkStageConfig;
284    const calib = getSouthparkStageAlignment((_a = baseDefault.image) == null ? void 0 : _a.url);
285    const defImg = baseDefault.image;
286    return {
287      ...baseDefault,
288      image: {
289        url: (defImg == null ? void 0 : defImg.url) || "/assets/visualizers/orchestra/bkg/classical.jpg",
290        size: (defImg == null ? void 0 : defImg.size) || "cover",
291        position: (defImg == null ? void 0 : defImg.position) || "center center",
292        repeat: (defImg == null ? void 0 : defImg.repeat) || "no-repeat",
293        blur: (defImg == null ? void 0 : defImg.blur) || 0,
294        scale: (defImg == null ? void 0 : defImg.scale) !== void 0 ? defImg.scale : calib.scale,
295        offsetX: (defImg == null ? void 0 : defImg.offsetX) !== void 0 ? defImg.offsetX : calib.offsetX || 0,
296        offsetY: (defImg == null ? void 0 : defImg.offsetY) !== void 0 ? defImg.offsetY : calib.offsetY,
297        backY: (defImg == null ? void 0 : defImg.backY) !== void 0 ? defImg.backY : calib.backY,
298        frontY: (defImg == null ? void 0 : defImg.frontY) !== void 0 ? defImg.frontY : calib.frontY,
299        backScale: (defImg == null ? void 0 : defImg.backScale) !== void 0 ? defImg.backScale : calib.backScale,
300        frontScale: (defImg == null ? void 0 : defImg.frontScale) !== void 0 ? defImg.frontScale : calib.frontScale,
301        horizonY: (defImg == null ? void 0 : defImg.horizonY) !== void 0 ? defImg.horizonY : calib.horizonY
302      }
303    };
304  }, [
305    track.id,
306    track.title,
307    track.backgroundImageUrl,
308    track.tags,
309    shuffleSeed,
310    spBackgroundConfig,
311    spBackgroundPresets
312  ]);
313  return resolvedBackgroundConfig;
314}
315const FAMILY_SORT_WEIGHTS = {
316  // 1. Basses (Low end foundation - Left stage)
317  4: 10,
318  // Basses (Acoustic, Electric, Synth Bass)
319  // 2. Guitars
320  3: 20,
321  // Guitars (Nylon, Steel, Clean, Distortion)
322  // 3. Drums & Percussion (Rhythm section, anchored in back)
323  "-1": 30,
324  // Ch 9 Drums / Rhythm kit
325  14: 35,
326  // Percussive
327  15: 38,
328  // Sound Effects
329  // 4. Keyboards
330  0: 40,
331  // Pianos (Acoustic Grand, Electric Piano)
332  2: 45,
333  // Organs (Hammond, Church, Accordion)
334  // 5. Synthesizers
335  10: 50,
336  // Synth Lead
337  11: 55,
338  // Synth Pad
339  12: 58,
340  // Synth FX
341  // 6. Woodwinds
342  9: 60,
343  // Pipes / Flutes (Flute, Pan Flute, Piccolo)
344  8: 65,
345  // Reeds (Saxophones, Oboe, Clarinet, Bassoon)
346  // 7. Brass
347  7: 70,
348  // Brass (Trumpet, Trombone, French Horn, Tuba)
349  // 8. Strings & Ensembles
350  5: 80,
351  // Strings (Violin, Viola, Cello, Contrabass, Harp)
352  6: 85,
353  // Ensembles (String Ensembles, Choirs)
354  1: 88,
355  // Chromatic Percussion (Vibraphone, Marimba, Glockenspiel)
356  // 9. Ethnic & World
357  13: 90
358  // Ethnic (Sitar, Banjo, Koto, Bagpipe)
359};
360const PERC_ROLE_WEIGHTS = {
361  drums: 30,
362  electronic: 31,
363  congas: 32,
364  bongos: 33,
365  tambourine: 34,
366  cowbell: 35,
367  triangle: 36,
368  shaker: 37,
369  claps: 38
370};
371function getPerspectiveScale(y, baseScale = 1, depthConfig) {
372  const backY = (depthConfig == null ? void 0 : depthConfig.backY) ?? 380;
373  const frontY = (depthConfig == null ? void 0 : depthConfig.frontY) ?? 475;
374  const backScale = (depthConfig == null ? void 0 : depthConfig.backScale) ?? 0.58;
375  const frontScale = (depthConfig == null ? void 0 : depthConfig.frontScale) ?? 0.98;
376  const effectiveBase = (depthConfig == null ? void 0 : depthConfig.baseScale) ?? baseScale;
377  const clampedY = Math.max(backY - 30, Math.min(frontY + 35, y));
378  const rangeY = Math.max(20, frontY - backY);
379  const progress = (clampedY - backY) / rangeY;
380  const scale = backScale + progress * (frontScale - backScale);
381  return Math.max(0.35, Math.min(1.25, scale)) * effectiveBase;
382}
383function calculateEnsemblePlacement(musicians, options = {}) {
384  const count = musicians.length;
385  if (count === 0) return [];
386  const {
387    stageConfig,
388    customPositions = {},
389    enableDensityScaling = true,
390    stageWidth = 1200,
391    stageHeight = 650
392  } = options;
393  const baseScale = (stageConfig == null ? void 0 : stageConfig.baseScale) ?? 1;
394  const globalYOffset = (stageConfig == null ? void 0 : stageConfig.yOffset) ?? 0;
395  const stageArcMultiplier = (stageConfig == null ? void 0 : stageConfig.arcFactor) ?? 1;
396  const densityScale = enableDensityScaling && count > 8 ? Math.max(0.75, 1 - (count - 8) * 0.022) : 1;
397  const prepared = musicians.map((m) => {
398    let weight = 5e3;
399    const prog = m.originalProgram !== void 0 && m.originalProgram !== null ? m.originalProgram : m.program;
400    const fam = m.originalFamilyIndex !== void 0 && m.originalFamilyIndex !== null ? m.originalFamilyIndex : m.isPercussion ? -1 : Math.floor(prog / 8);
401    if (m.isPercussion) {
402      const roleWeight = m.percRole ? PERC_ROLE_WEIGHTS[m.percRole] || 30 : 30;
403      weight = roleWeight * 1e3;
404    } else {
405      const base = FAMILY_SORT_WEIGHTS[fam] !== void 0 ? FAMILY_SORT_WEIGHTS[fam] : 50;
406      weight = base * 1e3 + prog % 128;
407    }
408    return {
409      musician: m,
410      fam,
411      sortWeight: weight
412    };
413  });
414  prepared.sort((a, b) => a.sortWeight - b.sortWeight);
415  const backY = (stageConfig == null ? void 0 : stageConfig.backY) ?? 380;
416  const frontY = (stageConfig == null ? void 0 : stageConfig.frontY) ?? 475;
417  const middleY = Math.round((backY + frontY) / 2);
418  prepared.some((p) => p.musician.isPercussion || p.fam === -1 || p.fam === 14);
419  let backGroup = [];
420  let middleGroup = [];
421  let frontGroup = [];
422  if (count <= 3) {
423    const hasPerc = prepared.some((p) => p.musician.isPercussion || p.fam === -1 || p.fam === 14);
424    if (hasPerc) {
425      prepared.forEach((p) => {
426        if (p.musician.isPercussion || p.fam === -1 || p.fam === 14) backGroup.push(p);
427        else frontGroup.push(p);
428      });
429    } else {
430      frontGroup = [...prepared];
431    }
432  } else if (count <= 7) {
433    prepared.forEach((p) => {
434      const isBack = p.musician.isPercussion || p.fam === -1 || p.fam === 14 || p.fam === 11 || p.fam === 12;
435      const isMiddle = p.fam === 7 || p.fam === 8 || p.fam === 9 || p.fam === 0 || p.fam === 2 || p.fam === 1 || p.fam === 6;
436      if (isBack) {
437        backGroup.push(p);
438      } else if (isMiddle) {
439        middleGroup.push(p);
440      } else {
441        frontGroup.push(p);
442      }
443    });
444    if (frontGroup.length >= 4 && middleGroup.length === 0) {
445      middleGroup.push(frontGroup.splice(Math.floor(frontGroup.length / 2), 1)[0]);
446    }
447    if (frontGroup.length > 3 && middleGroup.length === 1 && backGroup.length === 0) {
448      backGroup.push(frontGroup.shift());
449    }
450  } else {
451    prepared.forEach((p) => {
452      const isBack = p.musician.isPercussion || p.fam === -1 || p.fam === 14 || p.fam === 11 || p.fam === 12;
453      const isMiddle = p.fam === 7 || p.fam === 8 || p.fam === 9 || p.fam === 0 || p.fam === 2 || p.fam === 6 || p.fam === 1;
454      if (isBack) {
455        backGroup.push(p);
456      } else if (isMiddle) {
457        middleGroup.push(p);
458      } else {
459        frontGroup.push(p);
460      }
461    });
462    const targetPerRow = Math.ceil(count / 3);
463    while (frontGroup.length > targetPerRow + 1 && middleGroup.length < targetPerRow) {
464      middleGroup.push(frontGroup.splice(Math.floor(frontGroup.length / 2), 1)[0]);
465    }
466    while (middleGroup.length > targetPerRow + 1 && backGroup.length < targetPerRow) {
467      backGroup.push(middleGroup.splice(Math.floor(middleGroup.length / 2), 1)[0]);
468    }
469  }
470  const frontMargin = count >= 16 ? 110 : count >= 12 ? 130 : count >= 8 ? 150 : count >= 6 ? 160 : 200;
471  const middleMargin = frontMargin + 25;
472  const backMargin = frontMargin + 45;
473  function placeRow(group, rowName, baseYVal, margin) {
474    const n = group.length;
475    if (n === 0) return [];
476    const avail = stageWidth - margin * 2;
477    if (n === 1) {
478      return [{
479        m: group[0].musician,
480        rowName,
481        rowIndex: rowName === "back" ? 0 : rowName === "middle" ? 1 : 2,
482        defaultX: Math.round(stageWidth / 2),
483        defaultY: baseYVal
484      }];
485    }
486    const step = avail / (n - 1);
487    return group.map((p, i) => ({
488      m: p.musician,
489      rowName,
490      rowIndex: rowName === "back" ? 0 : rowName === "middle" ? 1 : 2,
491      defaultX: Math.round(margin + i * step),
492      defaultY: baseYVal
493    }));
494  }
495  const placedFront = placeRow(frontGroup, "front", frontY, frontMargin);
496  const placedMiddle = placeRow(middleGroup, "middle", middleY, middleMargin);
497  const placedBack = placeRow(backGroup, "back", backY, backMargin);
498  const allPlaced = [...placedBack, ...placedMiddle, ...placedFront];
499  if (count > 1) {
500    let minGap = 160;
501    if (count <= 2) minGap = 260;
502    else if (count <= 4) minGap = 200;
503    else if (count === 5) minGap = 175;
504    else if (count === 6) minGap = 155;
505    else if (count === 7) minGap = 140;
506    else if (count <= 10) minGap = 110;
507    else if (count <= 14) minGap = 85;
508    else minGap = 70;
509    const leftBound = frontMargin;
510    const rightBound = stageWidth - frontMargin;
511    for (let pass = 0; pass < 25; pass++) {
512      allPlaced.sort((a, b) => a.defaultX - b.defaultX);
513      let maxOverlap = 0;
514      for (let i = 0; i < allPlaced.length - 1; i++) {
515        const curr = allPlaced[i];
516        const next = allPlaced[i + 1];
517        const dist = next.defaultX - curr.defaultX;
518        const overlap = minGap - dist;
519        if (overlap > 0) {
520          if (overlap > maxOverlap) maxOverlap = overlap;
521          const shift = Math.ceil(overlap / 2);
522          curr.defaultX = Math.max(leftBound, curr.defaultX - shift);
523          next.defaultX = Math.min(rightBound, next.defaultX + shift);
524        }
525      }
526      if (maxOverlap <= 1) break;
527    }
528    allPlaced.sort((a, b) => a.defaultX - b.defaultX);
529    const ensembleLeft = allPlaced[0].defaultX;
530    const ensembleRight = allPlaced[allPlaced.length - 1].defaultX;
531    const ensembleCenter = (ensembleLeft + ensembleRight) / 2;
532    const centerOffset = Math.round(stageWidth / 2 - ensembleCenter);
533    if (Math.abs(centerOffset) > 2) {
534      allPlaced.forEach((m) => {
535        m.defaultX = Math.max(leftBound, Math.min(rightBound, m.defaultX + centerOffset));
536      });
537    }
538    allPlaced.forEach((m) => {
539      const arcProgress = (m.defaultX - leftBound) / Math.max(1, rightBound - leftBound);
540      const arcFactor = Math.sin(Math.max(0, Math.min(1, arcProgress)) * Math.PI);
541      const baseArcNudge = m.rowName === "front" ? 24 : m.rowName === "middle" ? 16 : 10;
542      const arcOffsetY = -arcFactor * baseArcNudge * stageArcMultiplier;
543      m.defaultY = Math.min(stageHeight - 145, Math.round(m.defaultY + globalYOffset + arcOffsetY));
544    });
545  } else if (allPlaced.length === 1) {
546    allPlaced[0].defaultY = Math.min(stageHeight - 145, Math.round(allPlaced[0].defaultY + globalYOffset));
547  }
548  const tempPlaced = allPlaced;
549  const result = tempPlaced.map((item) => {
550    const m = item.m;
551    const stableKey = m.stableKey || (typeof m.index === "number" ? `track-${m.index}${m.percRole ? "-" + m.percRole : ""}` : void 0);
552    const customPos = (stableKey ? customPositions[stableKey] : void 0) || (m.trackId !== void 0 ? customPositions[`trackId-${m.trackId}`] : void 0) || customPositions[m.id];
553    const posX = (customPos == null ? void 0 : customPos.x) ?? item.defaultX;
554    const posY = (customPos == null ? void 0 : customPos.y) ?? item.defaultY;
555    const basePerspectiveScale = getPerspectiveScale(posY, baseScale, stageConfig);
556    const musicianScale = basePerspectiveScale * densityScale * (m.scaleMultiplier ?? 1);
557    return {
558      m,
559      defaultX: item.defaultX,
560      defaultY: item.defaultY,
561      posX,
562      posY,
563      musicianScale,
564      rowName: item.rowName,
565      rowIndex: item.rowIndex
566    };
567  });
568  return result.sort((a, b) => a.posY - b.posY);
569}
570const LIGHTING_PROFILES = {
571  rock: {
572    id: "rock",
573    name: "Arena Rock Rig",
574    style: "searchlights",
575    baseIntensity: 0.55,
576    peakIntensity: 1,
577    beatPulseAmount: 0.35,
578    sweepSpeedMultiplier: 1.4,
579    trussColor: "#ef4444",
580    ambientColors: ["rgba(239, 68, 68, 0.25)", "rgba(59, 130, 246, 0.2)"],
581    blinderColor: "#ffffff",
582    beamColors: ["#ef4444", "#f97316", "#eab308", "#3b82f6", "#ec4899"],
583    beamCount: 6,
584    maxBeamSpreadAngle: 36,
585    hasStrobes: true,
586    hasBlinders: true,
587    floorGlowOpacity: 0.45
588  },
589  techno: {
590    id: "techno",
591    name: "Cyber Rave Matrix",
592    style: "lasers",
593    baseIntensity: 0.6,
594    peakIntensity: 1,
595    beatPulseAmount: 0.4,
596    sweepSpeedMultiplier: 2,
597    trussColor: "#06b6d4",
598    ambientColors: ["rgba(6, 182, 212, 0.3)", "rgba(168, 85, 247, 0.25)"],
599    blinderColor: "#00f6ff",
600    beamColors: ["#06b6d4", "#ec4899", "#3b82f6", "#a855f7", "#10b981"],
601    beamCount: 8,
602    maxBeamSpreadAngle: 55,
603    hasStrobes: true,
604    hasBlinders: true,
605    floorGlowOpacity: 0.5
606  },
607  classical: {
608    id: "classical",
609    name: "Concert Hall Chandelier",
610    style: "chandelier",
611    baseIntensity: 0.3,
612    peakIntensity: 0.65,
613    beatPulseAmount: 0.15,
614    sweepSpeedMultiplier: 0.6,
615    trussColor: "#f59e0b",
616    ambientColors: ["rgba(245, 158, 11, 0.18)", "rgba(217, 119, 6, 0.12)"],
617    blinderColor: "#fffbeb",
618    beamColors: ["#fde68a", "#fef3c7", "#f59e0b", "#fbbf24"],
619    beamCount: 4,
620    maxBeamSpreadAngle: 24,
621    hasStrobes: false,
622    hasBlinders: false,
623    floorGlowOpacity: 0.25
624  },
625  jazz: {
626    id: "jazz",
627    name: "Smoky Cabaret Pin-Spot",
628    style: "cabaret",
629    baseIntensity: 0.25,
630    peakIntensity: 0.6,
631    beatPulseAmount: 0.2,
632    sweepSpeedMultiplier: 0.7,
633    trussColor: "#6366f1",
634    ambientColors: ["rgba(49, 46, 129, 0.35)", "rgba(88, 28, 135, 0.2)"],
635    blinderColor: "#fef08a",
636    beamColors: ["#818cf8", "#a855f7", "#fde047", "#60a5fa"],
637    beamCount: 3,
638    maxBeamSpreadAngle: 22,
639    hasStrobes: false,
640    hasBlinders: false,
641    floorGlowOpacity: 0.22
642  },
643  anime: {
644    id: "anime",
645    name: "Celestial God Rays",
646    style: "godrays",
647    baseIntensity: 0.4,
648    peakIntensity: 0.78,
649    beatPulseAmount: 0.22,
650    sweepSpeedMultiplier: 0.85,
651    trussColor: "#c084fc",
652    ambientColors: ["rgba(192, 132, 252, 0.25)", "rgba(56, 189, 248, 0.2)"],
653    blinderColor: "#ffffff",
654    beamColors: ["#f472b6", "#38bdf8", "#c084fc", "#fb7185", "#fef08a"],
655    beamCount: 5,
656    maxBeamSpreadAngle: 32,
657    hasStrobes: false,
658    hasBlinders: true,
659    floorGlowOpacity: 0.35
660  },
661  folk: {
662    id: "folk",
663    name: "Golden Hour Lanterns",
664    style: "lanterns",
665    baseIntensity: 0.32,
666    peakIntensity: 0.68,
667    beatPulseAmount: 0.18,
668    sweepSpeedMultiplier: 0.65,
669    trussColor: "#ea580c",
670    ambientColors: ["rgba(234, 88, 12, 0.2)", "rgba(202, 138, 4, 0.15)"],
671    blinderColor: "#fed7aa",
672    beamColors: ["#fb923c", "#facc15", "#f97316", "#fdba74"],
673    beamCount: 4,
674    maxBeamSpreadAngle: 26,
675    hasStrobes: false,
676    hasBlinders: false,
677    floorGlowOpacity: 0.28
678  },
679  pop: {
680    id: "pop",
681    name: "Broadway Disco Pins",
682    style: "disco",
683    baseIntensity: 0.5,
684    peakIntensity: 0.9,
685    beatPulseAmount: 0.3,
686    sweepSpeedMultiplier: 1.3,
687    trussColor: "#ec4899",
688    ambientColors: ["rgba(236, 72, 153, 0.25)", "rgba(234, 179, 8, 0.2)"],
689    blinderColor: "#ffffff",
690    beamColors: ["#ec4899", "#06b6d4", "#eab308", "#a855f7", "#f43f5e"],
691    beamCount: 6,
692    maxBeamSpreadAngle: 42,
693    hasStrobes: true,
694    hasBlinders: true,
695    floorGlowOpacity: 0.4
696  },
697  reggae: {
698    id: "reggae",
699    name: "Sunset Horizon Spectrum",
700    style: "sunset",
701    baseIntensity: 0.4,
702    peakIntensity: 0.75,
703    beatPulseAmount: 0.25,
704    sweepSpeedMultiplier: 0.8,
705    trussColor: "#10b981",
706    ambientColors: ["rgba(16, 185, 129, 0.25)", "rgba(245, 158, 11, 0.2)"],
707    blinderColor: "#fef08a",
708    beamColors: ["#10b981", "#f59e0b", "#ef4444", "#84cc16"],
709    beamCount: 5,
710    maxBeamSpreadAngle: 35,
711    hasStrobes: false,
712    hasBlinders: false,
713    floorGlowOpacity: 0.35
714  },
715  cathedral: {
716    id: "cathedral",
717    name: "Cathedral Pillar Shafts",
718    style: "cathedral",
719    baseIntensity: 0.3,
720    peakIntensity: 0.7,
721    beatPulseAmount: 0.12,
722    sweepSpeedMultiplier: 0.5,
723    trussColor: "#38bdf8",
724    ambientColors: ["rgba(56, 189, 248, 0.18)", "rgba(250, 204, 21, 0.12)"],
725    blinderColor: "#ffffff",
726    beamColors: ["#ffffff", "#bae6fd", "#fef08a", "#e0f2fe"],
727    beamCount: 4,
728    maxBeamSpreadAngle: 18,
729    hasStrobes: false,
730    hasBlinders: true,
731    floorGlowOpacity: 0.25
732  },
733  standard: {
734    id: "standard",
735    name: "Theatrical Stage Rig",
736    style: "standard",
737    baseIntensity: 0.42,
738    peakIntensity: 0.8,
739    beatPulseAmount: 0.25,
740    sweepSpeedMultiplier: 1,
741    trussColor: "#818cf8",
742    ambientColors: ["rgba(129, 140, 248, 0.22)", "rgba(245, 158, 11, 0.12)"],
743    blinderColor: "#ffffff",
744    beamColors: ["#818cf8", "#f59e0b", "#38bdf8", "#c084fc"],
745    beamCount: 5,
746    maxBeamSpreadAngle: 30,
747    hasStrobes: false,
748    hasBlinders: true,
749    floorGlowOpacity: 0.32
750  }
751};
752function resolveLightingProfile(bg, tags) {
753  var _a;
754  const url = (((_a = bg == null ? void 0 : bg.image) == null ? void 0 : _a.url) || "").toLowerCase();
755  if (url.includes("rock") || url.includes("metal")) return LIGHTING_PROFILES.rock;
756  if (url.includes("techno") || url.includes("scifi") || url.includes("sci-fi") || url.includes("trap") || url.includes("electronic")) return LIGHTING_PROFILES.techno;
757  if (url.includes("classical")) return LIGHTING_PROFILES.classical;
758  if (url.includes("jazz") || url.includes("rnb")) return LIGHTING_PROFILES.jazz;
759  if (url.includes("anime") || url.includes("game_music")) return LIGHTING_PROFILES.anime;
760  if (url.includes("folk") || url.includes("country") || url.includes("world") || url.includes("original")) return LIGHTING_PROFILES.folk;
761  if (url.includes("pop") || url.includes("karaoke") || url.includes("occasions")) return LIGHTING_PROFILES.pop;
762  if (url.includes("reggae") || url.includes("latin") || url.includes("reggaeton")) return LIGHTING_PROFILES.reggae;
763  if (url.includes("religious") || url.includes("national")) return LIGHTING_PROFILES.cathedral;
764  if (Array.isArray(tags) && tags.length > 0) {
765    const tagStrings = tags.map((t) => typeof t === "string" ? t.toLowerCase() : ((t == null ? void 0 : t.name) || "").toLowerCase()).join(" ");
766    if (tagStrings.includes("rock") || tagStrings.includes("metal")) return LIGHTING_PROFILES.rock;
767    if (tagStrings.includes("techno") || tagStrings.includes("edm") || tagStrings.includes("dance") || tagStrings.includes("synth")) return LIGHTING_PROFILES.techno;
768    if (tagStrings.includes("classical") || tagStrings.includes("orchestral") || tagStrings.includes("piano")) return LIGHTING_PROFILES.classical;
769    if (tagStrings.includes("jazz") || tagStrings.includes("blues") || tagStrings.includes("r&b") || tagStrings.includes("rnb")) return LIGHTING_PROFILES.jazz;
770    if (tagStrings.includes("anime") || tagStrings.includes("game") || tagStrings.includes("chiptune")) return LIGHTING_PROFILES.anime;
771    if (tagStrings.includes("folk") || tagStrings.includes("country") || tagStrings.includes("acoustic")) return LIGHTING_PROFILES.folk;
772    if (tagStrings.includes("pop") || tagStrings.includes("karaoke") || tagStrings.includes("broadway")) return LIGHTING_PROFILES.pop;
773    if (tagStrings.includes("reggae") || tagStrings.includes("latin") || tagStrings.includes("salsa")) return LIGHTING_PROFILES.reggae;
774    if (tagStrings.includes("hymn") || tagStrings.includes("religious") || tagStrings.includes("anthem")) return LIGHTING_PROFILES.cathedral;
775  }
776  return LIGHTING_PROFILES.standard;
777}
778const SouthparkStageLightingComponent = ({
779  track,
780  currentTime = 0,
781  isPlaying,
782  bpm = 120,
783  bps = 2,
784  backgroundConfig,
785  lightingMode = "dynamic",
786  musicians = [],
787  musicianPositions = {},
788  activeNotes = {},
789  soloedIndex = null,
790  selectedTrackIndex = null,
791  globalIntensity = 0,
792  stageWidth = 1200,
793  stageHeight = 650
794}) => {
795  var _a, _b, _c;
796  if (lightingMode === "off") {
797    return null;
798  }
799  const profile = reactExports.useMemo(() => {
800    return resolveLightingProfile(backgroundConfig, track == null ? void 0 : track.tags);
801  }, [(_a = backgroundConfig == null ? void 0 : backgroundConfig.image) == null ? void 0 : _a.url, track == null ? void 0 : track.tags]);
802  const safeTime = typeof currentTime === "number" && !isNaN(currentTime) && isFinite(currentTime) ? Math.max(0, currentTime) : 0;
803  const safeBpm = typeof bpm === "number" && !isNaN(bpm) && isFinite(bpm) && bpm > 0 ? bpm : 120;
804  const safeBps = typeof bps === "number" && !isNaN(bps) && isFinite(bps) && bps > 0 ? bps : safeBpm / 60;
805  const effectiveBps = safeBps > 0 ? safeBps : 2;
806  const beatInterval = 1 / effectiveBps;
807  const rawBeatProg = safeTime % beatInterval / beatInterval;
808  const beatProgress = !isNaN(rawBeatProg) && isFinite(rawBeatProg) ? Math.max(0, Math.min(1, rawBeatProg)) : 0;
809  const rawBeatPulse = Math.exp(-5.5 * beatProgress);
810  const beatPulse = !isNaN(rawBeatPulse) && isFinite(rawBeatPulse) ? rawBeatPulse : 0;
811  const rawBarProg = safeTime % (beatInterval * 4) / (beatInterval * 4);
812  const barProgress = !isNaN(rawBarProg) && isFinite(rawBarProg) ? Math.max(0, Math.min(1, rawBarProg)) : 0;
813  const rawDownbeat = Math.exp(-6.5 * barProgress);
814  const downbeatPulse = !isNaN(rawDownbeat) && isFinite(rawDownbeat) ? rawDownbeat : 0;
815  let hasClimax = false;
816  let hasAttack = false;
817  let maxMusicianVel = 0;
818  for (const key in activeNotes) {
819    const n = activeNotes[key];
820    if (n) {
821      if (n.velocity > maxMusicianVel) maxMusicianVel = n.velocity;
822      if ((_b = n.features) == null ? void 0 : _b.isClimaxing) hasClimax = true;
823      if ((_c = n.features) == null ? void 0 : _c.isPhraseAttack) hasAttack = true;
824    }
825  }
826  const maxVelNorm = maxMusicianVel / 127;
827  const modeMultiplier = lightingMode === "subtle" ? 0.6 : 1;
828  const speedMultiplier = lightingMode === "subtle" ? 0.5 : 1;
829  const dynamicRange = profile.peakIntensity - profile.baseIntensity;
830  const musicEnergy = (globalIntensity || 0) * 0.55 + maxVelNorm * 0.45;
831  const climaxBoost = (hasClimax ? 0.28 : 0) + (hasAttack ? 0.15 : 0);
832  const rawIntensity = (profile.baseIntensity + musicEnergy * dynamicRange + beatPulse * profile.beatPulseAmount + climaxBoost) * modeMultiplier;
833  const effectiveIntensity = Math.max(0, Math.min(1, !isNaN(rawIntensity) && isFinite(rawIntensity) ? rawIntensity : profile.baseIntensity));
834  const trussY = 15;
835  const halfWidth = (stageWidth || 1200) / 2;
836  const beamCount = profile.beamCount || 5;
837  const rigBeams = reactExports.useMemo(() => {
838    const beams = [];
839    const time = isPlaying && safeTime ? safeTime : 0;
840    const sweepRate = effectiveBps * 0.6 * profile.sweepSpeedMultiplier * speedMultiplier;
841    for (let i = 0; i < beamCount; i++) {
842      const spreadRatio = i / (beamCount - 1 || 1) - 0.5;
843      const originX = halfWidth + spreadRatio * stageWidth * 0.75;
844      const originY = trussY;
845      const phaseOffset = i * (Math.PI / (beamCount * 0.75));
846      const primarySway = Math.sin(time * sweepRate + phaseOffset);
847      const harmonicSway = Math.sin(time * sweepRate * 1.5 + phaseOffset * 1.2) * 0.35;
848      const combinedSway = primarySway + harmonicSway;
849      const maxSweepPx = profile.maxBeamSpreadAngle / 45 * 220;
850      const targetX = originX + combinedSway * maxSweepPx;
851      const targetY = stageHeight - 20;
852      const beamColor = profile.beamColors[i % profile.beamColors.length];
853      const baseBeamOpacity = 0.18 + effectiveIntensity * 0.38 + downbeatPulse * 0.18;
854      beams.push({
855        id: `rig-beam-${i}`,
856        originX,
857        originY,
858        targetX,
859        targetY,
860        widthTop: 14 + effectiveIntensity * 8,
861        widthBottom: 85 + effectiveIntensity * 55 + (hasClimax ? 40 : 0),
862        color: beamColor,
863        opacity: Math.min(0.85, baseBeamOpacity * modeMultiplier)
864      });
865    }
866    return beams;
867  }, [
868    beamCount,
869    halfWidth,
870    stageWidth,
871    stageHeight,
872    trussY,
873    isPlaying,
874    safeTime,
875    effectiveBps,
876    profile,
877    speedMultiplier,
878    effectiveIntensity,
879    downbeatPulse,
880    hasClimax,
881    modeMultiplier
882  ]);
883  const characterSpotlights = reactExports.useMemo(() => {
884    const spots = [];
885    musicians.forEach((m) => {
886      const pos = musicianPositions[m.uniqueName];
887      if (!pos) return;
888      const noteData = activeNotes[m.uniqueName];
889      const vel = (noteData == null ? void 0 : noteData.velocity) || 0;
890      const isActive = vel > 0;
891      const isSoloed = soloedIndex === m.index;
892      const isSelected = selectedTrackIndex === m.index;
893      if (!isActive && !isSoloed && !isSelected) return;
894      const velNorm = Math.max(0.2, vel / 127);
895      const trackColor = pos.trackColor || profile.trussColor;
896      const originOffset = (pos.posX - halfWidth) * 0.55;
897      const beamOriginX = halfWidth + originOffset;
898      const beamOriginY = trussY;
899      const soloBoost = isSoloed || isSelected ? 0.35 : 0;
900      const spotOpacity = Math.min(0.95, (0.35 + velNorm * 0.45 + soloBo
900ost) * modeMultiplier);
901      const coneOpacity = Math.min(0.75, (0.18 + velNorm * 0.32 + soloBoost) * modeMultiplier);
902      const baseRadiusX = (48 + velNorm * 28) * pos.scale;
903      const baseRadiusY = (16 + velNorm * 10) * pos.scale;
904      const feetY = pos.posY + 78 * pos.scale;
905      spots.push({
906        musicianKey: m.uniqueName,
907        posX: pos.posX,
908        posY: pos.posY,
909        feetY,
910        scale: pos.scale,
911        color: trackColor,
912        velocity: vel,
913        isSoloed,
914        isSelected,
915        spotOpacity,
916        coneOpacity,
917        puddleRadiusX: baseRadiusX * (isSoloed ? 1.25 : 1),
918        puddleRadiusY: baseRadiusY * (isSoloed ? 1.25 : 1),
919        beamOriginX,
920        beamOriginY
921      });
922    });
923    return spots;
924  }, [musicians, musicianPositions, activeNotes, soloedIndex, selectedTrackIndex, profile.trussColor, halfWidth, trussY, modeMultiplier]);
925  const showBlinder = profile.hasBlinders && (hasClimax || profile.hasStrobes && downbeatPulse > 0.85);
926  const blinderOpacity = hasClimax ? 0.35 : downbeatPulse * 0.22;
927  return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "southpark-stage-lighting pointer-events-none select-none", children: [
928    /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [
929      /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spRigBeamGradSoft", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [
930        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.75" }),
931        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: "#ffffff", stopOpacity: "0.38" }),
932        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: "#ffffff", stopOpacity: "0.16" }),
933        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "82%", stopColor: "#ffffff", stopOpacity: "0.04" }),
934        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ffffff", stopOpacity: "0.0" })
935      ] }),
936      /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spBeamGradSoft", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [
937        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "currentColor", stopOpacity: "0.65" }),
938        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: "currentColor", stopOpacity: "0.38" }),
939        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: "currentColor", stopOpacity: "0.16" }),
940        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "82%", stopColor: "currentColor", stopOpacity: "0.04" }),
941        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "currentColor", stopOpacity: "0.0" })
942      ] }),
943      /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spRigBeamGrad", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [
944        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.75" }),
945        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: "#ffffff", stopOpacity: "0.35" }),
946        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "70%", stopColor: "#ffffff", stopOpacity: "0.1" }),
947        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ffffff", stopOpacity: "0.0" })
948      ] }),
949      /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: "spTrussWashGrad", cx: "50%", cy: "50%", r: "50%", children: [
950        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: profile.ambientColors[0], stopOpacity: "0.65" }),
951        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: profile.ambientColors[0], stopOpacity: "0.25" }),
952        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: profile.ambientColors[0], stopOpacity: "0" })
953      ] }),
954      /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: "spCrossStageGrad", cx: "50%", cy: "50%", r: "50%", children: [
955        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: profile.ambientColors[1], stopOpacity: "0.45" }),
956        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "60%", stopColor: profile.ambientColors[1], stopOpacity: "0.15" }),
957        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: profile.ambientColors[1], stopOpacity: "0" })
958      ] }),
959      /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: "spFloorPuddleGrad", cx: "50%", cy: "50%", r: "50%", children: [
960        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.85" }),
961        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "35%", stopColor: "#ffffff", stopOpacity: "0.35" }),
962        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "70%", stopColor: "#ffffff", stopOpacity: "0.08" }),
963        /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ffffff", stopOpacity: "0" })
964      ] }),
965      characterSpotlights.map((spot) => {
966        const keySafe = spot.musicianKey.replace(/[^a-zA-Z0-9_-]/g, "_");
967        return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
968          /* @__PURE__ */ jsxRuntimeExports.jsxs(
969            "linearGradient",
970            {
971              id: `spFollowBeamGrad-${keySafe}`,
972              x1: "0%",
973              y1: "0%",
974              x2: "0%",
975              y2: "100%",
976              children: [
977                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: spot.color, stopOpacity: "0.80" }),
978                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: spot.color, stopOpacity: "0.55" }),
979                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: spot.color, stopOpacity: "0.28" }),
980                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "80%", stopColor: spot.color, stopOpacity: "0.10" }),
981                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "94%", stopColor: spot.color, stopOpacity: "0.02" }),
982                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: spot.color, stopOpacity: "0.0" })
983              ]
984            }
985          ),
986          /* @__PURE__ */ jsxRuntimeExports.jsxs(
987            "radialGradient",
988            {
989              id: `spFloorSpotGrad-${keySafe}`,
990              cx: "50%",
991              cy: "50%",
992              r: "50%",
993              children: [
994                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: spot.color, stopOpacity: "0.85" }),
995                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "35%", stopColor: spot.color, stopOpacity: "0.55" }),
996                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "68%", stopColor: spot.color, stopOpacity: "0.22" }),
997                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "88%", stopColor: spot.color, stopOpacity: "0.05" }),
998                /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: spot.color, stopOpacity: "0.0" })
999              ]
1000            }
1001          )
1002        ] }, `defs-spot-${keySafe}`);
1003      })
1004    ] }),
1005    /* @__PURE__ */ jsxRuntimeExports.jsx(
1006      "ellipse",
1007      {
1008        cx: halfWidth,
1009        cy: trussY + 40,
1010        rx: stageWidth * 0.55,
1011        ry: 90,
1012        fill: "url(#spTrussWashGrad)",
1013        opacity: 0.65 + effectiveIntensity * 0.35
1014      }
1015    ),
1016    /* @__PURE__ */ jsxRuntimeExports.jsx(
1017      "ellipse",
1018      {
1019        cx: halfWidth,
1020        cy: stageHeight * 0.65,
1021        rx: stageWidth * 0.65,
1022        ry: 140,
1023        fill: "url(#spCrossStageGrad)",
1024        opacity: 0.45 + effectiveIntensity * 0.35
1025      }
1026    ),
1027    /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "overhead-truss-fixtures", opacity: 0.7 + effectiveIntensity * 0.3, children: rigBeams.map((beam, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(${beam.originX}, ${beam.originY})`, children: [
1028      /* @__PURE__ */ jsxRuntimeExports.jsx(
1029        "polygon",
1030        {
1031          points: "-12,-8 12,-8 9,4 -9,4",
1032          fill: "#18181b",
1033          stroke: "#27272a",
1034          strokeWidth: "1.5"
1035        }
1036      ),
1037      /* @__PURE__ */ jsxRuntimeExports.jsx(
1038        "circle",
1039        {
1040          cx: "0",
1041          cy: "2",
1042          r: Math.max(1, 6 + (beatPulse || 0) * 2),
1043          fill: beam.color,
1044          fillOpacity: 0.25
1045        }
1046      ),
1047      /* @__PURE__ */ jsxRuntimeExports.jsx(
1048        "circle",
1049        {
1050          cx: "0",
1051          cy: "2",
1052          r: Math.max(1, 3 + (beatPulse || 0) * 1.5),
1053          fill: beam.color
1054        }
1055      )
1056    ] }, `fixture-${idx}`)) }),
1057    /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "rig-volumetric-beams", children: rigBeams.map((beam) => {
1058      const topHalf = beam.widthTop / 2;
1059      const botHalf = beam.widthBottom / 2;
1060      const points = [
1061        `${beam.originX - topHalf},${beam.originY}`,
1062        `${beam.originX + topHalf},${beam.originY}`,
1063        `${beam.targetX + botHalf},${beam.targetY}`,
1064        `${beam.targetX - botHalf},${beam.targetY}`
1065      ].join(" ");
1066      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { opacity: beam.opacity, children: [
1067        /* @__PURE__ */ jsxRuntimeExports.jsx(
1068          "polygon",
1069          {
1070            points,
1071            color: beam.color,
1072            fill: "url(#spBeamGradSoft)",
1073            opacity: 0.8
1074          }
1075        ),
1076        /* @__PURE__ */ jsxRuntimeExports.jsx(
1077          "polygon",
1078          {
1079            points,
1080            fill: "url(#spRigBeamGradSoft)",
1081            opacity: 0.65
1082          }
1083        )
1084      ] }, beam.id);
1085    }) }),
1086    /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "character-follow-spots", children: characterSpotlights.map((spot) => {
1087      const keySafe = spot.musicianKey.replace(/[^a-zA-Z0-9_-]/g, "_");
1088      const topW = 10 * spot.scale;
1089      const botW = spot.puddleRadiusX * 1.35;
1090      const coneBottomY = spot.feetY + 6 * spot.scale;
1091      const conePoints = [
1092        `${spot.beamOriginX - topW},${spot.beamOriginY}`,
1093        `${spot.beamOriginX + topW},${spot.beamOriginY}`,
1094        `${spot.posX + botW},${coneBottomY}`,
1095        `${spot.posX - botW},${coneBottomY}`
1096      ].join(" ");
1097      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1098        /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1099          /* @__PURE__ */ jsxRuntimeExports.jsx(
1100            "polygon",
1101            {
1102              points: conePoints,
1103              fill: `url(#spFollowBeamGrad-${keySafe})`,
1104              opacity: spot.coneOpacity * 0.85
1105            }
1106          ),
1107          /* @__PURE__ */ jsxRuntimeExports.jsx(
1108            "polygon",
1109            {
1110              points: conePoints,
1111              fill: "url(#spRigBeamGradSoft)",
1112              opacity: spot.coneOpacity * 0.45
1113            }
1114          )
1115        ] }),
1116        /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1117          /* @__PURE__ */ jsxRuntimeExports.jsx(
1118            "ellipse",
1119            {
1120              cx: spot.posX,
1121              cy: spot.feetY,
1122              rx: spot.puddleRadiusX,
1123              ry: spot.puddleRadiusY,
1124              fill: `url(#spFloorSpotGrad-${keySafe})`,
1125              opacity: spot.spotOpacity * 0.85
1126            }
1127          ),
1128          /* @__PURE__ */ jsxRuntimeExports.jsx(
1129            "ellipse",
1130            {
1131              cx: spot.posX,
1132              cy: spot.feetY,
1133              rx: spot.puddleRadiusX * 0.6,
1134              ry: spot.puddleRadiusY * 0.6,
1135              fill: "url(#spFloorPuddleGrad)",
1136              opacity: spot.spotOpacity * 0.55
1137            }
1138          )
1139        ] }),
1140        (spot.isSoloed || spot.isSelected) && /* @__PURE__ */ jsxRuntimeExports.jsx("g", { transform: `translate(${spot.posX}, ${spot.posY - 65 * spot.scale})`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1141          "circle",
1142          {
1143            cx: "0",
1144            cy: "0",
1145            r: 42 * spot.scale,
1146            fill: "none",
1147            stroke: spot.color,
1148            strokeWidth: "2.5",
1149            strokeDasharray: "5 3",
1150            opacity: 0.8,
1151            className: "animate-spin",
1152            style: { animationDuration: "8s" }
1153          }
1154        ) })
1155      ] }, `spot-${spot.musicianKey}`);
1156    }) }),
1157    showBlinder && /* @__PURE__ */ jsxRuntimeExports.jsx(
1158      "rect",
1159      {
1160        x: "0",
1161        y: "0",
1162        width: stageWidth,
1163        height: stageHeight,
1164        fill: profile.blinderColor,
1165        opacity: blinderOpacity,
1166        style: { transition: "opacity 0.08s ease-out" }
1167      }
1168    )
1169  ] });
1170};
1171const SouthparkStageLighting = React.memo(SouthparkStageLightingComponent);
1172const BANNER_THEMES = {
1173  // 1. Classical Opera House (Arch proscenium with red velvet & gilded carvings)
1174  classical: {
1175    themeId: "classical",
1176    hangLeft: { x: 380, y: 12 },
1177    hangRight: { x: 820, y: 12 },
1178    bannerX: 320,
1179    bannerY: 98,
1180    bannerWidth: 560,
1181    bannerHeight: 104,
1182    hardwareType: "silk_cords",
1183    cardStyle: {
1184      fill: "#4a0e17",
1185      stroke: "#d4af37",
1186      strokeWidth: 3.5,
1187      rx: 14,
1188      titleColor: "#fff5ea",
1189      artistColor: "#e0c068",
1190      fontFamily: 'serif, "Times New Roman", Georgia',
1191      accentColor: "#d4af37",
1192      badgeText: "OPERA HALL"
1193    }
1194  },
1195  // 2. Stadium Concert Arena (Overhead heavy concert box truss)
1196  rock: {
1197    themeId: "rock",
1198    hangLeft: { x: 360, y: 0 },
1199    hangRight: { x: 840, y: 0 },
1200    bannerX: 310,
1201    bannerY: 86,
1202    bannerWidth: 580,
1203    bannerHeight: 100,
1204    hardwareType: "steel_cables",
1205    cardStyle: {
1206      fill: "#18181b",
1207      stroke: "#71717a",
1208      strokeWidth: 3,
1209      rx: 6,
1210      titleColor: "#fafafa",
1211      artistColor: "#f59e0b",
1212      fontFamily: 'Impact, "Arial Black", sans-serif',
1213      accentColor: "#e11d48",
1214      badgeText: "LIVE TOUR"
1215    }
1216  },
1217  // 3. Country Barn (Overhead horizontal rustic timber beam)
1218  country: {
1219    themeId: "country",
1220    hangLeft: { x: 375, y: 38 },
1221    hangRight: { x: 825, y: 38 },
1222    bannerX: 325,
1223    bannerY: 108,
1224    bannerWidth: 550,
1225    bannerHeight: 104,
1226    hardwareType: "hemp_rope",
1227    cardStyle: {
1228      fill: "#3e2723",
1229      stroke: "#8d6e63",
1230      strokeWidth: 4,
1231      rx: 8,
1232      titleColor: "#fff8e1",
1233      artistColor: "#ffcc80",
1234      fontFamily: '"Courier New", Courier, monospace',
1235      accentColor: "#bcaaa4",
1236      badgeText: "BARN STAGE"
1237    }
1238  },
1239  // 4. Vintage Jazz & Blues Club (Lighting pipe in front of warm brick wall)
1240  jazz: {
1241    themeId: "jazz",
1242    hangLeft: { x: 390, y: 16 },
1243    hangRight: { x: 810, y: 16 },
1244    bannerX: 335,
1245    bannerY: 96,
1246    bannerWidth: 530,
1247    bannerHeight: 98,
1248    hardwareType: "brass_chains",
1249    cardStyle: {
1250      fill: "#1c1917",
1251      stroke: "#b45309",
1252      strokeWidth: 3,
1253      rx: 16,
1254      titleColor: "#fef3c7",
1255      artistColor: "#f59e0b",
1256      fontFamily: '"Trebuchet MS", "Lucida Grande", sans-serif',
1257      accentColor: "#d97706",
1258      badgeText: "BLUE NOTE"
1259    }
1260  },
1261  // 5. Techno & EDM (Spaceship bridge bulkhead ceiling looking into cosmos)
1262  techno: {
1263    themeId: "techno",
1264    hangLeft: { x: 370, y: 0 },
1265    hangRight: { x: 830, y: 0 },
1266    bannerX: 315,
1267    bannerY: 82,
1268    bannerWidth: 570,
1269    bannerHeight: 96,
1270    hardwareType: "laser_conduit",
1271    cardStyle: {
1272      fill: "#080e1a",
1273      stroke: "#06b6d4",
1274      strokeWidth: 2.5,
1275      rx: 8,
1276      titleColor: "#67e8f9",
1277      artistColor: "#a855f7",
1278      fontFamily: "system-ui, -apple-system, sans-serif",
1279      accentColor: "#06b6d4",
1280      badgeText: "HOLONET // 01"
1281    }
1282  },
1283  // 6. Anime & Animation (Park pavilion bandstand arch with blue sky)
1284  anime: {
1285    themeId: "anime",
1286    hangLeft: { x: 390, y: 55 },
1287    hangRight: { x: 810, y: 55 },
1288    bannerX: 335,
1289    bannerY: 115,
1290    bannerWidth: 530,
1291    bannerHeight: 100,
1292    hardwareType: "ribbons",
1293    cardStyle: {
1294      fill: "#fef9c3",
1295      stroke: "#38bdf8",
1296      strokeWidth: 4,
1297      rx: 24,
1298      titleColor: "#0f172a",
1299      artistColor: "#ec4899",
1300      fontFamily: '"Comic Sans MS", "Chalkboard SE", sans-serif',
1301      accentColor: "#f43f5e",
1302      badgeText: "★ FESTIVAL ★"
1303    }
1304  },
1305  // 7. Underground Trap (Concrete warehouse cross-trusses & industrial beams)
1306  trap: {
1307    themeId: "trap",
1308    hangLeft: { x: 360, y: 20 },
1309    hangRight: { x: 840, y: 20 },
1310    bannerX: 315,
1311    bannerY: 94,
1312    bannerWidth: 570,
1313    bannerHeight: 102,
1314    hardwareType: "rusted_chains",
1315    cardStyle: {
1316      fill: "#121214",
1317      stroke: "#eab308",
1318      strokeWidth: 3,
1319      rx: 4,
1320      titleColor: "#f43f5e",
1321      artistColor: "#facc15",
1322      fontFamily: "Impact, sans-serif",
1323      accentColor: "#eab308",
1324      badgeText: "UNDERGROUND"
1325    }
1326  },
1327  // 8. Heavy Metal (Industrial proscenium arch with stage cans & smoke)
1328  metal: {
1329    themeId: "metal",
1330    hangLeft: { x: 370, y: 5 },
1331    hangRight: { x: 830, y: 5 },
1332    bannerX: 310,
1333    bannerY: 88,
1334    bannerWidth: 580,
1335    bannerHeight: 104,
1336    hardwareType: "spiked_chains",
1337    cardStyle: {
1338      fill: "#09090b",
1339      stroke: "#dc2626",
1340      strokeWidth: 3.5,
1341      rx: 2,
1342      titleColor: "#ffffff",
1343      artistColor: "#ef4444",
1344      fontFamily: 'Impact, "Arial Black", sans-serif',
1345      accentColor: "#dc2626",
1346      badgeText: "METAL FEST"
1347    }
1348  },
1349  // 9. Tropical Reggae (Beach boardwalk posts & festoon lights)
1350  reggae: {
1351    themeId: "reggae",
1352    hangLeft: { x: 330, y: 75 },
1353    hangRight: { x: 870, y: 75 },
1354    bannerX: 325,
1355    bannerY: 130,
1356    bannerWidth: 550,
1357    bannerHeight: 105,
1358    hardwareType: "hemp_rope",
1359    cardStyle: {
1360      fill: "#152417",
1361      stroke: "#16a34a",
1362      strokeWidth: 4,
1363      rx: 12,
1364      titleColor: "#fef08a",
1365      artistColor: "#ef4444",
1366      fontFamily: '"Trebuchet MS", sans-serif',
1367      accentColor: "#eab308",
1368      badgeText: "SUNSET ROOTS"
1369    }
1370  },
1371  // 10. Woodland Folk (Pine forest clearing canopy trees)
1372  folk: {
1373    themeId: "folk",
1374    hangLeft: { x: 290, y: 30 },
1375    hangRight: { x: 910, y: 30 },
1376    bannerX: 330,
1377    bannerY: 108,
1378    bannerWidth: 540,
1379    bannerHeight: 102,
1380    hardwareType: "clothespins",
1381    cardStyle: {
1382      fill: "#292524",
1383      stroke: "#a8a29e",
1384      strokeWidth: 3.5,
1385      rx: 10,
1386      titleColor: "#f5f5f4",
1387      artistColor: "#d6d3d1",
1388      fontFamily: "Georgia, serif",
1389      accentColor: "#84cc16",
1390      badgeText: "CAMPFIRE SESSIONS"
1391    }
1392  },
1393  // 11. Cathedral Religious (Gothic stone vaulted arches)
1394  religious: {
1395    themeId: "religious",
1396    hangLeft: { x: 390, y: 8 },
1397    hangRight: { x: 810, y: 8 },
1398    bannerX: 330,
1399    bannerY: 92,
1400    bannerWidth: 540,
1401    bannerHeight: 112,
1402    hardwareType: "gold_bullion",
1403    cardStyle: {
1404      fill: "#1e1b4b",
1405      stroke: "#eab308",
1406      strokeWidth: 3.5,
1407      rx: 14,
1408      titleColor: "#ffffff",
1409      artistColor: "#fef08a",
1410      fontFamily: '"Times New Roman", serif',
1411      accentColor: "#facc15",
1412      badgeText: "SACRED CHOIR"
1413    }
1414  },
1415  // 12. Broadway Occasions (Velvet valance arch with sparkling fairy lights)
1416  occasions: {
1417    themeId: "occasions",
1418    hangLeft: { x: 380, y: 22 },
1419    hangRight: { x: 820, y: 22 },
1420    bannerX: 315,
1421    bannerY: 96,
1422    bannerWidth: 570,
1423    bannerHeight: 106,
1424    hardwareType: "silk_cords",
1425    cardStyle: {
1426      fill: "#31102b",
1427      stroke: "#f59e0b",
1428      strokeWidth: 4,
1429      rx: 18,
1430      titleColor: "#fffbeb",
1431      artistColor: "#fde68a",
1432      fontFamily: "Georgia, serif",
1433      accentColor: "#fbbf24",
1434      badgeText: "★ GALA NIGHT ★"
1435    }
1436  },
1437  // 13. Hip Hop (Midnight alley utility power cables)
1438  hip_hop: {
1439    themeId: "hip_hop",
1440    hangLeft: { x: 340, y: 38 },
1441    hangRight: { x: 860, y: 52 },
1442    bannerX: 320,
1443    bannerY: 108,
1444    bannerWidth: 560,
1445    bannerHeight: 102,
1446    hardwareType: "power_lines",
1447    cardStyle: {
1448      fill: "#18181b",
1449      stroke: "#a855f7",
1450      strokeWidth: 3.5,
1451      rx: 4,
1452      titleColor: "#38bdf8",
1453      artistColor: "#f43f5e",
1454      fontFamily: 'Impact, "Arial Black", sans-serif',
1455      accentColor: "#a855f7",
1456      badgeText: "ALLEY CYPHER"
1457    }
1458  },
1459  // 14. Pop Arena (Overhead sleek concert lighting truss)
1460  pop: {
1461    themeId: "pop",
1462    hangLeft: { x: 370, y: 0 },
1463    hangRight: { x: 830, y: 0 },
1464    bannerX: 320,
1465    bannerY: 86,
1466    bannerWidth: 560,
1467    bannerHeight: 98,
1468    hardwareType: "tension_wires",
1469    cardStyle: {
1470      fill: "#0f172a",
1471      stroke: "#ec4899",
1472      strokeWidth: 3,
1473      rx: 18,
1474      titleColor: "#fdf2f8",
1475      artistColor: "#38bdf8",
1476      fontFamily: "system-ui, -apple-system, sans-serif",
1477      accentColor: "#ec4899",
1478      badgeText: "POP SPECTACULAR"
1479    }
1480  },
1481  // 15. Alternative Rock (Brick wall lighting truss)
1482  alt_rock: {
1483    themeId: "alt_rock",
1484    hangLeft: { x: 380, y: 20 },
1485    hangRight: { x: 820, y: 20 },
1486    bannerX: 325,
1487    bannerY: 94,
1488    bannerWidth: 550,
1489    bannerHeight: 98,
1490    hardwareType: "turnbuckles",
1491    cardStyle: {
1492      fill: "#09090b",
1493      stroke: "#8b5cf6",
1494      strokeWidth: 3,
1495      rx: 8,
1496      titleColor: "#ede9fe",
1497      artistColor: "#a78bfa",
1498      fontFamily: '"Trebuchet MS", sans-serif',
1499      accentColor: "#8b5cf6",
1500      badgeText: "INDIE ROCK"
1501    }
1502  },
1503  // 16. Karaoke Disco (Overhead disco lighting bar)
1504  karaoke: {
1505    themeId: "karaoke",
1506    hangLeft: { x: 360, y: 15 },
1507    hangRight: { x: 840, y: 15 },
1508    bannerX: 320,
1509    bannerY: 90,
1510    bannerWidth: 560,
1511    bannerHeight: 104,
1512    hardwareType: "disco_streamers",
1513    cardStyle: {
1514      fill: "#1e1b4b",
1515      stroke: "#f43f5e",
1516      strokeWidth: 3.5,
1517      rx: 20,
1518      titleColor: "#fef08a",
1519      artistColor: "#38bdf8",
1520      fontFamily: '"Arial Black", sans-serif',
1521      accentColor: "#f43f5e",
1522      badgeText: "DISCO LOUNGE"
1523    }
1524  },
1525  // 17. Latin Plaza (Colonial street balcony festoon light line)
1526  latin: {
1527    themeId: "latin",
1528    hangLeft: { x: 310, y: 50 },
1529    hangRight: { x: 890, y: 50 },
1530    bannerX: 325,
1531    bannerY: 118,
1532    bannerWidth: 550,
1533    bannerHeight: 105,
1534    hardwareType: "bunting",
1535    cardStyle: {
1536      fill: "#7c2d12",
1537      stroke: "#fb923c",
1538      strokeWidth: 4,
1539      rx: 16,
1540      titleColor: "#fff7ed",
1541      artistColor: "#fed7aa",
1542      fontFamily: "Georgia, serif",
1543      accentColor: "#ea580c",
1544      badgeText: "GRAN FIESTA"
1545    }
1546  },
1547  // 18. 16-Bit Game Music (Cavern stalactite crags)
1548  game_music: {
1549    themeId: "game_music",
1550    hangLeft: { x: 345, y: 15 },
1551    hangRight: { x: 855, y: 15 },
1552    bannerX: 325,
1553    bannerY: 92,
1554    bannerWidth: 550,
1555    bannerHeight: 100,
1556    hardwareType: "pixel_chains",
1557    cardStyle: {
1558      fill: "#022c22",
1559      stroke: "#10b981",
1560      strokeWidth: 3.5,
1561      rx: 0,
1562      titleColor: "#a7f3d0",
1563      artistColor: "#6ee7b7",
1564      fontFamily: '"Courier New", monospace',
1565      accentColor: "#34d399",
1566      badgeText: "QUEST // LVL 99"
1567    }
1568  },
1569  // 19. Indie Wildflower Meadow (Tree branches framing clearing)
1570  indie: {
1571    themeId: "indie",
1572    hangLeft: { x: 300, y: 45 },
1573    hangRight: { x: 900, y: 45 },
1574    bannerX: 330,
1575    bannerY: 114,
1576    bannerWidth: 540,
1577    bannerHeight: 100,
1578    hardwareType: "clothespins",
1579    cardStyle: {
1580      fill: "#fffbeb",
1581      stroke: "#d97706",
1582      strokeWidth: 3,
1583      rx: 12,
1584      titleColor: "#451a03",
1585      artistColor: "#b45309",
1586      fontFamily: "Georgia, serif",
1587      accentColor: "#f59e0b",
1588      badgeText: "ACOUSTIC MEADOW"
1589    }
1590  },
1591  // 20. Sci-Fi Bioluminescent Forest (Ancient glowing tree branches)
1592  "sci-fi": {
1593    themeId: "sci-fi",
1594    hangLeft: { x: 330, y: 15 },
1595    hangRight: { x: 870, y: 15 },
1596    bannerX: 320,
1597    bannerY: 92,
1598    bannerWidth: 560,
1599    bannerHeight: 100,
1600    hardwareType: "vines",
1601    cardStyle: {
1602      fill: "#042f2e",
1603      stroke: "#2dd4bf",
1604      strokeWidth: 3,
1605      rx: 16,
1606      titleColor: "#ccfbf1",
1607      artistColor: "#5eead4",
1608      fontFamily: "system-ui, sans-serif",
1609      accentColor: "#2dd4bf",
1610      badgeText: "MYSTIC GROVE"
1611    }
1612  },
1613  // 21. Ancient World Temple (Carved teakwood lintel beam)
1614  world: {
1615    themeId: "world",
1616    hangLeft: { x: 370, y: 35 },
1617    hangRight: { x: 830, y: 35 },
1618    bannerX: 325,
1619    bannerY: 108,
1620    bannerWidth: 550,
1621    bannerHeight: 105,
1622    hardwareType: "temple_cords",
1623    cardStyle: {
1624      fill: "#431407",
1625      stroke: "#f59e0b",
1626      strokeWidth: 3.5,
1627      rx: 8,
1628      titleColor: "#fef3c7",
1629      artistColor: "#fde68a",
1630      fontFamily: "serif",
1631      accentColor: "#d97706",
1632      badgeText: "TEMPLE COURTYARD"
1633    }
1634  },
1635  // 22. National Ceremonial Hall (Top horizontal lighting truss)
1636  national: {
1637    themeId: "national",
1638    hangLeft: { x: 380, y: 0 },
1639    hangRight: { x: 820, y: 0 },
1640    bannerX: 320,
1641    bannerY: 88,
1642    bannerWidth: 560,
1643    bannerHeight: 105,
1644    hardwareType: "gold_bullion",
1645    cardStyle: {
1646      fill: "#0f172a",
1647      stroke: "#eab308",
1648      strokeWidth: 4,
1649      rx: 12,
1650      titleColor: "#f8fafc",
1651      artistColor: "#fef08a",
1652      fontFamily: '"Times New Roman", serif',
1653      accentColor: "#facc15",
1654      badgeText: "CONCERT JUBILEE"
1655    }
1656  },
1657  // 23. Original Geometric Studio (Ceiling soffit beam)
1658  original: {
1659    themeId: "original",
1660    hangLeft: { x: 390, y: 5 },
1661    hangRight: { x: 810, y: 5 },
1662    bannerX: 325,
1663    bannerY: 86,
1664    bannerWidth: 550,
1665    bannerHeight: 96,
1666    hardwareType: "tension_wires",
1667    cardStyle: {
1668      fill: "#09090b",
1669      stroke: "#a1a1aa",
1670      strokeWidth: 2,
1671      rx: 6,
1672      titleColor: "#f4f4f5",
1673      artistColor: "#a1a1aa",
1674      fontFamily: "system-ui, -apple-system, sans-serif",
1675      accentColor: "#e4e4e7",
1676      badgeText: "STUDIO MASTER"
1677    }
1678  },
1679  // 24. Midnight R&B Lounge (Velvet canopy valance)
1680  rnb: {
1681    themeId: "rnb",
1682    hangLeft: { x: 380, y: 38 },
1683    hangRight: { x: 820, y: 38 },
1684    bannerX: 330,
1685    bannerY: 106,
1686    bannerWidth: 540,
1687    bannerHeight: 100,
1688    hardwareType: "brass_chains",
1689    cardStyle: {
1690      fill: "#2e1065",
1691      stroke: "#c084fc",
1692      strokeWidth: 3,
1693      rx: 14,
1694      titleColor: "#faf5ff",
1695      artistColor: "#e9d5ff",
1696      fontFamily: "Georgia, serif",
1697      accentColor: "#c084fc",
1698      badgeText: "MIDNIGHT LOUNGE"
1699    }
1700  },
1701  // 25. Reggaeton Neon Club (Warehouse ceiling truss)
1702  reggaeton: {
1703    themeId: "reggaeton",
1704    hangLeft: { x: 370, y: 20 },
1705    hangRight: { x: 830, y: 20 },
1706    bannerX: 315,
1707    bannerY: 96,
1708    bannerWidth: 570,
1709    bannerHeight: 102,
1710    hardwareType: "neon_cables",
1711    cardStyle: {
1712      fill: "#18181b",
1713      stroke: "#06b6d4",
1714      strokeWidth: 3.5,
1715      rx: 10,
1716      titleColor: "#f43f5e",
1717      artistColor: "#22d3ee",
1718      fontFamily: "Impact, sans-serif",
1719      accentColor: "#f43f5e",
1720      badgeText: "RITMO URBANO"
1721    }
1722  },
1723  // Default Fallback
1724  default: {
1725    themeId: "default",
1726    hangLeft: { x: 380, y: 0 },
1727    hangRight: { x: 820, y: 0 },
1728    bannerX: 325,
1729    bannerY: 92,
1730    bannerWidth: 550,
1731    bannerHeight: 102,
1732    hardwareType: "hemp_rope",
1733    cardStyle: {
1734      fill: "#1c1917",
1735      stroke: "#eab308",
1736      strokeWidth: 3.5,
1737      rx: 12,
1738      titleColor: "#fef08a",
1739      artistColor: "#fde047",
1740      fontFamily: '"Trebuchet MS", sans-serif',
1741      accentColor: "#eab308",
1742      badgeText: "HAMIENET STAGE"
1743    }
1744  }
1745};
1746function getBannerThemeId(imageUrlOrId) {
1747  if (!imageUrlOrId) return "default";
1748  const clean = imageUrlOrId.toLowerCase().trim();
1749  const match = clean.match(/\/([^/]+)\.(jpg|png|webp|svg)/i);
1750  const fileKey = match && match[1] ? match[1].toLowerCase() : clean;
1751  const keys = [
1752    "classical",
1753    "rock",
1754    "country",
1755    "jazz",
1756    "techno",
1757    "anime",
1758    "trap",
1759    "metal",
1760    "reggae",
1761    "folk",
1762    "religious",
1763    "occasions",
1764    "hip_hop",
1765    "pop",
1766    "alt_rock",
1767    "karaoke",
1768    "latin",
1769    "game_music",
1770    "indie",
1771    "sci-fi",
1772    "world",
1773    "national",
1774    "original",
1775    "rnb",
1776    "reggaeton"
1777  ];
1778  for (const k of keys) {
1779    if (fileKey.includes(k)) return k;
1780  }
1781  return "default";
1782}
1783function renderHardwareStrand(anchor, target, type, accentColor = "#f59e0b") {
1784  const dx = target.x - anchor.x;
1785  const dy = target.y - anchor.y;
1786  const dist = Math.sqrt(dx * dx + dy * dy);
1787  if (dist <= 0) return null;
1788  switch (type) {
1789    case "steel_cables":
1790    case "tension_wires":
1791      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1792        /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: anchor.x - 5, y: anchor.y, width: 10, height: 6, rx: 2, fill: "#3f3f46", stroke: "#71717a", strokeWidth: 1 }),
1793        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y - 6, stroke: "#a1a1aa", strokeWidth: 2.5 }),
1794        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y - 6, stroke: "#ffffff", strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.6 }),
1795        /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 4, y: target.y - 12, width: 8, height: 10, rx: 2, fill: "#71717a", stroke: "#27272a", strokeWidth: 1 }),
1796        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "none", stroke: "#d4d4d8", strokeWidth: 2 })
1797      ] }, `cable-${anchor.x}-${target.x}`);
1798    case "turnbuckles":
1799      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1800        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y + 2, r: 4, fill: "#52525b" }),
1801        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 4, x2: target.x, y2: target.y - 14, stroke: "#71717a", strokeWidth: 2 }),
1802        /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 4.5, y: target.y - 14, width: 9, height: 12, rx: 2, fill: "#27272a", stroke: "#a1a1aa", strokeWidth: 1.5 }),
1803        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4.5, fill: "none", stroke: "#a855f7", strokeWidth: 2 })
1804      ] }, `turnbuckle-${anchor.x}-${target.x}`);
1805    case "brass_chains":
1806    case "spiked_chains":
1807    case "rusted_chains": {
1808      const isSpiked = type === "spiked_chains";
1809      const linkColor = type === "brass_chains" ? "#d97706" : isSpiked ? "#451a03" : "#78716c";
1810      const highlightColor = type === "brass_chains" ? "#fde68a" : isSpiked ? "#dc2626" : "#d6d3d1";
1811      const numLinks = Math.max(3, Math.floor(dist / 14));
1812      const links = [];
1813      for (let i = 0; i < numLinks; i++) {
1814        const t = i / (numLinks - 1);
1815        const lx = anchor.x + dx * t;
1816        const ly = anchor.y + dy * t;
1817        const isAlt = i % 2 === 0;
1818        links.push(
1819          /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1820            /* @__PURE__ */ jsxRuntimeExports.jsx(
1821              "ellipse",
1822              {
1823                cx: lx,
1824                cy: ly,
1825                rx: isAlt ? 3.5 : 2,
1826                ry: isAlt ? 6.5 : 8,
1827                fill: "none",
1828                stroke: linkColor,
1829                strokeWidth: 2
1830              }
1831            ),
1832            /* @__PURE__ */ jsxRuntimeExports.jsx(
1833              "ellipse",
1834              {
1835                cx: lx,
1836                cy: ly,
1837                rx: isAlt ? 2 : 1,
1838                ry: isAlt ? 4.5 : 5.5,
1839                fill: "none",
1840                stroke: highlightColor,
1841                strokeWidth: 0.7,
1842                opacity: 0.7
1843              }
1844            ),
1845            isSpiked && isAlt && /* @__PURE__ */ jsxRuntimeExports.jsx(
1846              "polygon",
1847              {
1848                points: `${lx - 5},${ly} ${lx},${ly - 4} ${lx + 5},${ly} ${lx},${ly + 4}`,
1849                fill: "#b91c1c"
1850              }
1851            )
1852          ] }, `link-${i}`)
1853        );
1854      }
1855      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1856        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 4.5, fill: linkColor }),
1857        links,
1858        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5.5, fill: "none", stroke: linkColor, strokeWidth: 2.5 })
1859      ] }, `chain-${anchor.x}-${target.x}`);
1860    }
1861    case "silk_cords":
1862    case "gold_bullion": {
1863      const cordColor = type === "silk_cords" ? "#eab308" : "#fbbf24";
1864      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1865        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 6, fill: cordColor }),
1866        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 3, fill: "#78350f" }),
1867        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x - 1.5, y1: anchor.y, x2: target.x - 1.5, y2: target.y, stroke: cordColor, strokeWidth: 2 }),
1868        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x + 1.5, y1: anchor.y, x2: target.x + 1.5, y2: target.y, stroke: cordColor, strokeWidth: 2 }),
1869        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#78350f", strokeWidth: 1, strokeDasharray: "3 3" }),
1870        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 6, fill: "none", stroke: cordColor, strokeWidth: 2.5 }),
1871        /* @__PURE__ */ jsxRuntimeExports.jsx(
1872          "path",
1873          {
1874            d: `M ${target.x - 3.5} ${target.y + 4} L ${target.x + 3.5} ${target.y + 4} L ${target.x + 5} ${target.y + 22} L ${target.x - 5} ${target.y + 22} Z`,
1875            fill: cordColor
1876          }
1877        ),
1878        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x - 3, y1: target.y + 22, x2: target.x - 3, y2: target.y + 27, stroke: cordColor, strokeWidth: 1.5 }),
1879        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x, y1: target.y + 22, x2: target.x, y2: target.y + 28, stroke: cordColor, strokeWidth: 1.5 }),
1880        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x + 3, y1: target.y + 22, x2: target.x + 3, y2: target.y + 27, stroke: cordColor, strokeWidth: 1.5 })
1881      ] }, `silk-${anchor.x}-${target.x}`);
1882    }
1883    case "ribbons":
1884      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1885        /* @__PURE__ */ jsxRuntimeExports.jsx(
1886          "path",
1887          {
1888            d: `M ${anchor.x} ${anchor.y} C ${anchor.x - 16} ${anchor.y - 12}, ${anchor.x - 16} ${anchor.y + 12}, ${anchor.x} ${anchor.y} C ${anchor.x + 16} ${anchor.y - 12}, ${anchor.x + 16} ${anchor.y + 12}, ${anchor.x} ${anchor.y}`,
1889            fill: "#f472b6",
1890            stroke: "#ec4899",
1891            strokeWidth: 1.5
1892          }
1893        ),
1894        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 3.5, fill: "#fbcfe8" }),
1895        /* @__PURE__ */ jsxRuntimeExports.jsx(
1896          "path",
1897          {
1898            d: `M ${anchor.x} ${anchor.y} Q ${anchor.x + (target.x > anchor.x ? 8 : -8)} ${(anchor.y + target.y) / 2} ${target.x} ${target.y}`,
1899            fill: "none",
1900            stroke: "#f472b6",
1901            strokeWidth: 3.5
1902          }
1903        ),
1904        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4.5, fill: "#fb7185" })
1905      ] }, `ribbon-${anchor.x}-${target.x}`);
1906    case "laser_conduit":
1907      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1908        /* @__PURE__ */ jsxRuntimeExports.jsx(
1909          "polygon",
1910          {
1911            points: `${anchor.x - 6},${anchor.y} ${anchor.x + 6},${anchor.y} ${anchor.x},${anchor.y + 8}`,
1912            fill: "#06b6d4"
1913          }
1914        ),
1915        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y, stroke: "#06b6d4", strokeWidth: 5, opacity: 0.3 }),
1916        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y, stroke: "#a5f3fc", strokeWidth: 2 }),
1917        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: (anchor.x + target.x) / 2, cy: (anchor.y + target.y) / 2, r: 3.5, fill: "#38bdf8" }),
1918        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "#0891b2", stroke: "#67e8f9", strokeWidth: 1.5 })
1919      ] }, `laser-${anchor.x}-${target.x}`);
1920    case "vines":
1921      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1922        /* @__PURE__ */ jsxRuntimeExports.jsx(
1923          "path",
1924          {
1925            d: `M ${anchor.x} ${anchor.y} Q ${anchor.x - 12} ${(anchor.y + target.y) * 0.4} ${anchor.x + 6} ${(anchor.y + target.y) * 0.7} T ${target.x} ${target.y}`,
1926            fill: "none",
1927            stroke: "#0d9488",
1928            strokeWidth: 3
1929          }
1930        ),
1931        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x - 8, cy: (anchor.y + target.y) * 0.38, r: 2.5, fill: "#5eead4" }),
1932        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x + 4, cy: (anchor.y + target.y) * 0.68, r: 3, fill: "#99f6e4" }),
1933        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4.5, fill: "#14b8a6" })
1934      ] }, `vines-${anchor.x}-${target.x}`);
1935    case "power_lines":
1936      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1937        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#27272a", strokeWidth: 3.5 }),
1938        /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 6, y: target.y - 12, width: 12, height: 8, rx: 1, fill: "#a1a1aa", transform: `rotate(-12 ${target.x} ${target.y - 8})` }),
1939        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4, fill: "#52525b" })
1940      ] }, `power-${anchor.x}-${target.x}`);
1941    case "disco_streamers":
1942      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1943        /* @__PURE__ */ jsxRuntimeExports.jsx(
1944          "path",
1945          {
1946            d: `M ${anchor.x} ${anchor.y} Q ${anchor.x + 10} ${(anchor.y + target.y) * 0.4} ${anchor.x - 8} ${(anchor.y + target.y) * 0.7} T ${target.x} ${target.y}`,
1947            fill: "none",
1948            stroke: "#ec4899",
1949            strokeWidth: 2.5,
1950            strokeDasharray: "4 2"
1951          }
1952        ),
1953        /* @__PURE__ */ jsxRuntimeExports.jsx(
1954          "path",
1955          {
1956            d: `M ${anchor.x + 4} ${anchor.y} Q ${anchor.x - 6} ${(anchor.y + target.y) * 0.5} ${target.x + 4} ${target.y}`,
1957            fill: "none",
1958            stroke: "#38bdf8",
1959            strokeWidth: 2
1960          }
1961        ),
1962        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "#f43f5e" })
1963      ] }, `streamers-${anchor.x}-${target.x}`);
1964    case "clothespins":
1965      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1966        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#a8a29e", strokeWidth: 2, strokeDasharray: "3 1" }),
1967        /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 3, y: target.y - 10, width: 6, height: 16, rx: 1.5, fill: "#d6d3d1", stroke: "#78716c", strokeWidth: 1 }),
1968        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x - 2, y1: target.y - 3, x2: target.x + 2, y2: target.y - 3, stroke: "#57534e", strokeWidth: 1 })
1969      ] }, `peg-${anchor.x}-${target.x}`);
1970    case "bunting":
1971      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1972        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#fb923c", strokeWidth: 2.5 }),
1973        /* @__PURE__ */ jsxRuntimeExports.jsx(
1974          "polygon",
1975          {
1976            points: `${(anchor.x + target.x) / 2 - 6},${(anchor.y + target.y) / 2} ${(anchor.x + target.x) / 2 + 6},${(anchor.y + target.y) / 2} ${(anchor.x + target.x) / 2},${(anchor.y + target.y) / 2 + 10}`,
1977            fill: "#f43f5e"
1978          }
1979        ),
1980        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4, fill: "#ea580c" })
1981      ] }, `bunting-${anchor.x}-${target.x}`);
1982    case "pixel_chains": {
1983      const numSteps = Math.max(3, Math.floor(dist / 14));
1984      const steps = [];
1985      for (let i = 0; i < numSteps; i++) {
1986        const t = i / (numSteps - 1);
1987        const px = Math.round(anchor.x + dx * t);
1988        const py = Math.round(anchor.y + dy * t);
1989        steps.push(
1990          /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: px - 2, y: py - 3, width: 5, height: 7, fill: "#10b981", stroke: "#064e3b", strokeWidth: 1 }, `pixel-${i}`)
1991        );
1992      }
1993      return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { children: steps }, `pxchain-${anchor.x}-${target.x}`);
1994    }
1995    case "temple_cords":
1996      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1997        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#d97706", strokeWidth: 3, strokeDasharray: "4 2" }),
1998        /* @__PURE__ */ jsxRuntimeExports.jsx(
1999          "path",
2000          {
2001            d: `M ${target.x - 4} ${target.y - 4} Q ${target.x} ${target.y - 8} ${target.x + 4} ${target.y - 4} L ${target.x + 5} ${target.y + 4} L ${target.x - 5} ${target.y + 4} Z`,
2002            fill: "#f59e0b",
2003            stroke: "#78350f",
2004            strokeWidth: 1
2005          }
2006        ),
2007        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y + 6, r: 1.5, fill: "#451a03" })
2008      ] }, `temple-${anchor.x}-${target.x}`);
2009    case "neon_cables":
2010      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2011        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#06b6d4", strokeWidth: 4, opacity: 0.4 }),
2012        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#f43f5e", strokeWidth: 2 }),
2013        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "#06b6d4" })
2014      ] }, `neoncable-${anchor.x}-${target.x}`);
2015    case "hemp_rope":
2016    default:
2017      return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2018        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 5, fill: "#78350f", stroke: "#d97706", strokeWidth: 1.5 }),
2019        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#b45309", strokeWidth: 3.5 }),
2020        /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#fef3c7", strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.7 }),
2021        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 6, fill: "#451a03", stroke: "#f59e0b", strokeWidth: 2 }),
2022        /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 2.5, fill: "#18181b" })
2023      ] }, `rope-${anchor.x}-${target.x}`);
2024  }
2025}
2026const SouthparkStageBannerComponent = ({
2027  track,
2028  backgroundUrl,
2029  currentTime = 0,
2030  bpm = 120,
2031  isPlaying = false,
2032  globalIntensity = 0,
2033  showBanner = true
2034}) => {
2035  const themeKey = reactExports.useMemo(() => getBannerThemeId(backgroundUrl), [backgroundUrl]);
2036  const preset = reactExports.useMemo(() => BANNER_THEMES[themeKey] || BANNER_THEMES.default, [themeKey]);
2037  const rawTitle = ((track == null ? void 0 : track.title) || "HamieNET Orchestra").trim();
2038  const rawArtist = ((track == null ? void 0 : track.artist) || "").trim();
2039  const isPlaceholderArtist = !rawArtist || rawArtist.toLowerCase() === "unknown" || rawArtist.toLowerCase() === "unknown artist" || rawArtist.toLowerCase() === "null" || rawArtist.toLowerCase() === "undefined";
2040  const cleanArtist = isPlaceholderArtist ? null : rawArtist;
2041  let displayTitle = rawTitle;
2042  let titleFontSize = 21;
2043  if (displayTitle.length > 38) {
2044    displayTitle = displayTitle.slice(0, 36) + "…";
2045    titleFontSize = 16;
2046  } else if (displayTitle.length > 26) {
2047    titleFontSize = 18;
2048  } else if (displayTitle.length > 18) {
2049    titleFontSize = 19.5;
2050  }
2051  let displayArtist = cleanArtist;
2052  let artistFontSize = 13.5;
2053  if (displayArtist && displayArtist.length > 34) {
2054    displayArtist = displayArtist.slice(0, 32) + "…";
2055    artistFontSize = 12;
2056  }
2057  const { bannerX, bannerY, bannerWidth, bannerHeight, cardStyle, hangLeft, hangRight, hardwareType } = preset;
2058  const centerX = bannerX + bannerWidth / 2;
2059  const currentBannerY = bannerY;
2060  const attachInset = 28;
2061  const attachLeft = { x: bannerX + attachInset, y: currentBannerY + 10 };
2062  const attachRight = { x: bannerX + bannerWidth - attachInset, y: currentBannerY + 10 };
2063  if (!showBanner) return null;
2064  const swayDuration = Math.max(1.5, 60 / (bpm || 120) * 2);
2065  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
2066    "g",
2067    {
2068      className: "southpark-stage-banner select-none pointer-events-none",
2069      style: {
2070        transformOrigin: `${centerX}px ${Math.min(hangLeft.y, hangRight.y)}px`,
2071        animation: isPlaying ? `spBannerSwayAnim ${swayDuration.toFixed(2)}s ease-in-out infinite alternate` : "none",
2072        willChange: "transform"
2073      },
2074      children: [
2075        /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [
2076          /* @__PURE__ */ jsxRuntimeExports.jsx("style", { children: `
2077                    @keyframes spBannerSwayAnim {
2078                        0% { transform: rotate(-0.85deg); }
2079                        100% { transform: rotate(0.85deg); }
2080                    }
2081                ` }),
2082          /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spBannerGoldGrad", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [
2083            /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#fef08a" }),
2084            /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "50%", stopColor: "#eab308" }),
2085            /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ca8a04" })
2086          ] })
2087        ] }),
2088        renderHardwareStrand(hangLeft, attachLeft, hardwareType, cardStyle.accentColor),
2089        renderHardwareStrand(hangRight, attachRight, hardwareType, cardStyle.accentColor),
2090        /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2091          /* @__PURE__ */ jsxRuntimeExports.jsx(
2092            "rect",
2093            {
2094              x: bannerX,
2095              y: currentBannerY + 5,
2096              width: bannerWidth,
2097              height: bannerHeight,
2098              rx: cardStyle.rx,
2099              fill: "rgba(0, 0, 0, 0.4)"
2100            }
2101          ),
2102          /* @__PURE__ */ jsxRuntimeExports.jsx(
2103            "rect",
2104            {
2105              x: bannerX,
2106              y: currentBannerY,
2107              width: bannerWidth,
2108              height: bannerHeight,
2109              rx: cardStyle.rx,
2110              fill: cardStyle.fill,
2111              stroke: cardStyle.stroke,
2112              strokeWidth: cardStyle.strokeWidth,
2113              opacity: 0.96
2114            }
2115          ),
2116          /* @__PURE__ */ jsxRuntimeExports.jsx(
2117            "rect",
2118            {
2119              x: bannerX + 5,
2120              y: currentBannerY + 5,
2121              width: bannerWidth - 10,
2122              height: bannerHeight - 10,
2123              rx: Math.max(2, cardStyle.rx - 4),
2124              fill: "none",
2125              stroke: cardStyle.stroke,
2126              strokeWidth: 1,
2127              strokeDasharray: themeKey === "folk" || themeKey === "country" ? "4 3" : void 0,
2128              opacity: 0.4
2129            }
2130          ),
2131          themeKey === "occasions" && // Broadway Marquee Lights around the perimeter
2132          /* @__PURE__ */ jsxRuntimeExports.jsx("g", { children: [...Array(16)].map((_, i) => {
2133            const bulbX = bannerX + 16 + i * ((bannerWidth - 32) / 15);
2134            return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2135              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + 7, r: 2.5, fill: "#fef08a" }),
2136              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + 7, r: 4.5, fill: "#fef08a", opacity: 0.3 }),
2137              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + bannerHeight - 7, r: 2.5, fill: "#fef08a" }),
2138              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + bannerHeight - 7, r: 4.5, fill: "#fef08a", opacity: 0.3 })
2139            ] }, `bulb-top-${i}`);
2140          }) }),
2141          themeKey === "anime" && // Cute pastel decorative stars on corners
2142          /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { fill: "#38bdf8", children: [
2143            /* @__P
2143URE__ */ jsxRuntimeExports.jsx("path", { d: `M ${bannerX + 18} ${currentBannerY + 16} l 2 4 l 4 1 l -3 3 l 1 4 l -4 -2 l -4 2 l 1 -4 l -3 -3 l 4 -1 Z` }),
2144            /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: `M ${bannerX + bannerWidth - 18} ${currentBannerY + 16} l 2 4 l 4 1 l -3 3 l 1 4 l -4 -2 l -4 2 l 1 -4 l -3 -3 l 4 -1 Z` })
2145          ] }),
2146          themeKey === "trap" && // Yellow/Black Caution Hazard Stripes along edges
2147          /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2148            /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: bannerX + 4, y: currentBannerY + 4, width: bannerWidth - 8, height: 5, fill: "#eab308" }),
2149            /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: bannerX + 4, y: currentBannerY + bannerHeight - 9, width: bannerWidth - 8, height: 5, fill: "#eab308" })
2150          ] }),
2151          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + 12, cy: currentBannerY + 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }),
2152          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + bannerWidth - 12, cy: currentBannerY + 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }),
2153          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + 12, cy: currentBannerY + bannerHeight - 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }),
2154          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + bannerWidth - 12, cy: currentBannerY + bannerHeight - 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }),
2155          /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { id: "hamienet-banner-crest", children: [
2156            /* @__PURE__ */ jsxRuntimeExports.jsx(
2157              "circle",
2158              {
2159                cx: centerX,
2160                cy: currentBannerY - 14,
2161                r: 18,
2162                fill: cardStyle.fill,
2163                stroke: cardStyle.stroke,
2164                strokeWidth: 2
2165              }
2166            ),
2167            /* @__PURE__ */ jsxRuntimeExports.jsx(
2168              "circle",
2169              {
2170                cx: centerX,
2171                cy: currentBannerY - 14,
2172                r: 15.5,
2173                fill: "none",
2174                stroke: cardStyle.stroke,
2175                strokeWidth: 0.75,
2176                strokeDasharray: "2 1.5",
2177                opacity: 0.8
2178              }
2179            ),
2180            /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(${centerX - 13.5}, ${currentBannerY - 26}) scale(1.15)`, children: [
2181              /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4.5 14C4.5 12.067 6.067 10.5 8 10.5H16C17.933 10.5 19.5 12.067 19.5 14V17.5C19.5 18.8807 18.3807 20 17 20H7C5.61929 20 4.5 18.8807 4.5 17.5V14Z", fill: "#EAD8C1" }),
2182              /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M8 10.5C8 8.01472 10.0147 6 12.5 6H11.5C9.01472 6 7 8.01472 7 10.5C7 11.3284 7.67157 12 8.5 12H8C7.44772 12 7 11.5523 7 11V10.5C7 9.11929 8.11929 8 9.5 8H14.5C15.8807 8 17 9.11929 17 10.5V11C17 11.5523 16.5523 12 16 12H15.5C16.3284 12 17 11.3284 17 10.5C17 8.01472 14.9853 6 12.5 6H11.5C9.01472 6 7 8.01472 7 10.5", fill: "#D1B897" }),
2183              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "9.5", cy: "14.5", r: "1.1", fill: "black" }),
2184              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "14.5", cy: "14.5", r: "1.1", fill: "black" }),
2185              /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 16.5C12.5 17.5 11.5 17.5 12 16.5Z", stroke: "black", strokeWidth: "0.5", strokeLinecap: "round" }),
2186              /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M2 11L12 7L22 11L12 15L2 11Z", fill: "#1E293B" }),
2187              /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "11", y: "4", width: "2", height: "3", rx: "0.5", fill: "#1E293B" }),
2188              /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "21", y1: "11", x2: "23", y2: "11", stroke: "#FBBF24", strokeWidth: "1.5", strokeLinecap: "round" })
2189            ] }),
2190            /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2191              /* @__PURE__ */ jsxRuntimeExports.jsx(
2192                "rect",
2193                {
2194                  x: centerX - (cardStyle.badgeText ? 82 : 55),
2195                  y: currentBannerY - 1,
2196                  width: cardStyle.badgeText ? 164 : 110,
2197                  height: 17,
2198                  rx: 4,
2199                  fill: cardStyle.fill,
2200                  stroke: cardStyle.stroke,
2201                  strokeWidth: 1.5
2202                }
2203              ),
2204              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: centerX - (cardStyle.badgeText ? 75 : 48), cy: currentBannerY + 7.5, r: 1.5, fill: cardStyle.stroke }),
2205              /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: centerX + (cardStyle.badgeText ? 75 : 48), cy: currentBannerY + 7.5, r: 1.5, fill: cardStyle.stroke }),
2206              /* @__PURE__ */ jsxRuntimeExports.jsx(
2207                "text",
2208                {
2209                  x: centerX,
2210                  y: currentBannerY + 11,
2211                  textAnchor: "middle",
2212                  fill: cardStyle.accentColor || cardStyle.stroke,
2213                  fontSize: "8.5",
2214                  fontWeight: "900",
2215                  letterSpacing: "1.2",
2216                  fontFamily: "system-ui, sans-serif",
2217                  children: cardStyle.badgeText ? `HAMIENET • ${cardStyle.badgeText}` : "HAMIENET"
2218                }
2219              )
2220            ] })
2221          ] }),
2222          cleanArtist ? (
2223            // Song Title + Artist Layout
2224            /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2225              /* @__PURE__ */ jsxRuntimeExports.jsx(
2226                "text",
2227                {
2228                  x: centerX,
2229                  y: currentBannerY + bannerHeight * 0.44,
2230                  textAnchor: "middle",
2231                  dominantBaseline: "central",
2232                  fill: cardStyle.titleColor,
2233                  fontSize: titleFontSize,
2234                  fontWeight: "800",
2235                  letterSpacing: themeKey === "techno" ? "1.5" : "0.5",
2236                  fontFamily: cardStyle.fontFamily,
2237                  style: { textShadow: "0 2px 4px rgba(0,0,0,0.5)" },
2238                  children: displayTitle
2239                }
2240              ),
2241              /* @__PURE__ */ jsxRuntimeExports.jsx(
2242                "line",
2243                {
2244                  x1: centerX - 80,
2245                  y1: currentBannerY + bannerHeight * 0.63,
2246                  x2: centerX + 80,
2247                  y2: currentBannerY + bannerHeight * 0.63,
2248                  stroke: cardStyle.stroke,
2249                  strokeWidth: 1,
2250                  opacity: 0.5
2251                }
2252              ),
2253              /* @__PURE__ */ jsxRuntimeExports.jsx(
2254                "text",
2255                {
2256                  x: centerX,
2257                  y: currentBannerY + bannerHeight * 0.79,
2258                  textAnchor: "middle",
2259                  dominantBaseline: "central",
2260                  fill: cardStyle.artistColor,
2261                  fontSize: artistFontSize,
2262                  fontWeight: "600",
2263                  letterSpacing: "0.5",
2264                  fontFamily: cardStyle.fontFamily,
2265                  opacity: 0.9,
2266                  style: { textShadow: "0 1px 3px rgba(0,0,0,0.4)" },
2267                  children: displayArtist
2268                }
2269              )
2270            ] })
2271          ) : (
2272            // Song Title Only (centered vertically when no artist is available)
2273            /* @__PURE__ */ jsxRuntimeExports.jsx("g", { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2274              "text",
2275              {
2276                x: centerX,
2277                y: currentBannerY + bannerHeight * 0.54,
2278                textAnchor: "middle",
2279                dominantBaseline: "central",
2280                fill: cardStyle.titleColor,
2281                fontSize: titleFontSize * 1.15,
2282                fontWeight: "800",
2283                letterSpacing: themeKey === "techno" ? "1.5" : "0.5",
2284                fontFamily: cardStyle.fontFamily,
2285                style: { textShadow: "0 2px 5px rgba(0,0,0,0.6)" },
2286                children: displayTitle
2287              }
2288            ) })
2289          )
2290        ] })
2291      ]
2292    }
2293  );
2294};
2295const SouthparkStageBanner = React.memo(SouthparkStageBannerComponent);
2296export {
2297  SouthparkStageBanner as S,
2298  SouthparkStageLighting as a,
2299  calculateEnsemblePlacement as c,
2300  defaultSouthparkStageConfig as d,
2301  getSouthparkStageAlignment as g,
2302  resolveLightingProfile as r,
2303  useSouthparkStageBackground as u
2304};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.