Models 44

Mistral Large 4

by MistralReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
4 min 2 s
Cost
$0.046
Output tokens
21,782
Spent thinking
15,635 tokens
Speed
90.2 tok/s
Console errors
0
First token after
917 ms
First visible text after
3 min 15 s
Prompt tokens
577
Code size
17.1 KB
Finish reason
stop
Served by
Mistral
Model id
mistralai/mistral-large-4-0
Reasoning sent
{"effort":"high"}
Max output
262,144 tokens
Generated
Nov 12, 2026

What it was asked

The prompt
Build a low-poly 3D scene of a campfire in a forest clearing at night, with Three.js in a single HTML file. Load Three.js only through this import map (do not use any other URL or version): <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script> Requirements: - A ground with a few low-poly rocks and logs around a campfire in the center. - At least 30 low-poly trees placed randomly around the clearing (not on the fire). - The campfire has animated flames and a warm point light that flickers and casts shadows. - Sparks rising from the fire as a simple particle system. - A dark night sky with stars and a moon, plus fog that fades the distant trees. - OrbitControls to look around, with limits so the camera stays above the ground. - The renderer fills the window and handles resizing correctly.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 450 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Night Campfire — Low-Poly Forest</title>
<style>
  html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #070b16; }
  canvas { display: block; }
  #hud {
    position: fixed; top: 14px; left: 16px; pointer-events: none; user-select: none;
    color: #ffd9a8; text-shadow: 0 1px 8px rgba(0,0,0,.85);
    font-family: Georgia, 'Times New Roman', serif;
  }
  #hud h1 { margin: 0; font-size: 18px; font-weight: normal; letter-spacing: 3px; }
  #hud p  { margin: 3px 0 0; font-size: 11px; letter-spacing: 1px; opacity: .75;
            font-family: Verdana, sans-serif; }
  #vignette {
    position: fixed; inset: 0; pointer-events: none;
    background: radial-gradient(ellipse at 50% 45%, transparent 55%, rgba(0,0,12,.55) 100%);
  }
  @media (max-width: 480px) {
    #hud h1 { font-size: 14px; }
    #hud p { font-size: 10px; }
  }
</style>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>
</head>
<body>
<div id="hud">
  <h1>&#10022; NIGHT CAMPFIRE &#10022;</h1>
  <p>drag to orbit &nbsp;&middot;&nbsp; scroll to zoom</p>
</div>
<div id="vignette"></div>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/* ---------- renderer / scene / camera ---------- */
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0e1321, 0.026);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1500);
camera.position.set(9, 5.2, 11);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);

