diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,11 @@
 # Changelog for keid-render-basic
 
+## 0.1.10.0
+
+- Update to follow the changes in keid-core-0.1.11 and geomancy-0.3.
+- Fix lit pipelines by removing spurious sign flips.
+- Require LitColored to participate in the depth pre-pass.
+
 ## 0.1.9.0
 
 - Moved to geomancy-layout.
diff --git a/keid-render-basic.cabal b/keid-render-basic.cabal
--- a/keid-render-basic.cabal
+++ b/keid-render-basic.cabal
@@ -1,17 +1,17 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.35.2.
+-- This file has been generated from package.yaml by hpack version 0.38.1.
 --
 -- see: https://github.com/sol/hpack
 
 name:           keid-render-basic
-version:        0.1.9.0
+version:        0.1.10.0
 synopsis:       Basic rendering programs for Keid engine.
 category:       Game Engine
 homepage:       https://keid.haskell-game.dev
 author:         IC Rainbow
 maintainer:     keid@aenor.ru
-copyright:      2023 IC Rainbow
+copyright:      2025 IC Rainbow
 license:        BSD3
 license-file:   LICENSE
 build-type:     Simple
@@ -132,10 +132,10 @@
     , derive-storable
     , derive-storable-plugin
     , file-embed >=0.0.10
-    , geomancy
+    , geomancy >=0.3.0.0
     , geomancy-layout
     , gl-block
-    , keid-core >=0.1.9.0
+    , keid-core >=0.1.11.0
     , keid-geometry
     , neat-interpolation
     , resourcet
diff --git a/src/Render/Code/Lit.hs b/src/Render/Code/Lit.hs
--- a/src/Render/Code/Lit.hs
+++ b/src/Render/Code/Lit.hs
@@ -127,10 +127,10 @@
       baseColor.rgb;
     vec3 F0 = mix(vec3(0.04), albedo, metallic);
 
-    vec3 ray = scene.viewPosition.xyz - fPosition.xyz;
-    vec3 rayDir = normalize(ray); // V
-    float quadrance = dot(ray, ray);
-    float distance = sqrt(quadrance);
+    vec3 rayDir = normalize(scene.viewPosition.xyz - fPosition.xyz); // V
+    // vec3 rayDir = normalize(ray); // V
+    // float quadrance = dot(ray, ray);
+    // float viewDistance = sqrt(quadrance); // XXX: not used for directional lights
 
     // XXX: provided by caller
     // vec3 normal = normalize(fNormal); // N
@@ -138,7 +138,7 @@
     vec3 Lo = vec3(0.0);
     for (int l = 0; l < scene.numLights; l++) {
       // XXX: directional lights' hit angle doesn't depend on fragment position
-      vec3 lightDir = normalize(lights[l].direction.xyz); // L
+      vec3 lightDir = lights[l].direction.xyz; // L, pre-normalized
 
       float shade = 1.0; // XXX: 0 - occluded, 1 - lit
       if (lights[l].shadow.z > -1) {
@@ -147,12 +147,13 @@
 
         // TODO: pick on specialization constant
         shade = filterPCF(shadowCoord.xyz, lights[l].shadow.z);
-        // shade = shadow_factor(shadowCoord.xyz, lights[l].shadow.z, vec2(0));
+        // shade = shadow_factor(shadowCoord.xyz, lights[l].shadow.z, vec2(0)); // looks like crap
+        // TODO: VSM shadows
       }
 
       Lo += brdfSpecular(
-        lightDir,
-        ray,
+        -lightDir, // XXX: according to PBR convention
+        rayDir,
         normal,
         F0,
         metallic,
@@ -162,7 +163,8 @@
       ) * shade;
     }
 
-    vec3 reflection = prefilteredReflection(reflect(rayDir, normal), roughness).rgb;
+    vec3 R = reflect(-rayDir, normal);
+    vec3 reflection = prefilteredReflection(R, roughness).rgb;
 
     // IBL
     vec3 irradiance = vec3(0);
@@ -172,7 +174,7 @@
           cubes[nonuniformEXT(scene.envCubeId)],
           samplers[0]
         ),
