export function createBackground(ctx) {
  const { THREE, Reflector, scene, opts, isMobile } = ctx;
  const currentBackgroundType = opts.backgroundType || 'grid';
  const bgColor = opts.backgroundColor || '#f8f9fa';
  const gradientFloorColor = opts.gradientFloorColor || '#c0c1c2';
  const gridLineColor = opts.gridLineColor || '#dcdcdc';
  /* 'gridReflective' = Raster-Kulisse (Boden-Linien + Wände + 2D-Screen-Grid) PLUS
   * Reflector-Render-Pass des 'gradient'-Modus. Boden-Raster wird transparent gefüllt,
   * damit die Spiegelung durch die leeren Zellen scheint (nur Linien bleiben sichtbar). */
  const isReflectiveGrid = currentBackgroundType === 'gridReflective';
  const isGridLike = currentBackgroundType === 'grid' || isReflectiveGrid;
  /* Mobile: Reflector weglassen — ruckelt sonst auf schwachen Devices.
   * 'gridReflective' fällt auf Mobile auf das normale Raster zurück (opake Zellfüllung,
   * keine Spiegelung); 'gradient'-Fallback weiter unten: floorOverlay opacity 0.6. */
  const needsReflector = (currentBackgroundType === 'gradient' || isReflectiveGrid) && !isMobile;

  const hexToRgba = (hex, a) => {
    let s = String(hex || '').replace(/^#/, '').trim();
    if (s.length === 3) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2];
    const m = s.match(/^([0-9a-fA-F]{6})$/);
    if (!m) return `rgba(248,249,250,${a})`;
    const r = parseInt(m[1].slice(0, 2), 16);
    const g = parseInt(m[1].slice(2, 4), 16);
    const b = parseInt(m[1].slice(4, 6), 16);
    return `rgba(${r},${g},${b},${a})`;
  };
  const toStrokeStyle = (hex, a) => (a === 1 ? (hex && /^#?[0-9a-fA-F]{3,6}$/.test(String(hex).trim()) ? (String(hex).trim().startsWith('#') ? hex : '#' + hex) : hexToRgba(hex, 1)) : hexToRgba(hex, a));

  const gSize = isMobile ? 256 : 512;
  const gCanvas = document.createElement('canvas');
  gCanvas.width = gSize; gCanvas.height = gSize;
  const gCtx = gCanvas.getContext('2d');
  const grad = gCtx.createLinearGradient(0, gSize, 0, 0);
  grad.addColorStop(0, hexToRgba(bgColor, 1));
  grad.addColorStop(1, hexToRgba(bgColor, 0));
  gCtx.fillStyle = grad;
  gCtx.fillRect(0, 0, gSize, gSize);
  const gradientTex = new THREE.CanvasTexture(gCanvas);
  gradientTex.wrapS = gradientTex.wrapT = THREE.ClampToEdgeWrapping;

  const gridSize = 128;
  const gridFillColor = opts.backgroundTransparent ? 'rgba(0,0,0,0)' : (bgColor || '#f8f9fa');
  /* gridReflective: Boden-Zellen transparent füllen, damit der Reflector durchscheint.
   * Ohne aktiven Reflector (Mobile) bleibt die normale opake Füllung → Look wie 'grid'. */
  const floorGridFillColor = (isReflectiveGrid && needsReflector) ? 'rgba(0,0,0,0)' : gridFillColor;
  const WALL_LINE_OPACITY = 0.42;

  function drawGridCanvas(size, fillColor, lineColor, opacity) {
    const canvas = document.createElement('canvas');
    canvas.width = canvas.height = size;
    const c = canvas.getContext('2d');
    c.fillStyle = fillColor;
    c.fillRect(0, 0, size, size);
    const stroke = toStrokeStyle(lineColor, opacity);
    c.strokeStyle = stroke; c.lineWidth = 1;
    for (let i = 0; i <= size; i += 8) {
      c.beginPath(); c.moveTo(i, 0); c.lineTo(i, size); c.stroke();
      c.beginPath(); c.moveTo(0, i); c.lineTo(size, i); c.stroke();
    }
    c.strokeStyle = stroke; c.lineWidth = 1.5;
    for (let i = 0; i <= size; i += 32) {
      c.beginPath(); c.moveTo(i, 0); c.lineTo(i, size); c.stroke();
      c.beginPath(); c.moveTo(0, i); c.lineTo(size, i); c.stroke();
    }
    return canvas;
  }

  let gridTex = null;
  let wallGridTex = null;
  if (isGridLike) {
    gridTex = new THREE.CanvasTexture(drawGridCanvas(gridSize, floorGridFillColor, gridLineColor, 1));
    gridTex.wrapS = gridTex.wrapT = THREE.RepeatWrapping;
    gridTex.colorSpace = THREE.SRGBColorSpace;
    wallGridTex = new THREE.CanvasTexture(drawGridCanvas(gridSize, gridFillColor, gridLineColor, WALL_LINE_OPACITY));
    wallGridTex.wrapS = wallGridTex.wrapT = THREE.RepeatWrapping;
    wallGridTex.colorSpace = THREE.SRGBColorSpace;
  }

  function ensureGridTextures() {
    if (!gridTex) {
      gridTex = new THREE.CanvasTexture(drawGridCanvas(gridSize, floorGridFillColor, gridLineColor, 1));
      gridTex.wrapS = gridTex.wrapT = THREE.RepeatWrapping;
      gridTex.colorSpace = THREE.SRGBColorSpace;
    }
    if (!wallGridTex) {
      wallGridTex = new THREE.CanvasTexture(drawGridCanvas(gridSize, gridFillColor, gridLineColor, WALL_LINE_OPACITY));
      wallGridTex.wrapS = wallGridTex.wrapT = THREE.RepeatWrapping;
      wallGridTex.colorSpace = THREE.SRGBColorSpace;
    }
  }

  /* Einheitliche Grid-Repeat-Werte für Boden und Wände — die Szene soll in 2D wie in
   * 3D identisch aussehen (keine feineren 2D-spezifischen Repeats mehr). */
  const FLOOR_GRID_REPEAT = 32;
  const WALL_GRID_REPEAT = 32;

  const floorGeo = new THREE.PlaneGeometry(48, 48);

  let floorFadeTex = null;
  let reflector = null;
  if (needsReflector) {
    const floorFadeSize = 128;
    const floorFadeCanvas = document.createElement('canvas');
    floorFadeCanvas.width = floorFadeCanvas.height = floorFadeSize;
    const floorFadeCtx = floorFadeCanvas.getContext('2d');
    const floorFadeGrad = floorFadeCtx.createRadialGradient(floorFadeSize / 2, floorFadeSize / 2, 0, floorFadeSize / 2, floorFadeSize / 2, floorFadeSize / 2);
    floorFadeGrad.addColorStop(0, hexToRgba(gradientFloorColor, 0.75));
    floorFadeGrad.addColorStop(0.4, hexToRgba(gradientFloorColor, 0.25));
    floorFadeGrad.addColorStop(1, hexToRgba(gradientFloorColor, 0));
    floorFadeCtx.fillStyle = floorFadeGrad;
    floorFadeCtx.fillRect(0, 0, floorFadeSize, floorFadeSize);
    floorFadeTex = new THREE.CanvasTexture(floorFadeCanvas);
    floorFadeTex.wrapS = floorFadeTex.wrapT = THREE.ClampToEdgeWrapping;

    const reflectorColorSrc = opts.backgroundTransparent ? gradientFloorColor : bgColor;
    const reflectorColor = (() => {
      const m = String(reflectorColorSrc).replace(/^#/, '').match(/^([0-9a-fA-F]{6})$/);
      return m ? parseInt(m[1], 16) : 0xf8f9fa;
    })();
    const reflectorRes = 2048;
    const reflectorBlurShader = {
      name: 'ReflectorBlurShader',
      uniforms: Object.assign({}, Reflector.ReflectorShader.uniforms, {
        resolution: { value: new THREE.Vector2(reflectorRes, reflectorRes) },
        tFade: { value: floorFadeTex },
      }),
      vertexShader: `
        uniform mat4 textureMatrix;
        varying vec4 vUv;
        varying float vDistFrac;
        varying vec2 vFadeUv;
        #include <common>
        #include <logdepthbuf_pars_vertex>
        void main() {
          vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
          vUv = textureMatrix * vec4(position, 1.0);
          float ndcY = clipPos.y / clipPos.w;
          float viewDepth = clamp(-mvPos.z / 18.0, 0.0, 1.0);
          float screenFade = clamp(ndcY * 0.5 + 0.5, 0.0, 1.0);
          vDistFrac = min(screenFade, viewDepth);
          vFadeUv = (position.xy / 16.0 + 1.0) * 0.5;
          gl_Position = clipPos;
          #include <logdepthbuf_vertex>
        }`,
      fragmentShader: `
        uniform vec3 color;
        uniform sampler2D tDiffuse;
        uniform sampler2D tFade;
        uniform vec2 resolution;
        varying vec4 vUv;
        varying float vDistFrac;
        varying vec2 vFadeUv;
        #include <common>
        #include <fog_pars_fragment>
        #include <logdepthbuf_pars_fragment>
        #include <clipping_planes_pars_fragment>
        float blendOverlay(float base, float blend) {
          return (base < 0.5 ? (2.0 * base * blend) : (1.0 - 2.0 * (1.0 - base) * (1.0 - blend)));
        }
        vec3 blendOverlay(vec3 base, vec3 blend) {
          return vec3(blendOverlay(base.r, blend.r), blendOverlay(base.g, blend.g), blendOverlay(base.b, blend.b));
        }
        void main() {
          #include <logdepthbuf_fragment>
          #include <clipping_planes_fragment>
          vec2 uv = vUv.xy / vUv.w;
          vec2 px = 1.5 / resolution;
          vec4 c = texture2D(tDiffuse, uv) * 0.4
                 + texture2D(tDiffuse, uv + vec2(px.x, 0.0)) * 0.15
                 + texture2D(tDiffuse, uv - vec2(px.x, 0.0)) * 0.15
                 + texture2D(tDiffuse, uv + vec2(0.0, px.y)) * 0.15
                 + texture2D(tDiffuse, uv - vec2(0.0, px.y)) * 0.15;
          vec3 refl = blendOverlay(c.rgb, color);
          refl = mix(refl, color, 0.52);
          float fadeRamp = smoothstep(-0.4, 0.85, vDistFrac);
          vec3 final = mix(refl, color, fadeRamp);
          vec4 fade = texture2D(tFade, vFadeUv);
          gl_FragColor = vec4(mix(fade.rgb, final, fade.a), fade.a);
          #include <fog_fragment>
          #include <premultiplied_alpha_fragment>
          #include <dithering_fragment>
        }`,
    };
    reflector = new Reflector(floorGeo, {
      color: reflectorColor, textureWidth: reflectorRes, textureHeight: reflectorRes,
      clipBias: 0.001, shader: reflectorBlurShader,
    });
    reflector.rotation.x = -Math.PI / 2;
    reflector.position.y = -0.001;
    /* gridReflective: Reflector eine Stufe tiefer (-3), damit die Raster-Boden-Linien
     * (renderOrder -2) in der Transparent-Queue darüber gezeichnet werden. */
    reflector.renderOrder = isReflectiveGrid ? -3 : -2;
    /* r16x+: Reflector nutzt `_getReflectionCamera(camera)` statt `reflector.camera`. */
    if (reflector.camera && reflector.camera.layers) {
      reflector.camera.layers.set(0);
    }
    reflector.material.transparent = true;
    reflector.material.depthWrite = false;
    scene.add(reflector);
  }

  const bgTex = currentBackgroundType === 'gradient' ? gradientTex : gridTex;
  const isGridInit = isGridLike;
  const floorOverlayMat = new THREE.MeshBasicMaterial({
    map: bgTex,
    transparent: (isReflectiveGrid && reflector) ? true : (isGridInit ? opts.backgroundTransparent : true),
    opacity: isGridInit ? 1 : (reflector ? 0 : 0.6), depthWrite: false, side: THREE.DoubleSide,
  });
  const floorOverlay = new THREE.Mesh(floorGeo.clone(), floorOverlayMat);
  floorOverlay.rotation.x = -Math.PI / 2;
  floorOverlay.position.y = isGridInit ? 0.002 : 0.0005;
  /* Grid-Floor auf -2 (wie die Wände), damit er in der Transparent-Queue VOR den
   * Ruler-Labels (renderOrder -1) gerendert wird. Sonst überdeckt der halb-
   * transparente weiße Floor-Fill während des 2D-Morph den Y-Title-Label-Plane
   * (Grid + backgroundTransparent=false) — POP-OUT + Fade-In Artefakt. */
  floorOverlay.renderOrder = isGridInit ? -2 : -1;
  scene.add(floorOverlay);

  const wallGeo = new THREE.PlaneGeometry(48, 56);
  /* FrontSide statt DoubleSide: PlaneGeometry-Front-Face zeigt per Konstruktion in die
   * lokale +z-Richtung, durch die rotation.y-Werte unten zeigt das bei jeder Wand zur
   * Chart-Mitte (Innennormale). Face-Culling der GPU verwirft Back-Faces automatisch —
   * sobald die Kamera auf die Außenseite einer Wand orbitet (Wand stünde zwischen
   * Kamera und Chart), wird sie gratis unsichtbar. Null Per-Frame-Kosten. */
  const wallMat = new THREE.MeshBasicMaterial({ map: wallGridTex || gradientTex, transparent: isGridInit ? opts.backgroundTransparent : true, side: THREE.FrontSide });
  const walls = [];
  const WALL_BOTTOM_Y = 0, FLOOR_EDGE = 24;
  /* rotation.y so gewählt, dass die Front-Normale jeder Wand (PlaneGeometry +z local)
   * zur Chart-Mitte zeigt. Kombiniert mit side: THREE.FrontSide wird die Wand per
   * GPU-Face-Culling automatisch unsichtbar, sobald die Kamera auf die Außenseite
   * orbitet (Wand würde sonst Chart verdecken).
   *   Back  (z=-24): ry=0     → Front = +z  → Richtung Chart ✓
   *   Front (z=+24): ry=π     → Front = -z  → Richtung Chart ✓
   *   Left  (x=-24): ry=+π/2  → Front = +x  → Richtung Chart ✓  (vorher -π/2 = falsch)
   *   Right (x=+24): ry=-π/2  → Front = -x  → Richtung Chart ✓  (vorher +π/2 = falsch) */
  [[0, WALL_BOTTOM_Y + 28, -FLOOR_EDGE, 0], [0, WALL_BOTTOM_Y + 28, FLOOR_EDGE, Math.PI], [-FLOOR_EDGE, WALL_BOTTOM_Y + 28, 0, Math.PI / 2], [FLOOR_EDGE, WALL_BOTTOM_Y + 28, 0, -Math.PI / 2]].forEach(([x, y, z, ry]) => {
    const wall = new THREE.Mesh(wallGeo, wallMat.clone());
    wall.position.set(x, y, z);
    wall.rotation.y = ry;
    wall.renderOrder = -2;
    scene.add(wall);
    walls.push(wall);
  });

  /* ───────────────────── Screen-Space-Grid-Hintergrund (2D-Modus, nur Grid) ─────────────────────
   * Bei `backgroundType==='grid'` bauen wir einen kamera-agnostischen Screen-BG auf:
   * eigene Scene + OrthographicCamera + Fullscreen-Quad. Während des 2D/3D-Projection-
   * Tweens wird die 3D-Kulisse (floor, walls, floorOverlay) gegen diesen flachen
   * Millimeterpapier-Hintergrund gecrossfaded (siehe setGridMorph). In 3D ist der
   * Screen-BG vollständig unsichtbar (visible=false), die räumliche Grid-Box bleibt.
   * Zell-Dichte wird bei Resize über das Canvas-Aspect geführt, damit die Zellen
   * quadratisch bleiben (echtes Millimeterpapier, keine Verzerrung). */
  let bgScene = null, bgCamera = null, bgQuad = null, bgMaterial = null, screenGridTex = null;
  /* Original-scene.background (die vom User gewählte Block-Farbe) wird beim Init gespiegelt.
   * Solange der Screen-Grid-BG aktiv ist (setGridMorph > 0), muss scene.background auf null,
   * weil three.js diese Color intern in jedem `renderer.render(scene, camera)` als Fullscreen-
   * Fill **vor** die Geometrie zeichnet — das würde den im ersten Pass gerenderten bgQuad
   * komplett überschreiben. Stattdessen übernimmt `bgScene.background` die Farbe; der
   * Grid-Quad mit Transparenz kommt darüber. Bei tc===0 (voll 3D) wird der Originalwert
   * wiederhergestellt, damit die 3D-Kulisse auf dem gewohnten Farbhintergrund steht. */
  let _originalSceneBg = null;
  const SCREEN_GRID_CELLS_V = 12;
  if (isGridLike) {
    screenGridTex = new THREE.CanvasTexture(drawGridCanvas(gridSize, gridFillColor, gridLineColor, 1));
    screenGridTex.wrapS = screenGridTex.wrapT = THREE.RepeatWrapping;
    screenGridTex.colorSpace = THREE.SRGBColorSpace;
    bgScene = new THREE.Scene();
    /* Die Block-Farbe der Hauptszene auch in bgScene spiegeln, damit beim Crossfade
     * die Farbfläche hinter dem transparenten Grid-Quad nicht fehlt. `scene.background`
     * kann ein THREE.Color oder null sein (bei backgroundTransparent=true). */
    _originalSceneBg = scene.background || null;
    bgScene.background = (scene.background && scene.background.isColor)
      ? scene.background.clone()
      : null;
    bgCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
    bgMaterial = new THREE.MeshBasicMaterial({
      map: screenGridTex,
      transparent: true,
      opacity: 0,
      depthTest: false,
      depthWrite: false,
    });
    bgQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), bgMaterial);
    bgQuad.frustumCulled = false;
    bgQuad.visible = false;
    bgScene.add(bgQuad);
  }

  /** Passt die Zell-Wiederholung des Screen-BG an das aktuelle Canvas-Aspect an, sodass
   *  die Zellen immer quadratisch bleiben. Wird bei Resize aufgerufen. */
  function handleResize(width, height) {
    if (!screenGridTex) return;
    const aspect = (width > 0 && height > 0) ? (width / height) : 1;
    screenGridTex.repeat.set(SCREEN_GRID_CELLS_V * aspect, SCREEN_GRID_CELLS_V);
    screenGridTex.needsUpdate = true;
  }

  /** Crossfade zwischen 3D-Kulisse (t=0) und Screen-Space-Grid (t=1).
   *  Aufgerufen vom Projection-Tween (per-Frame) und einmalig im Initial-/Resume-Pfad
   *  mit t=1, wenn der Block direkt im 2D-Zustand startet. Im Gradient-/Reflector-Modus
   *  No-Op (keine Screen-BG-Infrastruktur angelegt). */
  function setGridMorph(t) {
    if (!bgQuad) return;
    const tc = Math.max(0, Math.min(1, t));
    /* BG-Quad */
    bgMaterial.opacity = tc;
    bgQuad.visible = tc > 0;
    /* scene.background togglen: während der Screen-Grid sichtbar ist (tc>0) muss der
     * Fullscreen-Fill der Haupt-Scene ausgeschaltet sein, sonst überzeichnet er den
     * im ersten Pass gerenderten bgQuad. In vollem 3D (tc===0) wird der Originalwert
     * wiederhergestellt. */
    if (tc > 0) {
      if (scene.background !== null) scene.background = null;
    } else if (scene.background !== _originalSceneBg) {
      scene.background = _originalSceneBg;
    }
    /* 3D-Kulisse: Boden-Overlay + alle 4 Wände. Bei tc===1 komplett visible=false,
     * um den Depth/Transparenz-Overhead zu sparen; dazwischen via opacity faden. */
    const cul3d = 1 - tc;
    const floor3dVisible = cul3d > 0;
    /* Reflector NICHT ausblenden: er wird — exakt wie im 'gradient'-Modus — projektions-
     * agnostisch gerendert und bleibt auch im 2D-Zustand sichtbar (liegt dort nur flach in
     * der Bodenebene, praktisch unsichtbar). So verhält sich 'gridReflective' beim 2D-Wechsel
     * identisch zum originalen Reflektierenden Boden. */
    floorOverlay.visible = floor3dVisible && floorOverlay.userData._baseVisible !== false;
    if (floor3dVisible) {
      /* Voller 3D-Endzustand (tc===0): Material exakt wie nach applyBackgroundTextures.
       * Sonst bleibt nach dem ersten Toggle erzwungenes transparent=true stehen — der Boden
       * landet dauerhaft in der Transparent-Queue und kann Ruler-/Y-Titel-Planes falsch
       * überblenden (Fade beim 2D/3D-Wechsel, „Popout“ beim ersten Morph). */
      if (tc <= 0) {
        floorOverlay.material.transparent = floorOverlay.userData._baseTransparent;
        floorOverlay.material.opacity = floorOverlay.userData._baseOpacity ?? 1;
      } else {
        floorOverlay.material.transparent = true;
        floorOverlay.material.opacity = cul3d * (floorOverlay.userData._baseOpacity ?? 1);
      }
      floorOverlay.material.needsUpdate = true;
    }
    walls.forEach((w) => {
      w.visible = floor3dVisible && w.userData._baseVisible !== false;
      if (!floor3dVisible) return;
      if (tc <= 0) {
        w.material.transparent = w.userData._baseTransparent;
        w.material.opacity = w.userData._baseWallOpacity ?? 1;
      } else {
        w.material.transparent = true;
        w.material.opacity = cul3d;
      }
      w.material.needsUpdate = true;
    });
  }

  /* Die Szenen-Kulisse (Boden, alle 4 Wände, Reflector) wird projection-agnostisch
   * gerendert — gleiches Aussehen in 2D (Pseudo-Ortho) und 3D, damit Init-Aussehen
   * bei defaultProjection='2d' und bei defaultProjection='3d' identisch ist und der
   * Toggle keine Kulisse-Unterschiede zeigt. Der Reflector ist im 2D-Modus praktisch
   * unsichtbar (frontale Pose, Spiegel liegt in der Boden-Ebene), kostet aber einen
   * zweiten Render-Pass — bewusst akzeptiert für visuelle Konsistenz.
   *
   * Base-Zustand (pre-Morph) von visibility & opacity wird in userData gespiegelt,
   * damit setGridMorph(0) die ursprünglichen Werte exakt wiederherstellen kann. */
  function _captureBase() {
    floorOverlay.userData._baseVisible = floorOverlay.visible;
    floorOverlay.userData._baseOpacity = floorOverlay.material.opacity;
    floorOverlay.userData._baseTransparent = floorOverlay.material.transparent;
    walls.forEach((w) => {
      w.userData._baseVisible = w.visible;
      w.userData._baseTransparent = w.material.transparent;
      w.userData._baseWallOpacity = w.material.opacity;
    });
  }
  function applyBackgroundTextures(currentBgType) {
    if (currentBgType === 'off') {
      if (reflector) reflector.visible = false;
      floorOverlay.visible = false;
      walls.forEach((w) => { w.visible = false; });
      return;
    }
    const isReflGrid = currentBgType === 'gridReflective';
    const gridLike = currentBgType === 'grid' || isReflGrid;
    if (gridLike) ensureGridTextures();
    const wallTexSrc = gridLike ? wallGridTex : gradientTex;
    /* Reflector sichtbar bei 'gradient' und 'gridReflective' (falls überhaupt angelegt). */
    if (reflector) reflector.visible = (currentBgType === 'gradient') || isReflGrid;
    floorOverlay.visible = true;
    floorOverlay.material.map = gridLike ? gridTex : gradientTex;
    /* gridReflective: Boden-Overlay transparent, damit die Spiegelung durch die leeren
     * Rasterzellen scheint (nur die Linien bleiben sichtbar). */
    floorOverlay.material.transparent = (isReflGrid && reflector) ? true : (gridLike ? opts.backgroundTransparent : true);
    floorOverlay.material.opacity = gridLike ? 1 : (reflector ? 0 : 0.6);
    floorOverlay.position.y = gridLike ? 0.002 : 0.0005;
    floorOverlay.renderOrder = gridLike ? -2 : -1;
    if (gridLike) {
      gridTex.repeat.set(FLOOR_GRID_REPEAT, FLOOR_GRID_REPEAT);
    }
    if (gridLike && wallTexSrc) {
      wallTexSrc.repeat.set(WALL_GRID_REPEAT, WALL_GRID_REPEAT);
    }
    walls.forEach((w) => {
      w.material.map = wallTexSrc;
      w.material.transparent = gridLike ? opts.backgroundTransparent : true;
      w.visible = gridLike;
    });
  }
  applyBackgroundTextures(currentBackgroundType);
  _captureBase();

  return {
    reflector, floorOverlay, walls, applyBackgroundTextures,
    gradientTex, gridTex, wallGridTex,
    bgScene, bgCamera, setGridMorph, handleResize,
    hasScreenGrid: !!bgQuad,
    isScreenGridActive: () => !!(bgQuad && bgQuad.visible),
  };
}