/* ---------- night sky dome ---------- */
const sky = new THREE.Mesh(
  new THREE.SphereGeometry(700, 32, 16),
  new THREE.ShaderMaterial({
    side: THREE.BackSide,
    depthWrite: false,
    vertexShader: `
      varying vec3 vP;
      void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
    fragmentShader: `
      varying vec3 vP;
      void main(){
        float h = normalize(vP).y;
        vec3 c = mix(vec3(0.055,0.075,0.13), vec3(0.006,0.010,0.028), smoothstep(-0.05, 0.55, h));
        gl_FragColor = vec4(c, 1.0);
      }`
  })
);
sky.renderOrder = -10;
scene.add(sky);

/* ---------- stars ---------- */
const STAR_COUNT = 900;
const starPos = new Float32Array(STAR_COUNT * 3);
const starCol = new Float32Array(STAR_COUNT * 3);
const cA = new THREE.Color(0xffffff), cB = new THREE.Color(0xafc4ff), cC = new THREE.Color(0xffe6b8);
for (let i = 0; i < STAR_COUNT; i++) {
  const v = new THREE.Vector3(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1);
  if (v.lengthSq() < 0.001) v.set(0, 1, 0);
  if (v.y < 0.04) v.y = 0.04 + Math.random() * 0.08;
  v.normalize().multiplyScalar(620);
  starPos.set([v.x, v.y, v.z], i * 3);
  const c = Math.random() < 0.7 ? cA : (Math.random() < 0.5 ? cB : cC);
  const b = 0.55 + Math.random() * 0.45;
  starCol.set([c.r * b, c.g * b, c.b * b], i * 3);
}
const starGeo = new THREE.BufferGeometry();
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
starGeo.setAttribute('color', new THREE.BufferAttribute(starCol, 3));
const starMat = new THREE.PointsMaterial({
  size: 2.2, sizeAttenuation: false, vertexColors: true,
  transparent: true, opacity: 0.9, depthWrite: false, fog: false
});
const stars = new THREE.Points(starGeo, starMat);
stars.frustumCulled = false;
scene.add(stars);

/* ---------- helpers ---------- */
function makeRadialTexture(inner, mid, outer) {
  const c = document.createElement('canvas');
  c.width = c.height = 128;
  const g = c.getContext('2d');
  const grad = g.createRadialGradient(64, 64, 2, 64, 64, 64);
  grad.addColorStop(0, inner);
  grad.addColorStop(0.35, mid);
  grad.addColorStop(1, outer);
  g.fillStyle = grad;
  g.fillRect(0, 0, 128, 128);
  return new THREE.CanvasTexture(c);
}

/* ---------- moon ---------- */
const moonDir = new THREE.Vector3(-0.38, 0.42, -0.55).normalize();
const moonPos = moonDir.clone().multiplyScalar(640);
const moon = new THREE.Mesh(
  new THREE.SphereGeometry(17, 24, 16),
  new THREE.MeshBasicMaterial({ color: 0xf7f2df, fog: false })
);
moon.position.copy(moonPos);
scene.add(moon);
const moonGlow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: makeRadialTexture('rgba(255,246,220,1)', 'rgba(255,240,200,0.35)', 'rgba(255,236,190,0)'),
  blending: THREE.AdditiveBlending, transparent: true, opacity: 0.85, depthWrite: false, fog: false
}));
moonGlow.position.copy(moonPos);
moonGlow.scale.setScalar(150);
scene.add(moonGlow);

/* ---------- lights ---------- */
scene.add(new THREE.HemisphereLight(0x33456e, 0x0b0e08, 0.55));
const moonLight = new THREE.DirectionalLight(0x8fa8e0, 0.5);
moonLight.position.copy(moonDir.clone().multiplyScalar(200));
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xff7a2a, 70, 45, 2);
fireLight.position.set(0, 1.0, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.2;
fireLight.shadow.camera.far = 30;
fireLight.shadow.bias = -0.004;
fireLight.shadow.normalBias = 0.03;
scene.add(fireLight);

/* ---------- ground (low-poly, vertex-colored) ---------- */
const groundGeo = new THREE.PlaneGeometry(160, 160, 52, 52);
groundGeo.rotateX(-Math.PI / 2);
{
  const p = groundGeo.attributes.position;
  const colors = new Float32Array(p.count * 3);
  const grass = new THREE.Color(0x2c4a26), dirt = new THREE.Color(0x4a3826),
        scorchC = new THREE.Color(0x14100c), moss = new THREE.Color(0x1c331e);
  for (let i = 0; i < p.count; i++) {
    const x = p.getX(i), z = p.getZ(i), r = Math.hypot(x, z);
    const h = Math.sin(x * 0.09) * Math.cos(z * 0.08) * 1.5
            + Math.sin(x * 0.21 + 2.0) * Math.cos(z * 0.17 + 1.0) * 0.55
            + Math.sin(x * 0.45 + 0.7) * Math.cos(z * 0.4 + 2.2) * 0.18;
    const flat = THREE.MathUtils.smoothstep(r, 3.5, 10); // keep clearing flat
    p.setY(i, h * flat);
    const c = grass.clone().lerp(Math.random() < 0.5 ? moss : dirt, Math.random() * 0.7);
    c.offsetHSL(0, 0, (Math.random() - 0.5) * 0.04);
    if (r < 4.5) c.lerp(scorchC, (1 - r / 4.5) * 0.9);
    colors.set([c.r, c.g, c.b], i * 3);
  }
  groundGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  groundGeo.computeVertexNormals();
}
const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({
  vertexColors: true, flatShading: true, roughness: 1, metalness: 0
}));
ground.receiveShadow = true;
scene.add(ground);

/* scorch mark under the fire */
const scorch = new THREE.Mesh(
  new THREE.CircleGeometry(1.15, 24),
  new THREE.MeshBasicMaterial({ color: 0x0a0806, transparent: true, opacity: 0.75, depthWrite: false })
);
scorch.rotation.x = -Math.PI / 2;
scorch.position.y = 0.02;
scorch.renderOrder = 1;
scene.add(scorch);

/* ---------- campfire ---------- */
const fire = new THREE.Group();
scene.add(fire);

const stoneMat = new THREE.MeshStandardMaterial({ color: 0x8a8d94, flatShading: true, roughness: 0.9 });
const barkMat  = new THREE.MeshStandardMaterial({ color: 0x5a4028, flatShading: true, roughness: 1 });

/* ring of stones */
for (let i = 0; i < 9; i++) {
  const a = i / 9 * Math.PI * 2 + Math.random() * 0.4;
  const r = 0.62 + Math.random() * 0.12;
  const s = 0.14 + Math.random() * 0.16;
  const st = new THREE.Mesh(new THREE.DodecahedronGeometry(s, 0), stoneMat);
  st.position.set(Math.cos(a) * r, s * 0.45, Math.sin(a) * r);
  st.scale.set(s * (0.8 + Math.random() * 0.5), s * (0.5 + Math.random() * 0.4), s * (0.8 + Math.random() * 0.5));
  st.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
  st.castShadow = st.receiveShadow = true;
  fire.add(st);
}

/* teepee of logs */
for (let i = 0; i < 6; i++) {
  const a = i / 6 * Math.PI * 2 + 0.26;
  const base = new THREE.Vector3(Math.cos(a) * 0.5, 0, Math.sin(a) * 0.5);
  const apex = new THREE.Vector3(0, 0.72, 0);
  const dir = apex.clone().sub(base);
  const len = dir.length();
  const log = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.095, len, 6), barkMat);
  log.position.copy(base).add(apex).multiplyScalar(0.5);
  log.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize());
  log.castShadow = log.receiveShadow = true;
  fire.add(log);
}

/* animated low-poly flames */
const flameDefs = [
  { r: 0.36, h: 1.15, c: 0xff2e00, o: 0.50 },
  { r: 0.27, h: 0.92, c: 0xff7300, o: 0.65 },
  { r: 0.18, h: 0.66, c: 0xffb62e, o: 0.80 },
  { r: 0.10, h: 0.45, c: 0xfff0b8, o: 0.95 }
];
const flames = [];
for (const d of flameDefs) {
  const g = new THREE.ConeGeometry(d.r, d.h, 7, 1);
  g.translate(0, d.h / 2, 0); // anchor base at origin so scaling grows upward
  const m = new THREE.Mesh(g, new THREE.MeshBasicMaterial({
    color: d.c, transparent: true, opacity: d.o,
    blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide
  }));
  m.position.y = 0.22;
  m.rotation.y = Math.random() * 6;
  fire.add(m);
  flames.push(m);
}

/* ember core */
const ember = new THREE.Mesh(
  new THREE.IcosahedronGeometry(0.17, 0),
  new THREE.MeshBasicMaterial({ color: 0xff6a00, blending: THREE.AdditiveBlending, transparent: true, opacity: 0.9, depthWrite: false })
);
ember.position.y = 0.3;
fire.add(ember);

/* warm glow sprite */
const fireGlow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: makeRadialTexture('rgba(255,190,90,0.9)', 'rgba(255,120,40,0.35)', 'rgba(255,80,20,0)'),
  blending: THREE.AdditiveBlending, transparent: true, opacity: 0.9, depthWrite: false
}));
fireGlow.position.y = 0.7;
fireGlow.scale.setScalar(2.2);
fire.add(fireGlow);

/* ---------- trees (>= 30, random, away from the fire) ---------- */
const trunkGeo = new THREE.CylinderGeometry(0.16, 0.28, 2.4, 5);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a3423, flatShading: true, roughness: 1 });
const coneGeos = [
  new THREE.ConeGeometry(1.35, 2.3, 6),
  new THREE.ConeGeometry(1.00, 2.0, 6),
  new THREE.ConeGeometry(0.62, 1.7, 6)
];
const folMats = [0x1e3d26, 0x274a2c, 0x183421].map(c =>
  new THREE.MeshStandardMaterial({ color: c, flatShading: true, roughness: 1 })
);
function makeTree(x, z, sMin, sMax) {
  const t = new THREE.Group();
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y = 1.2;
  trunk.castShadow = trunk.receiveShadow = true;
  t.add(trunk);
  const fm = folMats[(Math.random() * folMats.length) | 0];
  const ys = [2.5, 3.6, 4.6];
  coneGeos.forEach((g, j) => {
    const m = new THREE.Mesh(g, fm);
    m.position.y = ys[j];
    m.castShadow = m.receiveShadow = true;
    t.add(m);
  });
  const s = sMin + Math.random() * (sMax - sMin);
  t.scale.set(s, s * (0.9 + Math.random() * 0.3), s);
  t.position.set(x, 0, z);
  t.rotation.y = Math.random() * Math.PI * 2;
  scene.add(t);
}
let placed = 0, guard = 0;
while (placed < 34 && guard < 800) {           // clearing trees
  guard++;
  const a = Math.random() * Math.PI * 2;
  const r = 8.5 + Math.random() * 26;
  const x = Math.cos(a) * r, z = Math.sin(a) * r;
  if (Math.hypot(x, z) < 7.5) continue;        // never on the fire
  makeTree(x, z, 0.75, 1.7);
  placed++;
}
for (let i = 0; i < 22; i++) {                  // distant silhouettes in the fog
  const a = Math.random() * Math.PI * 2;
  const r = 38 + Math.random() * 18;
  makeTree(Math.cos(a) * r, Math.sin(a) * r, 1.1, 2.0);
}

/* ---------- scattered rocks & fallen logs ---------- */
for (let i = 0; i < 6; i++) {
  const a = Math.random() * Math.PI * 2, r = 3.2 + Math.random() * 6.5;
  const s = 0.25 + Math.random() * 0.5;
  const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(s, 0), stoneMat);
  rock.position.set(Math.cos(a) * r, s * 0.4, Math.sin(a) * r);
  rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
  rock.scale.y = 0.6 + Math.random() * 0.4;
  rock.castShadow = rock.receiveShadow = true;
  scene.add(rock);
}
const fallenLogGeo = new THREE.CylinderGeometry(0.09, 0.11, 1.6, 6);
for (let i = 0; i < 4; i++) {
  const a = Math.random() * Math.PI * 2, r = 2.2 + Math.random() * 2.2;
  const log = new THREE.Mesh(fallenLogGeo, barkMat);
  log.position.set(Math.cos(a) * r, 0.1, Math.sin(a) * r);
  log.rotation.order = 'YXZ';
  log.rotation.y = Math.random() * Math.PI * 2;
  log.rotation.z = Math.PI / 2;
  log.castShadow = log.receiveShadow = true;
  scene.add(log);
}

/* ---------- sparks (particle system) ---------- */
const SPARKS = 90;
const sPos = new Float32Array(SPARKS * 3);
const sCol = new Float32Array(SPARKS * 3);
const sVel = new Array(SPARKS);
const sLife = new Float32Array(SPARKS);
const sMax = new Float32Array(SPARKS);
function resetSpark(i, scattered) {
  const a = Math.random() * Math.PI * 2, r = Math.random() * 0.24;
  sPos[i * 3] = Math.cos(a) * r;
  sPos[i * 3 + 1] = scattered ? 0.5 + Math.random() * 3.2 : 0.5 + Math.random() * 0.3;
  sPos[i * 3 + 2] = Math.sin(a) * r;
  sVel[i] = new THREE.Vector3((Math.random() - 0.5) * 0.55, 0.9 + Math.random() * 1.5, (Math.random() - 0.5) * 0.55);
  sMax[i] = 0.8 + Math.random() * 1.7;
  sLife[i] = scattered ? Math.random() * sMax[i] : sMax[i];
}
for (let i = 0; i < SPARKS; i++) resetSpark(i, true);
const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sCol, 3));
const sparks = new THREE.Points(sparkGeo, new THREE.PointsMaterial({
  size: 0.13, vertexColors: true, transparent: true, opacity: 1,
  blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true
}));
sparks.frustumCulled = false;
scene.add(sparks);

/* ---------- orbit controls (camera stays above the ground) ---------- */
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 4;
controls.maxDistance = 60;
controls.minPolarAngle = 0.12;
controls.maxPolarAngle = Math.PI * 0.495;   // never below the horizon/ground
controls.autoRotate = true;
controls.autoRotateSpeed = 0.9;
let idleTimer = null;
controls.addEventListener('start', () => {
  controls.autoRotate = false;
  if (idleTimer) clearTimeout(idleTimer);
});
controls.addEventListener('end', () => {
  if (idleTimer) clearTimeout(idleTimer);
  idleTimer = setTimeout(() => { controls.autoRotate = true; }, 6000);
});
controls.addEventListener('change', () => {
  const t = controls.target;
  t.y = THREE.MathUtils.clamp(t.y, 0.4, 8);
  const hr = Math.hypot(t.x, t.z);
  if (hr > 12) { t.x *= 12 / hr; t.z *= 12 / hr; }
});

/* ---------- resize ---------- */
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

/* ---------- animation loop ---------- */
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  /* flames: flicker, sway, spin */
  for (let i = 0; i < flames.length; i++) {
    const f = flames[i];
    const sway = Math.sin(t * (7 + i * 1.9) + i * 2.4);
    f.scale.set(
      1 + 0.16 * sway + 0.06 * Math.sin(t * 15 + i),
      1 + 0.32 * Math.sin(t * (6 + i * 1.3) + i * 1.7) + 0.18 * Math.sin(t * 11.7 + i * 3),
      1 + 0.16 * Math.cos(t * (8.3 + i) + i)
    );
    f.rotation.y = t * (1.2 + i * 0.35) + i * 1.3;
    f.position.x = Math.sin(t * 2.6 + i * 2.1) * 0.035;
    f.position.z = Math.cos(t * 2.2 + i * 1.4) * 0.035;
    f.material.opacity = flameDefs[i].o * (0.8 + 0.25 * Math.sin(t * 12 + i * 4.2));
  }
  ember.scale.setScalar(1 + 0.25 * Math.sin(t * 9.3));

  /* flickering warm point light */
  const flick = Math.sin(t * 11.3) * 0.5 + Math.sin(t * 23.7 + 1.3) * 0.3 + Math.sin(t * 5.1 + 4.0) * 0.2;
  fireLight.intensity = 70 * (1 + 0.22 * flick) + Math.random() * 5;
  fireLight.position.y = 0.95 + 0.06 * Math.sin(t * 8.8);
  fireGlow.material.opacity = 0.75 + 0.2 * flick;
  fireGlow.scale.setScalar(2.0 + 0.35 * flick + 0.1 * Math.sin(t * 13.7));

  /* sparks rise, wobble and fade to black (additive => invisible) */
  for (let i = 0; i < SPARKS; i++) {
    sLife[i] -= dt;
    if (sLife[i] <= 0) resetSpark(i, false);
    const v = sVel[i];
    v.y = Math.max(0.35, v.y - 0.35 * dt);
    v.x += Math.sin(t * 4.1 + i * 1.3) * dt * 0.5;
    v.z += Math.cos(t * 3.3 + i * 2.1) * dt * 0.5;
    sPos[i * 3] += v.x * dt;
    sPos[i * 3 + 1] += v.y * dt;
    sPos[i * 3 + 2] += v.z * dt;
    const f = Math.max(sLife[i] / sMax[i], 0), b = f * f;
    sCol[i * 3] = b;
    sCol[i * 3 + 1] = 0.55 * b * f;
    sCol[i * 3 + 2] = 0.12 * b * f * f;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;

  /* gentle star twinkle */
  starMat.opacity = 0.75 + 0.18 * Math.sin(t * 0.9) + 0.07 * Math.sin(t * 2.3);

  controls.update();
  renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
Reasoning trace (15,635 tokens)
We need to build a single HTML file with Three.js via import map, low-poly campfire scene at night. Requirements:

- Ground with low-poly rocks and logs around campfire center
- >=30 trees randomly placed (not on fire)
- Animated flames + warm point light flickering with shadows
- Sparks particle system
- Dark night sky with stars and moon, fog
- OrbitControls with limits (minPolarAngle/maxPolarAngle so camera stays above ground — set maxPolarAngle < PI/2, minDistance/maxDistance)
- Renderer fills window, resize handling
- Self-contained, no external besides the import map URLs
- Must look good at 1280x800, adapt to 375px
- Something visible on first paint (no blocking start message needed since it's not a game — orbit controls work with mouse; keyboard not needed)
- No console errors

Let me write the code.

Key elements:

1. Import map exactly as given.
2. Module script importing three and OrbitControls from 'three/addons/controls/OrbitControls.js'.
3. Scene, fog (FogExp2 dark blue), sky: use a large sphere with shader or just scene background color + stars (Points) + moon (circle mesh with emissive, maybe a sprite). I'll do stars as Points with random positions on a dome, moon as a sphere with emissive material far away, plus maybe a moon glow sprite (canvas-generated texture — allowed since inline).
4. Ground: a circular or plane geometry, low-poly: use PlaneGeometry with segments and displace vertices randomly, flatShading, vertex colors maybe. Keep it simple: CircleGeometry with many segments, displace y randomly except near center, MeshStandardMaterial flatShading dark green/brown.
5. Campfire center: logs arranged in a teepee (cylinders rotated), stones ring (icosahedrons/dodecahedrons flattened), flames: several cone/plane meshes with additive material, animated scale/rotation in tick. Point light orange, intensity flickers via noise, castShadow true, shadow map size 1024.
6. Sparks: Points with custom positions updated each frame rising with wobble, fade via size/opacity — use PointsMaterial with vertexColors? Simpler: use ShaderMaterial or just PointsMaterial with transparent and animate positions, recycle when y > threshold. Additive blending, orange color. Could vary color per spark via vertex colors and update alpha... PointsMaterial doesn't support per-point opacity easily; use color attribute to fade to black with additive blending (additive blending makes black invisible). That works nicely.
7. Trees: >=30, random positions in ring radius 8..30, not within ~3 of center. Each tree: trunk cylinder + 2-3 cones (low poly, flatShading), dark green. Random scale/rotation. Maybe use InstancedMesh for performance but simple meshes fine for 30 trees (~100 meshes fine). Could merge but keep simple. To cast shadows they need castShadow; 30 trees fine.
8. Rocks and logs around fire: few rocks (dodecahedron), logs lying (cylinder) scattered.
9. OrbitControls: target at fire (0,1,0), enableDamping, maxPolarAngle = Math.PI/2 - 0.05 (stay above ground), minDistance 3, maxDistance 60, minPolarAngle maybe 0.15. Also pan limited? Could disable pan or clamp target. Keep pan enabled but maybe limit. Requirement: limits so camera stays above ground — maxPolarAngle < PI/2 does that.
10. Resize handler updating camera aspect and renderer size.
11. Renderer: antialias, shadowMap enabled PCFSoft, setPixelRatio min(devicePixelRatio,2), outputColorSpace default. Fill window: width/height 100%.
12. Animation loop with clock; flame animation: scale pulses, rotation; light intensity flicker = base + noise; sparks update; controls.update; render.

Also add subtle ambient/hemisphere light dim blue for night visibility, and maybe a faint directional moonlight with shadows? Requirement says point light casts shadows. Moon directional could also cast but keep one shadow light for perf. Add dim hemisphere.

Flames: create 3-4 flame meshes: ConeGeometry with flat shading? Better: use custom "flame" shape — a cone with distorted vertices, MeshBasicMaterial with color gradient? Simple approach: 3 cones nested, colors from yellow core to orange to red, additive blending, transparent, animated scale (y scale oscillates, x/z squish), slight rotation. Plus inner glow sprite at base. That looks decent.

Alternative: use planes with a canvas-generated flame texture (radial gradient) — but cones are fine and truly low-poly.

Actually a nicer flame: use a cone geometry, then in tick set scale.y = base*(1+0.3*sin(t*10+i)), rotation.y += ...; material opacity flickers. Additive blending on black background looks glowy.

Sparks: BufferGeometry with N=80 points, positions attribute, colors attribute. Each spark: pos, vel (up + random x/z), life. Update: pos += vel*dt; vel.y += slight? Actually sparks rise and slow: vel.y decreases; wobble x. When life ends or y>8, reset at fire top with new random vel, life. Color: lerp from yellow to dark red based on life, additive so fading to black = invisible. size ~ 0.15, sizeAttenuation true.

Stars: 800 points on sphere radius 400 (inside far plane 1000), sizes 1-2, white/blueish, slight twinkle via opacity? PointsMaterial single opacity; could twinkle by size — skip or animate material size slightly. Keep static, fine. Maybe two star layers different sizes.

Moon: sphere radius 12 at position like (-120, 140, -200), MeshBasicMaterial color pale #f4f1de, plus glow: sprite with canvas radial gradient texture, additive, scale 60. Also a subtle directional light from moon direction, very dim blue, no shadow (or with shadow? only point light needs shadows). Keep directional intensity 0.15.

Sky: scene.background = new Color(0x050914) or gradient via large sphere with shader. Simple: background color + fog FogExp2(0x0a1020, 0.028). Fog fades distant trees — yes.

Ground: CircleGeometry radius 60, 48 segments? Actually PlaneGeometry(120,120,40,40), displace vertices: y = noise-ish random * amplitude, but flatten near center (within r<6 keep y=0) and maybe raise edges slightly. Use simple pseudo-random with smoothing? Just random per vertex creates jagged; use a function like sum of sines for gentle hills: y = 1.2*sin(x*0.08)*cos(z*0.07) + 0.4*sin(x*0.3+1.7)*cos(z*0.23+0.5), zero near center via smoothstep factor. flatShading true. Vertex colors: mix dark green and brown based on height/random for variation. Use MeshStandardMaterial vertexColors true, flatShading.

Compute normals after displacement.

Trees: function makeTree(): group with trunk (CylinderGeometry(0.18,0.3,h,5)) brown, foliage: 2-3 cones stacked (ConeGeometry(r, h, 6)) dark green with slight color variance. castShadow on all. Random position: angle random, radius 9 + rand*22 (up to 31), ensure distance from center > 7. Also avoid overlapping fire ring. 34 trees.

Rocks: 7 rocks in ring radius 1.1-1.6 around fire: DodecahedronGeometry(rand 0.18-0.35), gray, flatShading, random rotation/scale y squashed, castShadow/receiveShadow.

Logs around fire: 5-6 cylinders arranged teepee: each log length 1.1 radius 0.09, leaning: position at ring radius 0.35, tilt ~ 65-75 deg from horizontal pointing to center top. Compute orientation: create cylinder along Y, then rotate to lean. Easiest: for i in 6: angle = i/6*2PI; log group at (cos*r, 0.05, sin*r), rotation.z = tilt, rotation.y = -angle+PI/2... Let me think: cylinder axis is Y. We want it leaning toward center, top at center. Place at angle a, offset r=0.32 from center, tilt around the axis perpendicular. Set mesh.rotation.y = -a (so local +? ) then rotation.z = tilt? Order of rotations in three.js default 'XYZ'. Simpler: use quaternion setFromUnitVectors? Or: create log, set position (cos(a)*0.34, 0.10, sin(a)*0.34), then rotation.set(0, 0, tilt) after rotating y by ... hmm.

Alternative simpler: logs lying flat in a small pile next to fire plus a few leaning. Requirement: "logs around a campfire" — a teepee of logs is classic. Let me do: for each log: 
```
const log = new Mesh(cylGeo, barkMat);
log.position.set(Math.cos(a)*0.35, 0.09, Math.sin(a)*0.35);
log.rotation.y = -a + Math.PI/2; // orient local X toward center? 
log.rotation.z = Math.PI/2 - leanAngle... 
```
Getting fiddly. Easier approach: build log pointing up (along Y), then apply quaternion: rotate around axis (sin? ). Use: log.quaternion.setFromUnitVectors(new Vector3(0,1,0), dir) where dir = normalized vector from base position to apex (0, 0.55, 0) minus base position... but setFromUnitVectors gives shortest arc; position log so its center is midpoint between base and apex: base = (cos a * r, 0, sin a * r), apex = (0, h, 0). mid = base/2 with y h/2. dir = (apex - base).normalize(). Then position = mid. Length of cylinder should equal |apex-base|. That works cleanly.

```
const base = new THREE.Vector3(Math.cos(a)*0.42, 0, Math.sin(a)*0.42);
const apex = new THREE.Vector3(0, 0.62, 0);
const dir = apex.clone().sub(base);
const len = dir.length();
const geo = new THREE.CylinderGeometry(0.07, 0.09, len, 6);
const log = new Mesh(geo, mat);
log.position.copy(base).add(apex).multiplyScalar(0.5);
log.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), dir.normalize());
```
Good. Plus 2-3 fallen logs on ground nearby: cylinder rotated z=PI/2 lying, random position ring 2-4.

Fire light: PointLight(0xff7722, intensity, distance 30, decay 2), position (0, 0.9, 0), castShadow, shadow.mapSize 1024, shadow.camera.near 0.1 far 25, bias -0.005. Flicker: intensity = 60? In three r155+ uses physically correct lighting by default (r170: useLegacyLights removed default false). PointLight intensity in candela; with decay 2, need high intensity like 30-80. I'll set intensity ~ 40 and flicker ±. Let's use intensity base 50, flicker = 50*(0.75+0.25*noise) plus small random. Also light color slightly varies. Distance 40, decay 2.

Also add a small warm PointLight? One is fine.

Ambient: HemisphereLight(0x223355 sky, 0x0a0f0a ground, 0.35). Plus dim DirectionalLight moon 0x8899ff 0.25 from moon position, no shadow.

Shadow: renderer.shadowMap.enabled = true, type PCFSoftShadowMap. Ground receiveShadow, trees cast+receive, rocks cast, logs cast.

Flames: group at fire. 3 cones:
- outer: ConeGeometry(0.32, 0.9, 7), color 0xff4400, transparent opacity 0.85, blending Additive, depthWrite false
- mid: ConeGeometry(0.22, 0.7, 6), color 0xff8800
- inner: ConeGeometry(0.13, 0.5, 6), color 0xffee88
Position base at y 0.25 (above logs base). Each frame: t = clock elapsed; flame.scale.set(1+0.15*sin(t*9+i*2), 1+0.3*sin(t*7+i*1.3)+0.2*sin(t*13), 1+0.15*cos(t*8)); rotation.y = t*2 + i; material opacity flicker. Also move slightly x/z sway. Also inner core bright.

Also ember glow: a small sphere/plane at base with emissive? Add a PointLight already. Add "glow" sprite at fire with additive radial gradient, scale pulsing.

Also maybe a subtle smoke? Not required.

Ground fire scorch: a dark circle mesh under fire (CircleGeometry radius 0.9, black, opacity 0.6) slightly above ground y=0.02 to avoid z-fighting, polygonOffset maybe. Good detail.

Camera: start at (8, 5, 10) looking at (0, 1, 0). fov 55. near 0.1 far 1000.

Controls: target (0,1.2,0); enableDamping dampingFactor 0.06; minDistance 4; maxDistance 55; maxPolarAngle = Math.PI * 0.495 (just under horizon so camera stays above ground); minPolarAngle = 0.1; maybe enablePan true but limit? Screen-space pan default. Could set controls.maxTargetRadius? Not in 0.170? There is maxTargetRadius in newer versions... Actually OrbitControls has minTargetRadius/maxTargetRadius added around r169? Not sure. Safer: on 'change' clamp target y between 0.5 and 8 and length horizontal <= 12. Or just disable pan (enablePan = false) — simplest and keeps target at fire. But panning is nice; clamping in change event: 
```
controls.addEventListener('change', ()=>{ const t=controls.target; t.y=THREE.MathUtils.clamp(t.y,0.5,6); const r=Math.hypot(t.x,t.z); if(r>10){t.x*=10/r;t.z*=10/r;} });
```
Fine.

Resize: window.addEventListener('resize', ...) update camera.aspect, camera.updateProjectionMatrix(), renderer.setSize(innerWidth, innerHeight).

HTML/CSS: body margin 0, overflow hidden, canvas block. Add a small overlay title? "Campfire — drag to orbit" hint, subtle, top-left, fades? Keep minimal: small text bottom-left "drag to orbit • scroll to zoom". Nice touch, doesn't block. First paint shows scene immediately.

Pixel ratio: renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)).

Stars: generate 900 points on upper hemisphere sphere radius 450, y>0. Use BufferGeometry positions, PointsMaterial({color:0xffffff, size:1.6, sizeAttenuation:false? At radius 450 with attenuation size would be tiny; use sizeAttenuation false with size 1.5-2.5 px — but PointsMaterial has single size. Use size 2, sizeAttenuation false, transparent, opacity 0.9, fog:false (material.fog = false so stars not fogged). Also second layer size 3 few bright stars. Colors slight variation via vertexColors: mix white/blue/yellow. With sizeAttenuation false, vertexColors still work.

Also twinkle: animate opacity of star material slightly sin — subtle. Fine.

Moon: sphere radius 14 at (-160, 190, -260)? Must be within far=1000 and outside fog influence — fog affects materials unless material.fog=false. MeshBasicMaterial has fog property; set fog:false for moon and glow sprite. DirectionalLight from moon direction: position (-60, 80, -100), target 0.

Moon glow sprite: canvas 128x128 radial gradient white->transparent, SpriteMaterial map, blending additive, depthWrite false, fog false, scale 90. Position same as moon slightly.

Fog: scene.fog = new THREE.FogExp2(0x0b1220, 0.022). Background color 0x070b16 slightly darker than fog so horizon blends. Actually with FogExp2, background color unaffected — set background same-ish as fog color for seamless: background 0x0b1220. Hmm night sky should be darker at zenith... Could use a big sky dome with gradient shader. Simple gradient dome: SphereGeometry(600, 32, 15) BackSide ShaderMaterial with top color #020409 and horizon #0d1626. That's nicer. fog:false not applicable to ShaderMaterial (fog only if shader includes fog chunks; it won't). Let's do gradient dome. Stars radius 550 inside dome 600. Moon at distance ~500 on dome. far plane 1200.

ShaderMaterial dome:
```
vertexShader: varying vec3 vPos; void main(){ vPos = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }
fragmentShader: varying vec3 vPos; void main(){ float h = normalize(vPos).y; vec3 top = vec3(0.008,0.012,0.03); vec3 hor = vec3(0.05,0.075,0.13); vec3 c = mix(hor, top, smoothstep(0.0,0.5,h)); gl_FragColor = vec4(c,1.0); }
```
side: BackSide, depthWrite false. Render first (renderOrder -1) or just it's far away.

Fog color should match horizon color ~ (0.05,0.075,0.13) => 0x0d1321. FogExp2 density 0.02 — at distance 40, factor = 1-exp(-(0.02*40)^2)= 1-exp(-0.64)=0.47 — trees at 30 faded nicely. Maybe 0.018.

Ground color: dark green-brown; with fog it fades. Vertex colors: base green 0x1d3320 varying to 0x2a2418 dirt near center? Nice: near fire, scorch brown. Compute color per vertex by radius: mix.

Trees color variation: use per-tree material clone? 34 materials fine. Or vertexColors on shared geometry... simpler: create 3-4 shared foliage materials with different greens, assign randomly.

Performance: shadows with ~40 casters fine.

Sparks: N=90. positions Float32Array, colors Float32Array. Arrays for vel, life, maxLife. PointsMaterial({size:0.12, vertexColors:true, transparent:true, blending:AdditiveBlending, depthWrite:false, sizeAttenuation:true}). Update in tick with dt clamped.

Reset spark: pos = (rand*0.3-0.15, 0.7+rand*0.2, ...), vel = (rand*0.3-0.15, 1.2+rand*1.2, rand*0.3-0.15), life = maxLife = 1+rand*1.5. Update: life -= dt; pos.addScaledVector(vel, dt); vel.x += sin(t*3+seed)*dt*0.3 wobble; vel.y *= (1 - 0.15*dt)? sparks decelerate slightly; gravity slight negative? Keep rising, slow. t01 = life/maxLife; color = lerp(dark red (0.3,0.05,0.0) -> yellow (1,0.8,0.3)) * t01^2 maybe; with additive, black = invisible. Set colors[i3] = r*brightness etc. When life<=0 reset. Also needsUpdate.

Also flame light illuminates sparks? They're basic material, fine — emissive look.

Embers bed: small cluster of glowing rocks? Add a few tiny spheres with emissive orange at base, pulsing opacity — nice. Optional: 8 small icosahedrons at base with MeshBasicMaterial color orange, scale pulsing. Keep simple: one "ember core" sphere radius 0.18 color 0xff6600 additive pulsing.

Rocks ring: 8 stones.

Scattered rocks: 5 bigger rocks random at radius 4-10. Fallen logs: 3.

Also maybe fireflies? Not required; skip (or tiny green points wandering — adds life; optional. Skip to keep focused, or add — it's cheap and pretty. Requirement list is specific; extra fireflies fine but keep code manageable. I'll skip fireflies... actually a few add magic; but risk none. I'll add 12 faint drifting fireflies? Hmm, keep scope tight — skip.)

Text overlay: div with title "Night Campfire" and hint. Style: position fixed, top 16px left 16px, color rgba(255,220,180,0.85), font-family system, text-shadow. Small. Also maybe a subtle vignette via CSS radial-gradient overlay for cinematic look — pointer-events none. Good.

Now write the animate loop:

```
const clock = new THREE.Clock();
function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;
  // flames
  flames.forEach((f,i)=>{ ... });
  // light flicker
  const n = Math.sin(t*11.3)*0.5+Math.sin(t*23.7)*0.3+Math.sin(t*5.1)*0.2;
  fireLight.intensity = 42*(1+0.28*n) + Math.random()*4;
  fireLight.position.y = 0.95 + Math.sin(t*9)*0.05;
  // glow sprite scale
  glow.scale.setScalar(1.6+0.25*Math.sin(t*7.7)+0.1*n);
  // sparks update
  ...
  // stars twinkle
  starMat.opacity = 0.75+0.2*Math.sin(t*0.8);
  // controls
  controls.update();
  renderer.render(scene, camera);
}
```

Flame animation details:
```
flames.forEach((f,i)=>{
  const s = 1 + 0.18*Math.sin(t*(8+i*1.7) + i*2.1) + 0.08*Math.sin(t*17+i);
  f.scale.set(s*1.0, 1+0.35*Math.sin(t*(6.5+i)+i*1.3)+0.15*Math.sin(t*12.3), s);
  f.rotation.y = t*(1.5+i*0.4) + i;
  f.position.x = Math.sin(t*3+i*2)*0.03;
  f.material.opacity = 0.75 + 0.2*Math.sin(t*10+i*2.7);
});
```
Materials per flame (clone) so opacity independent.

Cone origin is at center of height, so position cone at y = h/2. Set each flame mesh position.y = base + h/2 where base=0.15. When scaling y, it scales around center — flame will grow both up and down; better to anchor bottom: set geometry translated so base at 0: geo.translate(0, h/2, 0) then mesh position.y = baseY, scale grows upward. Do that.

Outer flame color 0xff3300 opacity .55, mid 0xff7a00 .7, core 0xffdd66 .9. Additive. depthWrite false. Also each flame slightly rotated x random for organic.

Also a fourth small outer red cone bigger/wider low opacity.

Ground geometry: PlaneGeometry(140,140,48,48), rotateX(-PI/2). Displace:
```
const pos = geo.attributes.position;
for i: x=pos.getX(i), z=pos.getZ(i); r=hypot(x,z);
 let h = (Math.sin(x*0.09)*Math.cos(z*0.08)*1.4 + Math.sin(x*0.23+2.0)*Math.cos(z*0.19+1.0)*0.5);
 const flat = THREE.MathUtils.smoothstep(r, 4, 9); // 0 near center, 1 far
 pos.setY(i, h*flat);
 // color
 const c = ...;