-        -normal,
+        normal * vec3(1, -1, 1),
         IRRADIANCE_LOD
       ).rgb;
     }
@@ -208,12 +210,13 @@
     vec3 color = Lo + ambient;
 
     // Tone mapping
+    // TODO: make configurable
     color =
       Uncharted2Tonemap(color * 4.5) /
       Uncharted2Tonemap(vec3(11.2)); // White point
 
     // Gamma correction
-    // XXX: not needed, we're in linear already
+    // XXX: not needed, we're in sRGB after tonemap
     // color = pow(color, vec3(1.0/2.2));
 
     // Happily ever after
@@ -257,8 +260,9 @@
 
     vec3 prefilteredReflection(vec3 R, float roughness) {
       vec3 color = vec3(0);
+      vec3 R_cube = R * vec3(1, -1, 1);
 
-      if (scene.envCubeId > -1) {
+      if (scene.envCubeId > -1) { // TODO: wrap the whole call
         float lod = roughness * MAX_REFLECTION_LOD;
         float lodf = floor(lod);
         float lodc = ceil(lod);
@@ -268,7 +272,7 @@
             cubes[nonuniformEXT(scene.envCubeId)],
             samplers[0]
           ),
-          R,
+          R_cube,
           lodf
         ).rgb;
 
@@ -277,7 +281,7 @@
             cubes[nonuniformEXT(scene.envCubeId)],
             samplers[0]
           ),
-          R,
+          R_cube,
           lodc
         ).rgb;
 
@@ -302,6 +306,7 @@
       float dotNH = clamp(dot(N, H), 0.0, 1.0);
       float dotNV = clamp(dot(N, V), 0.0, 1.0);
       float dotNL = clamp(dot(N, L), 0.0, 1.0);
+      float dotVH = clamp(dot(V, H), 0.0, 1.0);
 
       vec3 color = vec3(0.0);
 
@@ -312,7 +317,7 @@
         float rroughness = max(0.05, roughness);
         float G = G_SchlicksmithGGX(dotNL, dotNV, rroughness);
         // F = Fresnel factor (Reflectance depending on angle of incidence)
-        vec3 F = F_Schlick(dotNV, F0);
+        vec3 F = F_Schlick(dotVH, F0);
         vec3 spec = D * F * G / (4.0 * dotNL * dotNV + 0.001);
         vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
         color += (kD * ALBEDO / 3.1415926535897932384626433832795 + spec) * dotNL * lightColor;
diff --git a/src/Render/Debug/Code.hs b/src/Render/Debug/Code.hs
--- a/src/Render/Debug/Code.hs
+++ b/src/Render/Debug/Code.hs
@@ -86,28 +86,28 @@
           break;
 
         case 2:
-          float d0 = 1.0;
+          float d0 = 0.0;
           if ((shadow_mask & 1) == 1)
             d0 = texture(
               shadowmaps,
               vec3(fTexCoord, 0.0)
             ).x;
 
-          float d1 = 1.0;
+          float d1 = 0.0;
           if ((shadow_mask & 2) == 2)
             d1 = texture(
               shadowmaps,
               vec3(fTexCoord, 1.0)
             ).x;
 
-          float d2 = 1.0;
+          float d2 = 0.0;
           if ((shadow_mask & 4) == 4)
             d2 = texture(
               shadowmaps,
               vec3(fTexCoord, 2.0)
             ).x;
 
-          texel = vec4(1-d0, 1-d1, 1-d2, 1.0);
+          texel = vec4(d0, d1, d2, 1.0);
           break;
 
         default:
diff --git a/src/Render/DescSets/Set0.hs b/src/Render/DescSets/Set0.hs
--- a/src/Render/DescSets/Set0.hs
+++ b/src/Render/DescSets/Set0.hs
@@ -258,7 +258,7 @@
       , Vk.magFilter     = shadowFilter
       , Vk.minFilter     = shadowFilter
       , Vk.compareEnable = True
-      , Vk.compareOp     = Vk.COMPARE_OP_LESS
+      , Vk.compareOp     = Vk.COMPARE_OP_GREATER
       }
 
   let ifor = flip Vector.imapM
diff --git a/src/Render/DescSets/Sun.hs b/src/Render/DescSets/Sun.hs
--- a/src/Render/DescSets/Sun.hs
+++ b/src/Render/DescSets/Sun.hs
@@ -33,6 +33,7 @@
 import Geomancy (Vec3, Vec4, vec3, vec4)
 import Geomancy.Transform (Transform)
 import Geomancy.Transform qualified as Transform
+import Geomancy.Quaternion qualified as Quaternion
 import Geomancy.Vec4 qualified as Vec4
 import Vulkan.Core10 qualified as Vk
 import Vulkan.CStruct.Extends (SomeStruct(..))
@@ -173,7 +174,7 @@
   , siRadius      = Camera.PROJECTION_FAR / 2
   , siTarget      = 0
 
-  , siDepthRange = Camera.PROJECTION_FAR
+  , siDepthRange = Camera.PROJECTION_FAR -- TODO: use explicit near/far
   , siSize       = 512
   , siShadowIx   = -1
   }
@@ -192,7 +193,7 @@
 mkSun SunInput{..} =
   ( bbTransform
   , Sun
-      { sunViewProjection = mconcat vp
+      { sunViewProjection = pv
       , sunShadow         = vec4 0 0 siShadowIx siSize
       , sunPosition       = Vec4.fromVec3 position 0
       , sunDirection      = Vec4.fromVec3 direction 0
@@ -200,39 +201,26 @@
       }
   )
   where
-    vp =
-      [ Transform.rotateY (-siAzimuth)
-      , Transform.rotateX (-siInclination)
-
-      , Transform.translate 0 0 siRadius
-
-      -- XXX: some area beyond the near plane receives light, but not shadows
-      , Transform.scale3
-          (1 / siSize)
-          (1 / siSize)
-          (1 / siDepthRange)
-      ]
-
-    position = Transform.apply (vec3 0 0 siRadius) rotation
-
-    direction = Transform.apply (vec3 0 0 $ -1) rotation
+    pv = p <> v
+    p = -- just a scaled ortho box
+      Transform.scale3
+        (1 / siSize)
+        (1 / siSize)
+        (1 / siDepthRange)
+    v =
+      Transform.dirPos
+        (Quaternion.extrinsic (-siAzimuth) (-siInclination) 0)
+        (vec3 0 0 siRadius)
 
     bbTransform = mconcat
-      [ -- XXX: orient wire box "green/near -> far/red"
-        Transform.rotateX (τ/4)
-        -- XXX: the rest must be matched with VP flipped
-      , Transform.translate 0 0 0.5                 -- XXX: shift origin to the near face
-
-        -- XXX: reverse light transform
-      , Transform.scale3 siSize siSize siDepthRange -- XXX: size to projection volume
-      , Transform.translate 0 0 (-siRadius)         -- XXX: translate near face to radius
-      , rotation                                    -- XXX: apply sphere coords
+      [ Transform.inverse pv
+      , Transform.translate 0 0 0.5 -- shift origin to the near face
+      , Transform.rotateX (τ/4) -- put the green side towards the light
       ]
 
-    rotation = mconcat
-      [ Transform.rotateX siInclination
-      , Transform.rotateY siAzimuth
-      ]
+    position = rotation `Quaternion.rotate` vec3 0 0 siRadius
+    direction = rotation `Quaternion.rotate` vec3 0 0 (-1)
+    rotation = Quaternion.intrinsic 0 (siInclination) (siAzimuth)
 
 type Observer = Worker.ObserverIO (VectorS.Vector ("bounding box" ::: Transform))
 
diff --git a/src/Render/ForwardMsaa.hs b/src/Render/ForwardMsaa.hs
--- a/src/Render/ForwardMsaa.hs
+++ b/src/Render/ForwardMsaa.hs
@@ -78,7 +78,7 @@
       }
     clear = Vector.fromList
       [ clearColor
-      , Vk.DepthStencil (Vk.ClearDepthStencilValue 1.0 0)
+      , Vk.DepthStencil (Vk.ClearDepthStencilValue 0.0 0)
       , clearColor
       ]
     clearColor = Vk.Color zero
diff --git a/src/Render/Lit/Colored/Pipeline.hs b/src/Render/Lit/Colored/Pipeline.hs
--- a/src/Render/Lit/Colored/Pipeline.hs
+++ b/src/Render/Lit/Colored/Pipeline.hs
@@ -65,11 +65,16 @@
   { Graphics.cDescLayouts  = Tagged @'[Scene] [set0]
   , Graphics.cStages       = stageSpirv
   , Graphics.cVertexInput  = Graphics.vertexInput @Pipeline
+  , Graphics.cDepthWrite   = False -- XXX: Lit pipelines require depth pre-pass
+  , Graphics.cDepthCompare = Vk.COMPARE_OP_EQUAL
   }
 
 configBlend :: Tagged Scene DsLayoutBindings -> Config
 configBlend tset0 = (config tset0)
   { Graphics.cBlend = True
+  , Graphics.cDepthCompare =
+      -- XXX: blended objects aren't a part of depth prepass
+      Vk.COMPARE_OP_GREATER_OR_EQUAL
   }
 
 stageCode :: Graphics.StageCode
diff --git a/src/Render/Lit/Material/Pipeline.hs b/src/Render/Lit/Material/Pipeline.hs
--- a/src/Render/Lit/Material/Pipeline.hs
+++ b/src/Render/Lit/Material/Pipeline.hs
@@ -75,7 +75,7 @@
       True
   , Graphics.cDepthCompare =
       -- XXX: blended objects aren't a part of depth prepass
-      Vk.COMPARE_OP_LESS_OR_EQUAL
+      Vk.COMPARE_OP_GREATER_OR_EQUAL
   }
 
 stageCode :: Graphics.StageCode