```
smoothstep(x, min, max) in three MathUtils: smoothstep(x, min, max). Yes.

Colors: near center dark scorched: mix between dirt #2a2019 and grass #1c2f1c by noise; plus height tint. Use Color lerp:
```
const grass = new Color(0x24401f), dirt = new Color(0x3a2c1c), dark = new Color(0x171310);
let c = grass.clone().lerp(dirt, Math.random()*0.5);
if (r<3.2) c.lerp(dark, 1 - r/3.2 * 0.85);
else c.lerp(dirt, ...)? 
```
Also add slight random brightness. Set color attribute. Material vertexColors:true, flatShading:true, roughness 1.

Wait flatShading with vertex colors: computed normals per face — fine.

receiveShadow on ground.

Trees placement: avoid center r>7.5, also avoid being too close to each other? Random may cluster; acceptable. Radius between 8 and 34, 34 trees. Also scale 0.7-1.6.

Tree function:
```
function makeTree(){
  const g = new THREE.Group();
  const h = 1.6+Math.random()*1.4;
  const trunk = new Mesh(trunkGeo, trunkMat);
  trunk.scale.y = h; // trunkGeo height 1 centered? define trunkGeo = CylinderGeometry(0.14,0.22,1,5) then position y=h/2
  ...
}
```
Simpler: build with fixed proportions and scale group.

trunk: CylinderGeometry(0.16, 0.26, 2.2, 5), position y 1.1.
foliage cones: ConeGeometry(1.3, 2.2, 6) at y 2.6; ConeGeometry(1.0,1.9,6) at y 3.7; ConeGeometry(0.65,1.6,6) at y 4.7. Group scale s = 0.75+rand*0.9. castShadow all meshes.

Foliage materials: array of 3: 0x1d3a24, 0x24482a, 0x183020 with flatShading. MeshStandardMaterial flatShading.

Trunk mat: 0x3a2a1c flatShading.

Positions: 
```
let placed=0, attempts=0;
while(placed<34 && attempts<500){ attempts++; const a=Math.random()*Math.PI*2; const r=8+Math.random()*26; const x=Math.cos(a)*r, z=Math.sin(a)*r; if(Math.hypot(x,z)<7.5) continue; ... place }
```
r up to 34 < ground 70 fine, fog will fade outer ones (fog at 34: (0.02*34)^2=0.46, factor 0.37 — hmm not very faded; distant trees at 34 only 37% fogged. Increase density to 0.028: at 34, (0.952)^2=0.906, factor 0.595. At 60 (edge of visible ground): (1.68)^2=2.82, factor 0.94 — ground edge heavily fogged. Good: density 0.026. Let me pick 0.025. Trees at 8-34: at 8: (0.2)^2=0.04→4% fog; at 20: 0.25→22%; at 34: 0.72→51%. Nice fade. Fog color 0x0d1424 matches horizon.

Hmm but fire light distance 40 lights trees within 40 — fine.

Moon directional light: color 0x9db4ff intensity 0.35, position (-80, 120, -160). No shadow (perf) — or enable shadow for moonlit look? Point light already casts. Keep moon no shadow.

Hemisphere: sky 0x2a3a5f, ground 0x0c0f0a, intensity 0.5. Plus small AmbientLight 0x111722 0.4? Hemisphere enough.

Tone mapping: renderer.toneMapping = ACESFilmicToneMapping, exposure 1.1 — makes fire glow nicer. outputColorSpace default SRGB in r170.

Shadow settings on fireLight: castShadow true; shadow.mapSize.set(1024,1024); shadow.camera.near=0.2; shadow.camera.far=30; shadow.bias=-0.004; shadow.radius maybe (PCFSoft ignores radius). Set camera left/right/top/bottom default ±? PointLight shadow uses cube camera; near/far enough.

Camera: PerspectiveCamera(55, aspect, 0.1, 1500). position (9, 5.5, 11).

Controls target (0, 1.1, 0).

UI overlay: 
```
<div id="hud"><h1>Campfire Night</h1><p>drag to orbit · scroll to zoom</p></div>
```
styled.

Also handle visibility of everything at first paint — yes, animate starts immediately.

Check OrbitControls import path: 'three/addons/controls/OrbitControls.js' — with importmap "three/addons/": ".../examples/jsm/" so resolves. Good.

Potential console error sources: none expected. Ensure no `useLegacyLights` (removed). Ensure physically correct: in r170, renderer.useLegacyLights default false and removed? In r165 default changed; r170 `useLegacyLights` removed entirely I think (deprecated r155, removed r165). Don't set it. Light intensities: PointLight with decay=2 physical units — intensity 40 gives strong local light. Directional 0.35 fine. Hemisphere 0.5 fine.

Let me also double-check `THREE.MathUtils.smoothstep(x, min, max)` signature — yes smoothstep(x, min, max).

Sparks color attribute: PointsMaterial vertexColors true uses color attribute multiplied with material color (white). Additive blending: final = src*srcAlpha? AdditiveBlending in three: srcRGB * srcAlpha? Actually AdditiveBlending = src * srcAlpha + dst (ONE, ONE? ). In three.js AdditiveBlending: blendSrc = SrcAlphaFactor, blendDst = OneFactor. With transparent true, alpha from material opacity (global) — per-point alpha not available; so fade via color to black works since src color * alpha added. opacity 1. So set colors to black when dead. Good.

Points frustumCulled = false to avoid disappearing when bounding sphere off — set geometry.boundingSphere manually or frustumCulled=false on points. Do points.frustumCulled = false.

Stars: also frustumCulled false or big bounding sphere; set frustumCulled=false.

Moon glow sprite texture via canvas:
```
function makeGlowTexture(){ const c=document.createElement('canvas'); c.width=c.height=128; const g=c.getContext('2d'); const grad=g.createRadialGradient(64,64,0,64,64,64); grad.addColorStop(0,'rgba(255,250,230,1)'); grad.addColorStop(0.25,'rgba(255,240,200,0.55)'); grad.addColorStop(1,'rgba(255,230,180,0)'); g.fillStyle=grad; g.fillRect(0,0,128,128); const tex=new THREE.CanvasTexture(c); return tex; }
```
CanvasTexture inline — allowed (no external).

Fire glow sprite similar orange.

Moon mesh: MeshBasicMaterial({color:0xf5f0dc, fog:false}) sphere 13 at (-180, 210, -300) normalized * 520 maybe. Let's place at direction: dir = new Vector3(-0.35, 0.5, -0.55).normalize().multiplyScalar(520). Sphere radius 16. Glow sprite at same pos scale 140, opacity 0.9.

Stars on sphere radius 560, only y>0? Full sphere fine (below horizon hidden by ground? ground plane finite 140 — stars below horizon visible beyond ground edge... dome horizon color covers; stars below horizon would show beyond ground plane edge at distance 70+ where fog covers ground to horizon color; star at y<0 at radius 560 would be below dome horizon line — visible against fog color, looks odd. Restrict stars to y > -0.05? Just y>0.02 hemisphere plus a few near horizon. Fine: generate with y = Math.abs(y)*0.9+... simpler: sample direction, if y<0.03 set y=0.03+rand*... Just do: y = 0.05 + Math.random()*0.95 normalized? I'll generate unit vectors and if v.y < 0.05, v.y = 0.05 + Math.random()*0.1 then normalize. OK.

Star colors: mostly white, some #bcd0ff, some #ffe9c4.

Now HUD text: also mention "Click or press a key to start"? Not needed — not a game; orbit works on click/drag directly. The instruction about keyboard applies if demo uses keyboard; we don't. But maybe add keyboard arrows to orbit? OrbitControls has listenToKeyEvents for pan with arrow keys. Not necessary. Skip to avoid needing click-focus message. Mouse works without iframe focus? Mouse events on iframe work when hovering; the note says keyboard events only after click — mouse is fine. OK.

Write final HTML. Also add `<meta viewport>`. Title.

Resize: also update on orientationchange? resize covers.

Let me also ensure no horizontal scrollbar: body overflow hidden.

Edge: `renderer.setSize(window.innerWidth, window.innerHeight)` and CSS canvas display block.

Shadow acne: bias -0.002, normalBias 0.02 maybe. Use shadow.normalBias = 0.03.

Let me now write flame geometry: 
```
function flameGeo(r, h, seg){ const g = new THREE.ConeGeometry(r, h, seg, 1); g.translate(0, h/2, 0); return g; }
```
Flames:
```
const flameDefs = [
 {r:0.34, h:1.05, c:0xff3300, o:0.55},
 {r:0.25, h:0.85, c:0xff7b00, o:0.7},
 {r:0.16, h:0.6, c:0xffc23d, o:0.85},
 {r:0.09, h:0.42, c:0xfff3b0, o:0.95},
];
```
MeshBasicMaterial({color, transparent:true, opacity, blending:AdditiveBlending, depthWrite:false, side:DoubleSide}). Position group at (0, 0.28, 0). Each flame mesh y=0 (geometry base at 0). Random rotation.z slight tilt.

Scale animation anchored at base since geometry translated — good.

Also flames castShadow? Basic material with shadows — flame casting shadow would be weird; set castShadow false (default false). Light comes from point light at center anyway.

Logs teepee height 0.62 apex; flames base 0.28 inside. Good.

Ember core: sphere r 0.16 at y 0.32, MeshBasicMaterial 0xff5500 additive, pulse scale. Plus point light at 0.8.

Stones ring: 8 stones at r 0.62 angle i/8*2PI + jitter, scale random, y = 0.5*scaleY.

Fallen logs: 3 logs lying: cylinder rotated to horizontal at random pos r 2.2-3.5, rotation.y random, y = radius 0.09. Slight tilt. castShadow.

Scattered rocks: 6 at r 3.5-9, dodecahedron scaled.

Also a couple of bushes? skip.

Now sparks update code:

```
const SPARKS = 90;
const sPos = new Float32Array(SPARKS*3), sCol = new Float32Array(SPARKS*3);
const sVel = [], sLife = [], sMax = [];
for(...){ resetSpark(i, true); }
function resetSpark(i, init){
  const a = Math.random()*Math.PI*2, r = Math.random()*0.22;
  sPos[i*3] = Math.cos(a)*r; sPos[i*3+1] = init? 0.6+Math.random()*2.5 : 0.55+Math.random()*0.25; sPos[i*3+2]=Math.sin(a)*r;
  sVel[i] = new THREE.Vector3((Math.random()-0.5)*0.5, 0.9+Math.random()*1.4, (Math.random()-0.5)*0.5);
  sMax[i] = 0.9+Math.random()*1.6; sLife[i] = init? Math.random()*sMax[i] : sMax[i];
}
```
Update per frame:
```
for i: sLife[i]-=dt; if(sLife[i]<=0){resetSpark(i,false);} 
 const p = sVel[i]; sPos[i*3]+=p.x*dt; ... p.y -= 0.25*dt? sparks decelerate: p.y = Math.max(0.3, p.y - 0.5*dt); wobble: p.x += Math.sin(t*4+i)*dt*0.4; p.z += Math.cos(t*3.3+i*1.7)*dt*0.4;
 const f = Math.max(sLife[i]/sMax[i],0); const b = f*f; 
 sCol[i*3] = 1.0*b; sCol[i*3+1] = 0.55*b*f? let's: r= b, g=0.6*b*f, bl=0.15*b*f*f → yellow→red→black.