diff --git a/src/Render/Lit/Textured/Pipeline.hs b/src/Render/Lit/Textured/Pipeline.hs
--- a/src/Render/Lit/Textured/Pipeline.hs
+++ b/src/Render/Lit/Textured/Pipeline.hs
@@ -71,8 +71,10 @@
 
 configBlend :: Tagged Scene DsLayoutBindings -> Config
 configBlend tset0 = (config tset0)
-  { Graphics.cBlend      = True
-  , Graphics.cDepthCompare = Vk.COMPARE_OP_LESS_OR_EQUAL
+  { Graphics.cBlend = True
+  , Graphics.cDepthCompare =
+      -- XXX: blended objects aren't a part of depth prepass
+      Vk.COMPARE_OP_GREATER_OR_EQUAL
   }
 
 stageCode :: Graphics.StageCode
diff --git a/src/Render/ShadowMap/Pipeline.hs b/src/Render/ShadowMap/Pipeline.hs
--- a/src/Render/ShadowMap/Pipeline.hs
+++ b/src/Render/ShadowMap/Pipeline.hs
@@ -37,8 +37,8 @@
 
 defaults :: Settings
 defaults = Settings
-  { cull      = Vk.CULL_MODE_BACK_BIT
-  , depthBias = Just (2.0, 2.5)
+  { cull      = Vk.CULL_MODE_FRONT_BIT
+  , depthBias = Just (-2.0, -2.5)
   }
 
 allocate
diff --git a/src/Render/ShadowMap/RenderPass.hs b/src/Render/ShadowMap/RenderPass.hs
--- a/src/Render/ShadowMap/RenderPass.hs
+++ b/src/Render/ShadowMap/RenderPass.hs
@@ -87,7 +87,7 @@
       }
 
     clear = Vector.fromList
-      [ Vk.DepthStencil (Vk.ClearDepthStencilValue 1.0 0)
+      [ Vk.DepthStencil (Vk.ClearDepthStencilValue 0.0 0)
       ]
 
 -- ** Render pass
diff --git a/src/Render/Skybox/Code.hs b/src/Render/Skybox/Code.hs
--- a/src/Render/Skybox/Code.hs
+++ b/src/Render/Skybox/Code.hs
@@ -15,21 +15,20 @@
 
     ${set0binding0}
 
-    layout(location = 0) out vec3 fragUVW;
+    layout(location = 0) out vec3 fUVW;
 
-    float farZ = 0.9999; // 1 - 1e-7;
+    float farZ = 5.960464478e-8; // FLT_EPS
 
     void main() {
       vec4 pos = vec4(0.0);
       switch(gl_VertexIndex) {
           case 0: pos = vec4(-1.0,  3.0, farZ, 1.0); break;
-          case 1: pos = vec4(-1.0, -1.0, farZ, 1.0); break; // XXX: swapped with 2. culling?
+          case 1: pos = vec4(-1.0, -1.0, farZ, 1.0); break;
           case 2: pos = vec4( 3.0, -1.0, farZ, 1.0); break;
       }
 
       vec3 unProjected = (scene.invProjection * pos).xyz;
-      unProjected *= -1;
-      fragUVW = mat3(scene.invView) * unProjected;
+      fUVW = mat3(scene.invView) * unProjected;
 
       gl_Position = pos;
     }
@@ -46,18 +45,20 @@
     ${set0binding1}
     ${set0binding3}
 
-    layout(location = 0) in vec3 fragUVW;
+    layout(location = 0) in vec3 fUVW;
 
     layout(location = 0) out vec4 outColor;
 
     void main() {
       if (scene.envCubeId > -1) {
+        vec3 dir = fUVW;
+        dir.y *= -1; // TODO: make customizable
         outColor = texture(
           samplerCube(
             cubes[nonuniformEXT(scene.envCubeId)],
             samplers[3] // XXX: linear
           ),
-          fragUVW
+          dir
         );
       }
     }
diff --git a/src/Render/Unlit/Colored/Code.hs b/src/Render/Unlit/Colored/Code.hs
--- a/src/Render/Unlit/Colored/Code.hs
+++ b/src/Render/Unlit/Colored/Code.hs
@@ -40,11 +40,10 @@
   [glsl|
     #version 450
 
-    layout(location = 0) in vec4 fragColor;
-
+    layout(location = 0) in vec4 fColor;
     layout(location = 0) out vec4 outColor;
 
     void main() {
-      outColor = fragColor;
+      outColor = fColor;
     }
   |]
diff --git a/src/Stage/Loader/UI.hs b/src/Stage/Loader/UI.hs
--- a/src/Stage/Loader/UI.hs
+++ b/src/Stage/Loader/UI.hs
@@ -13,7 +13,7 @@
 
 import Control.Monad.Trans.Resource (ResourceT)
 import Control.Monad.Trans.Resource qualified as Resource
-import Geomancy (vec4)
+import Geomancy (vec4, withVec2)
 import Geomancy.Layout qualified as Layout
 import Geomancy.Layout.Alignment qualified as Alignment
 import Geomancy.Layout.Box qualified as Box
@@ -119,24 +119,22 @@
   -- Spinner
 
   spinnerTransformTimeP <-
-    Worker.spawnTimed_ True 100 mempty do
-      t <- getMonotonicTime
-      pure $ Transform.rotateZ (realToFrac t)
-
-  spinnerTransformBoxP <-
-    Worker.spawnMerge1
-      (Box.mkTransform) -- FIXME: . Layout.boxFitScale 64)
-      subtitleBoxP
+    Worker.spawnTimed_ True 100 0.0 Engine.secondsSinceStart
 
   spinnerP <-
     Worker.spawnMerge2
-      ( \place turn ->
-          UnlitTextured.stores1
+      ( \box time ->
+          withVec2 box.position \x y ->
+            withVec2 box.size \w h ->
+            UnlitTextured.stores1
               (Samplers.linear Samplers.indices)
               spinnerIx
-              [turn, place]
+              [ Transform.translate x y 0
+              , Transform.rotateZ time
+              , Transform.scale (min 256 $ min w h)
+              ]
       )
-      spinnerTransformBoxP
+      subtitleBoxP
       spinnerTransformTimeP
 
   -- Models