```
Set needsUpdate.

Points size 0.14, sizeAttenuation true.

Also maybe smoke particles? skip.

Now, camera min polar 0.12, max polar Math.PI*0.495. minDistance 4, maxDistance 60. autoRotate? Could enable slow auto-rotate for demo liveliness — but user can drag; autoRotate pauses on interaction? It continues unless disabled on 'start' event. Add gentle autoRotate speed 0.6, and disable on user start, re-enable after idle 8s? Nice touch: 
```
controls.autoRotate = true; controls.autoRotateSpeed = 0.8;
controls.addEventListener('start', ()=>{ controls.autoRotate=false; clearTimeout(idleT); });
controls.addEventListener('end', ()=>{ idleT=setTimeout(()=>controls.autoRotate=true, 6000); });
```
That makes first seconds show motion. Good.

Clamp target in change event.

DPR: min(devicePixelRatio, 2).

Now assemble full file carefully. Watch out: importmap must be exactly as given. Script type="module" after it.

Code:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Night Campfire — Low-Poly Forest</title>
<style>
  html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#070b16;}
  canvas{display:block;}
  #hud{position:fixed;top:14px;left:16px;color:#ffd9a8;pointer-events:none;font-family:Georgia,'Times New Roman',serif;text-shadow:0 1px 6px rgba(0,0,0,.8);user-select:none;}
  #hud h1{margin:0;font-size:18px;font-weight:normal;letter-spacing:2px;}
  #hud p{margin:2px 0 0;font-size:11px;opacity:.75;letter-spacing:1px;font-family:Verdana,sans-serif;}
  #vignette{position:fixed;inset:0;pointer-events:none;background:radial-gradient(ellipse at 50% 45%, transparent 55%, rgba(0,0,10,.55) 100%);}
</style>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>
</head>
<body>
<div id="hud"><h1>✦ NIGHT CAMPFIRE ✦</h1><p>drag to orbit &nbsp;·&nbsp; scroll to zoom</p></div>
<div id="vignette"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
...
</script>
</body>
</html>
```

JS details:

```js
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0d1424, 0.026);

const camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, 0.1, 1500);
camera.position.set(9, 5.2, 11);

const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);
```

Sky dome, stars, moon, lights, ground, fire, trees, rocks/logs, sparks, controls, animate, resize.

Sky dome:
```js
const sky = new THREE.Mesh(
  new THREE.SphereGeometry(700, 32, 16),
  new THREE.ShaderMaterial({
    side: THREE.BackSide, depthWrite:false,
    vertexShader:`varying vec3 vP; void main(){ vP=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
    fragmentShader:`varying vec3 vP; void main(){ float h=normalize(vP).y; vec3 c=mix(vec3(0.055,0.075,0.13), vec3(0.006,0.01,0.028), smoothstep(-0.05,0.55,h)); gl_FragColor=vec4(c,1.0);}`
  })
);
sky.renderOrder = -10; scene.add(sky);
```
Fog color should match horizon 0x0d1321 ≈ (0.051,0.075,0.129). Set fog 0x0e1522? Let's compute: 0.055*255=14 (0x0e), 0.075*255=19 (0x13), 0.13*255=33 (0x21) → 0x0e1321. Use fog color 0x0e1321, density 0.026.

Stars:
```js
const starCount=900, sp=new Float32Array(starCount*3), sc=new Float32Array(starCount*3);
const cA=new THREE.Color(0xffffff), cB=new THREE.Color(0xaFC4ff), cC=new THREE.Color(0xffe6b8);
for(let i=0;i<starCount;i++){
  const v=new THREE.Vector3().randomDirection();
  if(v.y<0.04) v.y=0.04+Math.random()*0.08;
  v.normalize().multiplyScalar(620);
  sp.set([v.x,v.y,v.z], i*3);
  const c = Math.random()<0.7?cA:(Math.random()<0.5?cB:cC);
  const b=0.55+Math.random()*0.45;
  sc.set([c.r*b,c.g*b,c.b*b], i*3);
}
const starGeo=new THREE.BufferGeometry();
starGeo.setAttribute('position', new THREE.BufferAttribute(sp,3));
starGeo.setAttribute('color', new THREE.BufferAttribute(sc,3));
const starMat=new THREE.PointsMaterial({size:2.2, sizeAttenuation:false, vertexColors:true, transparent:true, opacity:0.9, depthWrite:false, fog:false});
const stars=new THREE.Points(starGeo, starMat); stars.frustumCulled=false; scene.add(stars);
```
Note Vector3.randomDirection exists in r170? randomDirection added r131? Yes exists. But randomDirection can give y negative; handled.

Moon:
```js
const moonDir = new THREE.Vector3(-0.38, 0.42, -0.55).normalize();
const moonPos = moonDir.clone().multiplyScalar(640);
const moon = new THREE.Mesh(new THREE.SphereGeometry(17, 24, 16), new THREE.MeshBasicMaterial({color:0xf7f2df, fog:false}));
moon.position.copy(moonPos); scene.add(moon);
// glow
const glowTex = makeRadialTexture('rgba(255,246,220,1)','rgba(255,240,200,0.35)','rgba(255,236,190,0)');
const moonGlow = new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex, blending:THREE.AdditiveBlending, transparent:true, opacity:0.85, depthWrite:false, fog:false}));
moonGlow.position.copy(moonPos); moonGlow.scale.setScalar(150); scene.add(moonGlow);
```
makeRadialTexture:
```js
function makeRadialTexture(inner, mid, outer){
  const c=document.createElement('canvas'); c.width=c.height=128;
  const g=c.getContext('2d');
  const grad=g.createRadialGradient(64,64,2,64,64,64);
  grad.addColorStop(0,inner); grad.addColorStop(0.35,mid); grad.addColorStop(1,outer);
  g.fillStyle=grad; g.fillRect(0,0,128,128);
  return new THREE.CanvasTexture(c);
}
```

Lights:
```js
scene.add(new THREE.HemisphereLight(0x33456e, 0x0b0e08, 0.55));
const moonLight = new THREE.DirectionalLight(0x8fa8e0, 0.5);
moonLight.position.copy(moonDir.clone().multiplyScalar(200));
scene.add(moonLight);
const fireLight = new THREE.PointLight(0xff7a2a, 60, 45, 2);
fireLight.position.set(0, 1.0, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024,1024);
fireLight.shadow.camera.near = 0.2;
fireLight.shadow.camera.far = 30;
fireLight.shadow.bias = -0.004;
fireLight.shadow.normalBias = 0.03;
scene.add(fireLight);
```
Intensity 60 with decay 2: at distance 1, 60; at 5, 60/25=2.4 — visible. Maybe base 80. Flicker around 70±25%. Let's base 70.

Ground:
```js
const groundGeo = new THREE.PlaneGeometry(160,160,52,52);
groundGeo.rotateX(-Math.PI/2);
{
  const p = groundGeo.attributes.position;
  const colors = new Float32Array(p.count*3);
  const grass=new THREE.Color(0x2c4a26), dirt=new THREE.Color(0x4a3826), scorch=new THREE.Color(0x14100c), moss=new THREE.Color(0x1c331e);
  for(let i=0;i<p.count;i++){
    const x=p.getX(i), z=p.getZ(i), r=Math.hypot(x,z);
    let h=(Math.sin(x*0.09)*Math.cos(z*0.08))*1.5 + Math.sin(x*0.21+2.0)*Math.cos(z*0.17+1.0)*0.55 + Math.sin(x*0.45+0.7)*Math.cos(z*0.4+2.2)*0.18;
    const flat=THREE.MathUtils.smoothstep(r,3.5,10);
    p.setY(i, h*flat);
    const c = grass.clone().lerp(Math.random()<0.5?moss:dirt, Math.random()*0.7);
    c.offsetHSL(0,0,(Math.random()-0.5)*0.04);
    if(r<4.5) c.lerp(scorch, (1-r/4.5)*0.9);
    colors.set([c.r,c.g,c.b], i*3);
  }
  groundGeo.setAttribute('color', new THREE.BufferAttribute(colors,3));
  groundGeo.computeVertexNormals();
}
const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({vertexColors:true, flatShading:true, roughness:1, metalness:0}));
ground.receiveShadow = true;
scene.add(ground);
```
Note smoothstep(r, 3.5, 10): MathUtils.smoothstep(x, min, max) — returns 0..1. Good.

Scorch disc:
```js
const scorch = new THREE.Mesh(new THREE.CircleGeometry(1.15, 24), new THREE.MeshBasicMaterial({color:0x0a0806, transparent:true, opacity:0.75, depthWrite:false}));
scorch.rotation.x=-Math.PI/2; scorch.position.y=0.02; scorch.renderOrder=1; scene.add(scorch);
```
Hmm depthWrite false with renderOrder above ground; ground at y=0 with displaced center flat=0 → y=0 near center. scorch at 0.02 fine. polygonOffset alternative; fine.

Fire group:
```js
const fire = new THREE.Group(); scene.add(fire);
// stone ring
const stoneGeo = new THREE.DodecahedronGeometry(1, 0);
const stoneMat = new THREE.MeshStandardMaterial({color:0x8a8d94, flatShading:true, roughness:0.9});
for(let i=0;i<9;i++){
  const a=i/9*Math.PI*2 + Math.random()*0.4;
  const r=0.62+Math.random()*0.12;
  const s=0.14+Math.random()*0.16;
  const st=new THREE.Mesh(stoneGeo, stoneMat);
  st.position.set(Math.cos(a)*r, s*0.45, Math.sin(a)*r);
  st.scale.set(s*(0.8+Math.random()*0.5), s*(0.5+Math.random()*0.4), s*(0.8+Math.random()*0.5));
  st.rotation.set(Math.random()*3, Math.random()*3, Math.random()*3);
  st.castShadow=st.receiveShadow=true;
  fire.add(st);
}
// teepee logs
const barkMat = new THREE.MeshStandardMaterial({color:0x5a4028, flatShading:true, roughness:1});
for(let i=0;i<6;i++){
  const a=i/6*Math.PI*2 + 0.26;
  const base=new THREE.Vector3(Math.cos(a)*0.5, 0, Math.sin(a)*0.5);
  const apex=new THREE.Vector3(0, 0.72, 0);
  const dir=apex.clone().sub(base); const len=dir.length();
  const log=new THREE.Mesh(new THREE.CylinderGeometry(0.07,0.095,len,6), barkMat);
  log.position.copy(base).add(apex).multiplyScalar(0.5);
  log.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), dir.normalize());
  log.castShadow=log.receiveShadow=true;
  fire.add(log);
}
```
Wait apex y 0.72 with base r 0.5: len = sqrt(0.25+0.518)=~0.876. Logs lean nicely.

Flames:
```js
const flames=[];
const flameDefs=[ {r:0.36,h:1.15,c:0xff2e00,o:0.5}, {r:0.27,h:0.92,c:0xff7300,o:0.65}, {r:0.18,h:0.66,c:0xffb62e,o:0.8}, {r:0.10,h:0.45,c:0xfff0b8,o:0.95} ];
for(const d of flameDefs){
  const g=new THREE.ConeGeometry(d.r, d.h, 7, 1);
  g.translate(0, d.h/2, 0);
  const m=new THREE.MeshBasicMaterial({color:d.c, transparent:true, opacity:d.o, blending:THREE.AdditiveBlending, depthWrite:false, side:THREE.DoubleSide});
  const f=new THREE.Mesh(g,m);
  f.position.y=0.22; f.rotation.y=Math.random()*6;
  fire.add(f); flames.push(f);
}
// ember core
const ember=new THREE.Mesh(new THREE.IcosahedronGeometry(0.17,0), new THREE.MeshBasicMaterial({color:0xff6a00, blending:THREE.AdditiveBlending, transparent:true, opacity:0.9, depthWrite:false}));
ember.position.y=0.3; fire.add(ember);
// glow sprite
const fireGlow=new THREE.Sprite(new THREE.SpriteMaterial({map:makeRadialTexture('rgba(255,190,90,0.9)','rgba(255,120,40,0.35)','rgba(255,80,20,0)'), blending:THREE.AdditiveBlending, transparent:true, opacity:0.9, depthWrite:false}));
fireGlow.position.y=0.7; fireGlow.scale.setScalar(2.2); fire.add(fireGlow);
```

Trees:
```js
const trunkGeo=new THREE.CylinderGeometry(0.16,0.28,2.4,5);
const trunkMat=new THREE.MeshStandardMaterial({color:0x4a3423, flatShading:true, roughness:1});
const coneGeos=[new THREE.ConeGeometry(1.35,2.3,6), new THREE.ConeGeometry(1.0,2.0,6), new THREE.ConeGeometry(0.62,1.7,6)];
const folMats=[0x1e3d26,0x274a2c,0x183421].map(c=>new THREE.MeshStandardMaterial({color:c, flatShading:true, roughness:1}));
const TREE_COUNT=34;
for(let i=0;i<TREE_COUNT;i++){
  let x,z,r;
  do { const a=Math.random()*Math.PI*2; r=8.5+Math.random()*26; x=Math.cos(a)*r; z=Math.sin(a)*r; } while(Math.hypot(x,z)<7.5);
  const t=new THREE.Group();
  const trunk=new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y=1.2; trunk.castShadow=trunk.receiveShadow=true; t.add(trunk);
  const fm=folMats[i%folMats.length];
  const ys=[2.5,3.6,4.6];
  coneGeos.forEach((g,j)=>{ const m=new THREE.Mesh(g,fm); m.position.y=ys[j]; m.castShadow=m.receiveShadow=true; t.add(m); });
  const s=0.75+Math.random()*0.95;
  t.scale.set(s,s*(0.9+Math.random()*0.3),s);
  t.position.set(x,0,z);
  t.rotation.y=Math.random()*Math.PI*2;
  scene.add(t);
}
```
Trees cast shadow — 34 trees × 4 meshes into 1024 shadow map from point light (cube 6 renders!) Point light shadow = cube shadow map renders scene 6 times. 34 trees*4 + rocks etc ≈ 160 casters ×6 = ~1000 draw calls for shadow — might be heavy but okay on desktop; could reduce: shadow camera renders only near stuff? Point light shadow cube renders whole scene each face. 160 casters fine actually, each face ~160 draws → 960 draws + main 200 → ok at 60fps on typical GPU. Fine.

Alternatively reduce trees casting: all cast. OK.

Scattered rocks & fallen logs:
```js
for(let i=0;i<6;i++){
  const a=Math.random()*Math.PI*2, r=3.2+Math.random()*6.5;
  const s=0.25+Math.random()*0.5;
  const rock=new THREE.Mesh(new THREE.DodecahedronGeometry(s,0), stoneMat);
  rock.position.set(Math.cos(a)*r, s*0.4, Math.sin(a)*r);
  rock.rotation.set(Math.random()*3,Math.random()*3,Math.random()*3);
  rock.scale.y=0.6+Math.random()*0.4;
  rock.castShadow=rock.receiveShadow=true;
  scene.add(rock);
}
const logGeo=new THREE.CylinderGeometry(0.09,0.11,1.6,6);
for(let i=0;i<4;i++){
  const a=Math.random()*Math.PI*2, r=2.2+Math.random()*2.2;
  const log=new THREE.Mesh(logGeo, barkMat);
  log.position.set(Math.cos(a)*r, 0.1, Math.sin(a)*r);
  log.rotation.z=Math.PI/2; log.rotation.y=Math.random()*Math.PI*2;
  // combine: set rotation order... simpler: log.rotation.set(Math.PI/2, 0, Math.random()*Math.PI*2)? 
  log.castShadow=log.receiveShadow=true;
  scene.add(log);
}
```
Rotation: default order XYZ; rotation.z=PI/2 then rotation.y — Euler applied as R = Rx*Ry*Rz? Three.js Euler XYZ means rotation matrix Rz then Ry then Rx applied in order... Actually applies in order Z, then Y, then X? The matrix is composed as R = R_x * R_y * R_z? In three.js, Euler order 'XYZ' means the object is rotated around X, then Y, then Z in local... The composed matrix is Rz*Ry*Rx? Ugh. Simple: set rotation.set(0, Math.random()*PI*2, Math.PI/2, 'YXZ')? Just do: log.rotation.order='YXZ'; log.rotation.y=a; log.rotation.z=PI/2 — with order YXZ, Y applied first then X then Z? Matrix = RY * RX * RZ. With rotation.x=0, rotation.y=a, rotation.z=PI/2: R = Ry(a) * Rz(PI/2). Cylinder axis Y → Rz(PI/2) maps Y to X (horizontal), then Ry(a) rotates around world Y → horizontal log at angle a. 

So: `log.rotation.order='YXZ'; log.rotation.y=Math.random()*Math.PI*2; log.rotation.z=Math.PI/2;` Good.

Sparks as planned.

Controls:
```js
const controls=new OrbitControls(camera, renderer.domElement);
controls.target.set(0,1.1,0);
controls.enableDamping=true; controls.dampingFactor=0.06;
controls.minDistance=4; controls.maxDistance=60;
controls.minPolarAngle=0.12;
controls.maxPolarAngle=Math.PI*0.495; // stay above ground
controls.autoRotate=true; controls.autoRotateSpeed=0.9;
let idleTimer=null;
controls.addEventListener('start',()=>{controls.autoRotate=false; if(idleTimer)clearTimeout(idleTimer);});
controls.addEventListener('end',()=>{ if(idleTimer)clearTimeout(idleTimer); idleTimer=setTimeout(()=>{controls.autoRotate=true;},6000); });
controls.addEventListener('change',()=>{
  const t=controls.target;
  t.y=THREE.MathUtils.clamp(t.y,0.4,8);
  const hr=Math.hypot(t.x,t.z);
  if(hr>12){ t.x*=12/hr; t.z*=12/hr; }
});
```

Resize:
```js
addEventListener('resize',()=>{
  camera.aspect=innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth,innerHeight);
});
```

Animate:
```js
const clock=new THREE.Clock();
const sparkData=[];
function resetSpark(i, scattered){...}
init sparks...
function animate(){
  requestAnimationFrame(animate);
  const dt=Math.min(clock.getDelta(),0.05);
  const t=clock.elapsedTime;
  // flames
  for(let i=0;i<flames.length;i++){
    const f=flames[i];
    const sway=Math.sin(t*(7+i*1.9)+i*2.4);
    f.scale.set(1+0.16*sway+0.06*Math.sin(t*15+i), 1+0.32*Math.sin(t*(6+i*1.3)+i*1.7)+0.18*Math.sin(t*11.7+i*3), 1+0.16*Math.cos(t*(8.3+i)+i));
    f.rotation.y=t*(1.2+i*0.35)+i*1.3;
    f.position.x=Math.sin(t*2.6+i*2.1)*0.035;
    f.position.z=Math.cos(t*2.2+i*1.4)*0.035;
    f.material.opacity=(flameDefs[i].o)*(0.8+0.25*Math.sin(t*12+i*4.2));
  }
  ember.scale.setScalar(1+0.25*Math.sin(t*9.3));
  // light flicker
  const flick=Math.sin(t*11.3)*0.5+Math.sin(t*23.7+1.3)*0.3+Math.sin(t*5.1+4.0)*0.2;
  fireLight.intensity=70*(1+0.22*flick)+Math.random()*5;
  fireLight.position.y=0.95+0.06*Math.sin(t*8.8);
  fireGlow.material.opacity=0.75+0.2*flick;
  fireGlow.scale.setScalar(2.0+0.35*flick+0.1*Math.sin(t*13.7));
  // sparks
  for(let i=0;i<SPARKS;i++){
    sLife[i]-=dt;
    if(sLife[i]<=0) resetSpark(i,false);
    const v=sVel[i];
    v.y=Math.max(0.35, v.y-0.35*dt);
    v.x+=Math.sin(t*4.1+i*1.3)*dt*0.5;
    v.z+=Math.cos(t*3.3+i*2.1)*dt*0.5;
    sPos[i*3]+=v.x*dt; sPos[i*3+1]+=v.y*dt; sPos[i*3+2]+=v.z*dt;
    const f=Math.max(sLife[i]/sMax[i],0), b=f*f;
    sCol[i*3]=b; sCol[i*3+1]=0.55*b*f; sCol[i*3+2]=0.12*b*f*f;
  }
  sparkGeo.attributes.position.needsUpdate=true;
  sparkGeo.attributes.color.needsUpdate=true;
  starMat.opacity=0.75+0.18*Math.sin(t*0.9)+0.07*Math.sin(t*2.3);
  controls.update();
  renderer.render(scene,camera);
}
animate();
```

Spark init: build arrays before animate; resetSpark uses sPos etc.

```js
const SPARKS=90;
const sPos=new Float32Array(SPARKS*3), sCol=new Float32Array(SPARKS*3);
const sVel=new Array(SPARKS), sLife=new Float32Array(SPARKS), sMax=new Float32Array(SPARKS);
function resetSpark(i, scattered){
  const a=Math.random()*Math.PI*2, r=Math.random()*0.24;
  sPos[i*3]=Math.cos(a)*r;
  sPos[i*3+1]=scattered? 0.5+Math.random()*3.2 : 0.5+Math.random()*0.3;
  sPos[i*3+2]=Math.sin(a)*r;
  sVel[i]=new THREE.Vector3((Math.random()-0.5)*0.55, 0.9+Math.random()*1.5, (Math.random()-0.5)*0.55);
  sMax[i]=0.8+Math.random()*1.7;
  sLife[i]=scattered? Math.random()*sMax[i] : sMax[i];
}
for(let i=0;i<SPARKS;i++) resetSpark(i,true);
const sparkGeo=new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sPos,3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sCol,3));
const sparks=new THREE.Points(sparkGeo, new THREE.PointsMaterial({size:0.13, vertexColors:true, transparent:true, opacity:1, blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true}));
sparks.frustumCulled=false;
scene.add(sparks);
```

Note: resetSpark assigns sVel[i]=new Vector3 each reset — allocation per reset per frame for dead sparks; only a few per frame, fine.

Mobile 375px: HUD small, fine. Camera fov fine.

One more check: `THREE.MathUtils.smoothstep(r, 3.5, 10)` — signature smoothstep(x, min, max). Yes.

PointsMaterial fog:false — property exists on Material (fog boolean). Yes.

SpriteMaterial fog property exists too. Yes.

ShaderMaterial sky — fog doesn't apply since no fog chunks. Good.

CircleGeometry scorch with MeshBasicMaterial transparent — renders after ground due renderOrder 1. Ground renderOrder default 0. OK. Actually transparent objects render after opaque anyway; renderOrder within transparent sorted by depth. Fine, but depthWrite false and position y 0.02 above ground → visible. OK.

Check: fireLight distance 45, decay 2 — at ground r=8: intensity 70/64≈1.1, times albedo — visible warm pool. Good. Trees at 10-20 get 70/100..70/400 = 0.7..0.17 — dim rim, nice.

Hemisphere 0.55 + moon dir 0.5 gives night visibility. Exposure 1.15.

Shadow camera for point light: default cube camera near/far set. far=30 covers clearing. Trees beyond 30 from light not shadowed — fine.

One concern: point light shadow with 6 faces each rendering ~180 casters — ok.

Also set `renderer.domElement` style via CSS canvas display block; body overflow hidden prevents scrollbar.

autoRotate rotates camera around target — with maxPolarAngle limit, fine.

Double-check OrbitControls r170 API: maxPolarAngle etc. yes. 'start'/'end' events yes.

Also `controls.enablePan` default true; change-clamp handles target. Good.

Check flameDefs referenced in animate — define in outer scope. Yes.

Edge: `starMat.opacity` twinkle — fine.

Also ensure `innerWidth/innerHeight` used (window props). OK.

Potential issue: `Vector3.randomDirection()` exists r170? Added in r131? Let me recall — `randomDirection()` was added in r142? I believe it exists in recent versions (r170 yes, docs list it). Safe alternative: manual. I'll use manual to be safe:
```js
const v=new THREE.Vector3(Math.random()*2-1, Math.random()*2-1, Math.random()*2-1);
if(v.lengthSq()<0.001) v.set(0,1,0);
```
Then normalize. Safer.

Also `MathUtils.clamp` exists. Yes.

ConeGeometry(radius, height, radialSegments, heightSegments) — fine.

DodecahedronGeometry(radius, detail) fine.

Let me also add subtle "heat glow" light on ground? no.

Add a few distant tree silhouettes beyond fog? The 34 trees up to r=34.5; maybe add ring of extra far trees at r 40-55 darker to fill horizon: e.g., 20 more trees at r 38-55, they'll be heavily fogged silhouettes. Requirement ≥30; adding more fine. Let's include them (total 54). They'll be mostly fog — nice depth. Their shadows: point light far=30 won't include them; still rendered in main pass. OK.

Actually point light shadow camera far 30 — objects beyond don't cast in shadow map (clipped), fine.

Write final code now. Also ensure exactly one fenced code block, no text outside.

HUD font: Georgia serif fine.

Also add `touch-action: none` on canvas? OrbitControls sets it. Fine.

Let me write it fully and carefully check syntax.

One more: `scene.fog` affects PointsMaterial stars (fog:false set), moon (fog:false), sprites (fog:false), sky shader (no fog). Ground/trees fogged. Good.

Camera far 1500 > sky 700. Good.

Final check on import map exact string: 
`<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>` — copy exactly.

Write it.