Загрузка данных


<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aim Trainer 3D</title>
<style>
  *{scrollbar-width:none;-ms-overflow-style:none}
  *::-webkit-scrollbar{display:none;width:0;height:0}
  :root{
    --c1:#00e5ff;--c2:#00e5ff;
    --c1-rgb:0,229,255;--c2-rgb:0,229,255;
    --text:#e8eef7;
    --menu-bg: radial-gradient(ellipse at 50% 40%,#140a26 0%,#0b0714 55%,#05030a 100%);
    --ui-bg-1-rgb: 30,20,52;
    --ui-bg-2-rgb: 14,10,26;
    --ui-border: #2a2140;
  }
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{width:100%;height:100%;overflow:hidden;background:#07060f;
    font-family:'Segoe UI',system-ui,-apple-system,sans-serif;color:var(--text);user-select:none}
  canvas{display:block}
  .hidden{display:none !important}
  #ui{position:fixed;inset:0;z-index:20;pointer-events:none}
  .screen{position:absolute;inset:0;display:flex;flex-direction:column;
    align-items:center;justify-content:center;gap:18px;pointer-events:auto;
    background:var(--menu-bg);animation:screenIn .42s cubic-bezier(.2,.85,.3,1);padding:20px;overflow:hidden}
  @keyframes screenIn{
    0%{opacity:0;transform:scale(1.02);filter:blur(6px)}
    60%{filter:blur(0)}
    100%{opacity:1;transform:scale(1);filter:blur(0)}
  }
  .screen::before,.screen::after{content:'';position:absolute;border-radius:50%;
    filter:blur(100px);opacity:.45;pointer-events:none;z-index:-1}
  .screen::before{width:460px;height:460px;background:var(--c1);top:-140px;left:-120px;
    animation:floatA 9s ease-in-out infinite}
  .screen::after{width:400px;height:400px;background:var(--c2);bottom:-160px;right:-100px;
    animation:floatB 11s ease-in-out infinite}
  @keyframes floatA{50%{transform:translate(30px,20px)}}
  @keyframes floatB{50%{transform:translate(-25px,-15px)}}
  .logo{font-size:clamp(38px,7vw,72px);font-weight:900;letter-spacing:8px;
    background:linear-gradient(120deg,var(--c1),var(--c2));
    -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
    filter:drop-shadow(0 0 30px rgba(var(--c2-rgb),.4))}
  .sub{color:var(--text);opacity:.55;letter-spacing:5px;font-size:12px;
    text-transform:uppercase;margin-top:-14px;font-weight:600}
  .mainBtns{display:flex;flex-direction:column;gap:14px;width:280px;margin-top:12px}
  .mbtn{position:relative;padding:16px 28px;font-size:15px;font-weight:700;
    letter-spacing:3px;text-transform:uppercase;color:var(--text);cursor:pointer;
    border-radius:14px;border:1px solid var(--ui-border);
    background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),.85),rgba(var(--ui-bg-2-rgb),.85));
    backdrop-filter:blur(10px);transition:.22s;overflow:hidden;font-family:inherit;text-align:center}
  .mbtn:hover{border-color:var(--c2);transform:translateY(-2px);
    box-shadow:0 10px 40px rgba(var(--c2-rgb),.35),0 0 0 1px rgba(var(--c2-rgb),.4) inset;color:#fff}
  .mbtn.play{border-color:rgba(var(--c1-rgb),.4);
    background:linear-gradient(160deg,rgba(var(--c1-rgb),.15),rgba(var(--ui-bg-2-rgb),.85))}
  .mbtn.play:hover{border-color:var(--c1)}
  .mbtn.exit:hover{border-color:#ff2d6f;
    box-shadow:0 10px 40px #ff2d6f55,0 0 0 1px #ff2d6f66 inset}
  .title{font-size:clamp(24px,3.4vw,36px);font-weight:800;letter-spacing:5px;
    background:linear-gradient(120deg,var(--c1),var(--c2));
    -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
    text-transform:uppercase}
  .modes{display:grid;grid-template-columns:repeat(3,minmax(190px,215px));gap:11px}
  @media(max-width:760px){.modes{grid-template-columns:repeat(2,minmax(190px,215px))}}
  @media(max-width:520px){.modes{grid-template-columns:minmax(190px,215px)}}
  .card{position:relative;padding:16px 18px;border-radius:14px;cursor:pointer;
    border:1px solid var(--ui-border);
    background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),.9),rgba(var(--ui-bg-2-rgb),.9));
    backdrop-filter:blur(10px);transition:.22s;overflow:hidden;
    animation:cardIn .45s cubic-bezier(.2,.85,.3,1) backwards}
  .card:nth-child(1){animation-delay:.04s}
  .card:nth-child(2){animation-delay:.09s}
  .card:nth-child(3){animation-delay:.14s}
  .card:nth-child(4){animation-delay:.19s}
  .card:nth-child(5){animation-delay:.24s}
  @keyframes cardIn{from{opacity:0;transform:translateY(14px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}
  .card:hover{transform:translateY(-4px);border-color:rgba(var(--c2-rgb),.55);
    box-shadow:0 12px 40px rgba(var(--c2-rgb),.28)}
  .card h3{font-size:15px;letter-spacing:1px;margin-bottom:5px;
    background:linear-gradient(120deg,var(--c1),var(--c2));
    -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
  .card p{font-size:11px;color:var(--text);opacity:.55;line-height:1.45}
  .card .tag{position:absolute;top:11px;right:11px;font-size:8.5px;letter-spacing:1.5px;
    padding:2px 6px;border-radius:5px;background:rgba(var(--c2-rgb),.15);
    color:var(--c2);border:1px solid rgba(var(--c2-rgb),.3)}
  .tabs{display:flex;gap:6px;padding:5px;border-radius:14px;
    background:rgba(var(--ui-bg-1-rgb),.7);border:1px solid var(--ui-border);
    backdrop-filter:blur(10px);flex-wrap:wrap;justify-content:center;
    animation:tabBarIn .45s cubic-bezier(.2,.85,.3,1) backwards}
  @keyframes tabBarIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
  .tab{padding:10px 18px;border:none;border-radius:10px;cursor:pointer;font-family:inherit;
    font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
    color:#8895b5;background:transparent;transition:.2s;position:relative;overflow:hidden}
  .tab::after{content:'';position:absolute;left:50%;top:50%;width:0;height:0;
    background:rgba(var(--c2-rgb),.35);border-radius:50%;transform:translate(-50%,-50%);
    transition:width .4s ease, height .4s ease, opacity .5s ease;opacity:0;pointer-events:none}
  .tab.ripple::after{width:180px;height:180px;opacity:1;transition:0s}
  .tab:hover{color:var(--text)}
  .tab.active{color:#04020a;background:linear-gradient(120deg,var(--c1),var(--c2));
    box-shadow:0 6px 20px rgba(var(--c2-rgb),.35)}
  .settingsBox{display:flex;flex-direction:column;gap:14px;padding:22px 26px;
    border-radius:16px;border:1px solid var(--ui-border);
    background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),.9),rgba(var(--ui-bg-2-rgb),.9));
    backdrop-filter:blur(10px);width:520px;max-width:95vw;min-height:340px;
    animation:boxIn .4s cubic-bezier(.2,.85,.3,1) backwards}
  @keyframes boxIn{from{opacity:0;transform:translateY(16px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
  .tabContent{display:flex;flex-direction:column;gap:14px;
    animation:tabIn .42s cubic-bezier(.18,.9,.32,1.15)}
  @keyframes tabIn{
    0%{opacity:0;transform:translateY(14px) scale(.97);filter:blur(5px)}
    55%{filter:blur(0)}
    100%{opacity:1;transform:translateY(0) scale(1);filter:blur(0)}
  }
  .tabContent .row{animation:rowIn .4s cubic-bezier(.2,.85,.3,1) backwards}
  .tabContent .row:nth-child(1){animation-delay:.03s}
  .tabContent .row:nth-child(2){animation-delay:.07s}
  .tabContent .row:nth-child(3){animation-delay:.11s}
  .tabContent .row:nth-child(4){animation-delay:.15s}
  .tabContent .row:nth-child(5){animation-delay:.19s}
  .tabContent .row:nth-child(6){animation-delay:.23s}
  @keyframes rowIn{from{opacity:0;transform:translateX(-10px)}to{opacity:1;transform:translateX(0)}}
  .sliderGrid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  @media(max-width:480px){.sliderGrid{grid-template-columns:1fr}}
  .row{display:flex;flex-direction:column;gap:7px}
  .row-h{flex-direction:row !important;align-items:center;justify-content:space-between;gap:16px}
  .row .lbl{display:flex;justify-content:space-between;align-items:center;
    font-size:10.5px;letter-spacing:2px;color:#8895b5;text-transform:uppercase;font-weight:600}
  .row .lbl b{color:var(--c1);font-weight:700;letter-spacing:0;font-size:12px}
  .gated{transition:opacity .25s, filter .25s}
  .gated.off{opacity:.35;pointer-events:none;filter:grayscale(.6)}
  input[type=range]{-webkit-appearance:none;width:100%;height:4px;border-radius:4px;
    background:linear-gradient(90deg,var(--c1),var(--c2));outline:none;cursor:pointer}
  input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;
    border-radius:50%;background:#fff;cursor:pointer;
    box-shadow:0 0 10px var(--c2),0 0 0 3px rgba(var(--c2-rgb),.25)}
  input[type=range]::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;
    background:#fff;cursor:pointer;box-shadow:0 0 10px var(--c2)}
  .colorRow{display:grid;grid-template-columns:1fr 1fr;gap:12px}
  .colorCell{display:flex;flex-direction:column;gap:5px}
  .colorCell span{font-size:9.5px;letter-spacing:2px;color:#8895b5;text-transform:uppercase;font-weight:600}
  input[type=color]{width:100%;height:44px;padding:0;border:none;border-radius:9px;
    cursor:pointer;background:transparent}
  input[type=color]::-webkit-color-swatch-wrapper{padding:0}
  input[type=color]::-webkit-color-swatch{border:1px solid var(--ui-border);border-radius:9px}
  input[type=color]::-moz-color-swatch{border:1px solid var(--ui-border);border-radius:9px}
  .presetGrid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
  @media(max-width:480px){.presetGrid{grid-template-columns:repeat(2,1fr)}}
  .preset,.shapeBtn,.effectBtn{padding:11px 4px;border-radius:10px;border:1px solid var(--ui-border);
    cursor:pointer;font-family:inherit;font-weight:700;font-size:10px;letter-spacing:1px;
    text-transform:uppercase;color:#8895b5;
    background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),1),rgba(var(--ui-bg-2-rgb),1));transition:.2s}
  .preset{font-size:10.5px}
  .preset:hover,.shapeBtn:hover,.effectBtn:hover{color:var(--text);border-color:rgba(var(--c2-rgb),.5);
    transform:translateY(-2px)}
  .preset.active,.shapeBtn.active,.effectBtn.active{color:#04020a;
    background:linear-gradient(120deg,var(--c1),var(--c2));border-color:transparent;
    box-shadow:0 8px 24px rgba(var(--c2-rgb),.4)}
  .shapeGrid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
  @media(max-width:560px){.shapeGrid{grid-template-columns:repeat(3,1fr)}}
  .effectGrid{display:grid;grid-template-columns:repeat(5,1fr);gap:7px}
  @media(max-width:560px){.effectGrid{grid-template-columns:repeat(3,1fr)}}
  .toggle{position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0}
  .toggle input{opacity:0;width:0;height:0;position:absolute}
  .toggle .sl{position:absolute;inset:0;background:var(--ui-border);border-radius:24px;
    transition:.2s;cursor:pointer}
  .toggle .sl::before{content:'';position:absolute;width:18px;height:18px;left:3px;top:3px;
    background:#fff;border-radius:50%;transition:.2s}
  .toggle input:checked + .sl{background:linear-gradient(120deg,var(--c1),var(--c2))}
  .toggle input:checked + .sl::before{transform:translateX(20px)}
  .backBtn{padding:10px 24px;border-radius:10px;cursor:pointer;font-family:inherit;
    font-size:12px;font-weight:600;letter-spacing:3px;text-transform:uppercase;
    background:transparent;color:#8895b5;border:1px solid var(--ui-border);transition:.2s}
  .backBtn:hover{color:var(--text);border-color:var(--c2);background:rgba(var(--c2-rgb),.1)}
  .hint{font-size:10px;letter-spacing:1.5px;color:var(--text);opacity:.45;
    text-align:center;padding:0 6px;line-height:1.5}
  .divider{height:1px;background:linear-gradient(90deg,transparent,var(--ui-border),transparent);margin:4px 0}
  .overlay{position:fixed;inset:0;z-index:25;display:flex;align-items:center;justify-content:center;
    background:#05030ac0;backdrop-filter:blur(8px);pointer-events:auto;
    animation:overlayIn .3s ease;padding:20px;overflow:hidden}
  @keyframes overlayIn{from{opacity:0;backdrop-filter:blur(0)}to{opacity:1;backdrop-filter:blur(8px)}}
  .overlay .panel{animation:panelIn .4s cubic-bezier(.2,.85,.3,1.05)}
  @keyframes panelIn{from{opacity:0;transform:translateY(18px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}
  .panel{padding:28px 34px;border-radius:20px;border:1px solid rgba(var(--c2-rgb),.35);
    background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),.95),rgba(var(--ui-bg-2-rgb),.95));
    box-shadow:0 30px 80px rgba(var(--c2-rgb),.2);display:flex;flex-direction:column;
    align-items:center;gap:16px;min-width:360px;max-width:92vw}
  .panel h3{font-size:19px;letter-spacing:4px;text-transform:uppercase;
    background:linear-gradient(120deg,var(--c1),var(--c2));
    -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
  .panelRow{display:flex;flex-direction:column;gap:8px;width:100%}
  .panelLabel{font-size:10px;letter-spacing:3px;color:#8895b5;text-transform:uppercase;
    font-weight:700;text-align:left}
  .sizeOpts{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;width:100%}
  .sizeBtn{height:72px;padding:6px 4px;border-radius:12px;border:1px solid var(--ui-border);
    cursor:pointer;font-family:inherit;font-weight:800;font-size:9.5px;letter-spacing:1px;
    color:#8895b5;background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),1),rgba(var(--ui-bg-2-rgb),1));
    transition:.2s;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px}
  .sizeBtn:hover{color:var(--text);border-color:rgba(var(--c2-rgb),.5);transform:translateY(-2px)}
  .sizeBtn.active{color:#04020a;background:linear-gradient(120deg,var(--c1),var(--c2));
    border-color:transparent;box-shadow:0 8px 24px rgba(var(--c2-rgb),.4)}
  .sizeBtn i{display:block;border-radius:50%;background:currentColor;
    width:var(--ds,12px);height:var(--ds,12px)}
  .sizeBtn[data-size="xs"] i{--ds:5px}
  .sizeBtn[data-size="s"]  i{--ds:8px}
  .sizeBtn[data-size="m"]  i{--ds:12px}
  .sizeBtn[data-size="l"]  i{--ds:17px}
  .sizeBtn[data-size="xl"] i{--ds:22px}
  .timeOpts{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;width:100%}
  .tbtn{padding:16px 8px;border-radius:12px;border:1px solid var(--ui-border);cursor:pointer;
    font-family:inherit;font-weight:800;font-size:15px;color:var(--text);
    background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),1),rgba(var(--ui-bg-2-rgb),1));
    transition:.2s;display:flex;flex-direction:column;align-items:center;gap:3px}
  .tbtn span{font-size:8.5px;letter-spacing:2px;color:#8895b5;font-weight:600}
  .tbtn:hover{border-color:var(--c1);transform:translateY(-3px);color:var(--c1)}
  .tbtn:hover span{color:var(--c1)}
  #hud{position:fixed;inset:0;z-index:10;pointer-events:none}
  .topbar{display:flex;align-items:flex-start;justify-content:space-between;padding:24px 34px}
  .stat{display:flex;flex-direction:column;gap:3px}
  .stat span{font-size:10px;letter-spacing:4px;color:#5d7a97;font-weight:700}
  .stat b{font-size:28px;font-weight:800;color:var(--text);
    text-shadow:0 0 20px rgba(var(--c1-rgb),.3)}
  .stat.right{align-items:flex-end}
  .timer{font-size:42px;font-weight:900;color:var(--c1);
    text-shadow:0 0 30px rgba(var(--c1-rgb),.5);letter-spacing:2px;
    font-variant-numeric:tabular-nums}
  .timer.warn{color:#ff2d6f;text-shadow:0 0 30px #ff2d6f88;animation:pulse .8s infinite}
  @keyframes pulse{50%{opacity:.55}}
  .crosshair{position:absolute;left:50%;top:50%;width:0;height:0;
    --gap:5px; --len:10px; --thick:2px; --dotSize:2px; --chColor:#00e5ff}
  .crosshair .line{position:absolute;background:var(--chColor);
    box-shadow:0 0 8px var(--chColor);border-radius:1px}
  .crosshair .l1{left:calc(var(--thick) * -0.5); bottom:var(--gap);width:var(--thick); height:var(--len)}
  .crosshair .l2{left:calc(var(--thick) * -0.5); top:var(--gap);width:var(--thick); height:var(--len)}
  .crosshair .l3{top:calc(var(--thick) * -0.5); right:var(--gap);height:var(--thick); width:var(--len)}
  .crosshair .l4{top:calc(var(--thick) * -0.5); left:var(--gap);height:var(--thick); width:var(--len)}
  .crosshair .dot{position:absolute;left:calc(var(--dotSize) * -0.5);top:calc(var(--dotSize) * -0.5);
    width:var(--dotSize); height:var(--dotSize); background:var(--chColor);
    box-shadow:0 0 6px var(--chColor);border-radius:50%}
  .crosshair.noDot .dot{display:none}
  .crosshair.hit .line, .crosshair.hit .dot{background:#ff2d6f;box-shadow:0 0 12px #ff2d6f}
  .crosshair.onTarget .line, .crosshair.onTarget .dot{
    background:var(--c2);box-shadow:0 0 14px var(--c2)}
  .modeLabel{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);
    font-size:11px;letter-spacing:6px;color:#4d6a86;text-transform:uppercase;font-weight:700}
  .trackBar{position:absolute;bottom:56px;left:50%;transform:translateX(-50%);
    width:220px;height:4px;background:#1a2233;border-radius:4px;overflow:hidden}
  .trackBar i{display:block;height:100%;background:linear-gradient(90deg,var(--c1),var(--c2));
    width:0%;transition:width .1s}
  .resGrid{display:grid;grid-template-columns:repeat(2,145px);gap:11px}
  .resGrid>div{padding:14px;border-radius:14px;text-align:center;
    background:linear-gradient(160deg,rgba(var(--ui-bg-1-rgb),.9),rgba(var(--ui-bg-2-rgb),.9));
    border:1px solid var(--ui-border);
    animation:rowIn .45s cubic-bezier(.2,.85,.3,1) backwards}
  .resGrid>div:nth-child(1){animation-delay:.05s}
  .resGrid>div:nth-child(2){animation-delay:.1s}
  .resGrid>div:nth-child(3){animation-delay:.15s}
  .resGrid>div:nth-child(4){animation-delay:.2s}
  .resGrid span{display:block;font-size:9px;letter-spacing:3px;color:#6b8199;
    margin-bottom:7px;text-transform:uppercase;font-weight:700}
  .resGrid b{font-size:22px;color:var(--text);font-weight:800}
  .btn{background:linear-gradient(120deg,var(--c1),var(--c2));color:#04020a;border:none;
    border-radius:12px;padding:13px 36px;font-size:13px;font-weight:800;letter-spacing:3px;
    cursor:pointer;transition:.2s;text-transform:uppercase;font-family:inherit;
    box-shadow:0 10px 30px rgba(var(--c2-rgb),.3);width:100%}
  .btn:hover{transform:translateY(-2px);box-shadow:0 15px 40px rgba(var(--c2-rgb),.5)}
  .btn.ghost{background:transparent;color:var(--text);border:1px solid var(--ui-border);
    box-shadow:none;opacity:.75}
  .btn.ghost:hover{opacity:1;border-color:var(--c2);background:rgba(var(--c2-rgb),.1)}
</style>
</head>
<body>

<div id="ui">
  <div id="screen-main" class="screen">
    <h1 class="logo">AIM TRAINER</h1>
    <p class="sub">3D · Тренажёр аима</p>
    <div class="mainBtns">
      <button class="mbtn play" data-go="play">Играть</button>
      <button class="mbtn" data-go="settings">Настройки</button>
      <button class="mbtn exit" id="exitBtnMain">Выход</button>
    </div>
  </div>

  <div id="screen-play" class="screen hidden">
    <h2 class="title">Выбери режим</h2>
    <div class="modes" id="modesGrid"></div>
    <button class="backBtn" data-back>← Назад</button>
  </div>

  <div id="screen-settings" class="screen hidden">
    <h2 class="title">Настройки</h2>
    <div class="tabs">
      <button class="tab active" data-tab="graphics">Графика</button>
      <button class="tab" data-tab="crosshair">Прицел</button>
      <button class="tab" data-tab="camera">Камера</button>
      <button class="tab" data-tab="animations">Анимации</button>
      <button class="tab" data-tab="custom">Кастомизация</button>
    </div>
    <div class="settingsBox">
      <div class="tabContent" data-tab="graphics">
        <div class="row">
          <div class="lbl"><span>Качество графики</span></div>
          <div class="presetGrid">
            <button class="preset" data-preset="low">Низкая</button>
            <button class="preset" data-preset="medium">Средняя</button>
            <button class="preset" data-preset="high">Высокая</button>
            <button class="preset" data-preset="ultra">Ультра</button>
          </div>
        </div>
        <div class="row hidden" id="particlesRow">
          <div class="lbl"><span>Частицы (ультра)</span><b id="particlesVal">700</b></div>
          <input type="range" id="particlesSlider" min="0" max="1500" step="50" value="700">
        </div>
        <div class="row hidden" id="bloomRow">
          <div class="lbl"><span>Сила Bloom (ультра)</span><b id="bloomVal">0.75</b></div>
          <input type="range" id="bloomSlider" min="0" max="3" step="0.05" value="0.75">
        </div>
        <div class="row hidden" id="brightnessRow">
          <div class="lbl"><span>Яркость (ультра)</span><b id="brightnessVal">1.00</b></div>
          <input type="range" id="brightnessSlider" min="0.3" max="3" step="0.05" value="1">
        </div>
        <div class="row hidden" id="effBloomRow">
          <div class="lbl"><span>Сила блума эффектов (ультра)</span><b id="effBloomVal">1.00×</b></div>
          <input type="range" id="effBloomSlider" min="0" max="3" step="0.05" value="1">
        </div>
        <div class="row">
          <div class="lbl"><span>Форма целей</span></div>
          <div class="shapeGrid">
            <button class="shapeBtn active" data-shape="sphere">Сфера</button>
            <button class="shapeBtn" data-shape="cube">Куб</button>
            <button class="shapeBtn" data-shape="heart">Сердце</button>
            <button class="shapeBtn" data-shape="star">Звезда</button>
            <button class="shapeBtn" data-shape="crystal">Кристалл</button>
          </div>
        </div>
        <div class="row">
          <div class="lbl"><span>Эффект попадания · 11 вариантов</span></div>
          <div class="effectGrid">
            <button class="effectBtn" data-effect="none">Ничего</button>
            <button class="effectBtn" data-effect="circle">Кружок</button>
            <button class="effectBtn" data-effect="stars">Звёзды</button>
            <button class="effectBtn" data-effect="explosion">Взрыв</button>
            <button class="effectBtn" data-effect="shards">Осколки</button>
            <button class="effectBtn" data-effect="lightning">Молния</button>
            <button class="effectBtn" data-effect="plasma">Плазма</button>
            <button class="effectBtn" data-effect="sand">Песок</button>
            <button class="effectBtn" data-effect="sparks">Искры</button>
            <button class="effectBtn" data-effect="crystals">Кристаллы</button>
            <button class="effectBtn" data-effect="portals">Порталы</button>
          </div>
        </div>
        <div class="row">
          <div class="lbl"><span>Размер эффектов</span><b id="effSizeVal">1.00×</b></div>
          <input type="range" id="effSizeSlider" min="0.3" max="2.5" step="0.05" value="1">
        </div>
        <div class="row">
          <div class="lbl"><span>Скорость эффекта попадания</span><b id="effSpeedVal">1.00×</b></div>
          <input type="range" id="effSpeed" min="0.25" max="3" step="0.05" value="1">
        </div>
        <p class="hint" id="graphicsHint">
          Выбери пресет — графика применится сразу. Ультра включает HDR-Bloom, отражения и частицы.
        </p>
      </div>

      <div class="tabContent hidden" data-tab="crosshair">
        <div class="sliderGrid">
          <div class="row"><div class="lbl"><span>Толщина</span><b id="chThickVal">2</b></div>
            <input type="range" id="chThick" min="1" max="8" step="1" value="2"></div>
          <div class="row"><div class="lbl"><span>Длина линий</span><b id="chLenVal">10</b></div>
            <input type="range" id="chLen" min="0" max="30" step="1" value="10"></div>
          <div class="row"><div class="lbl"><span>Промежуток</span><b id="chGapVal">5</b></div>
            <input type="range" id="chGap" min="0" max="20" step="1" value="5"></div>
          <div class="row"><div class="lbl"><span>Размер точки</span><b id="chDotVal">2</b></div>
            <input type="range" id="chDot" min="0" max="10" step="1" value="2"></div>
        </div>
        <div class="row">
          <div class="lbl"><span>Цвет прицела</span></div>
          <div class="colorRow">
            <div class="colorCell"><span>Цвет</span><input type="color" id="chColor" value="#00e5ff"></div>
            <div class="colorCell"><span>Синхр. с темой</span>
              <label class="toggle" style="margin-top:8px">
                <input type="checkbox" id="chSyncTheme" checked><span class="sl"></span>
              </label></div>
          </div>
        </div>
        <div class="row">
          <div class="lbl"><span>Прозрачность</span><b id="chOpacityVal">100%</b></div>
          <input type="range" id="chOpacity" min="20" max="100" step="1" value="100">
        </div>
        <div class="row row-h"><div class="lbl"><span>Показывать точку</span></div>
          <label class="toggle"><input type="checkbox" id="chDotToggle" checked><span class="sl"></span></label></div>
        <div class="row row-h"><div class="lbl"><span>Прицел не меняет цвет</span></div>
          <label class="toggle"><input type="checkbox" id="chFixed"><span class="sl"></span></label></div>
      </div>

      <div class="tabContent hidden" data-tab="camera">
        <div class="row"><div class="lbl"><span>Сенса</span><b id="sensVal">1.00</b></div>
          <input type="range" id="sens" min="0.1" max="5" step="0.01" value="1"></div>
        <div class="row"><div class="lbl"><span>Угол обзора (FOV)</span><b id="fovVal">90</b></div>
          <input type="range" id="fov" min="60" max="120" step="1" value="90"></div>
        <p class="hint">Сенса влияет на скорость движения камеры. FOV — угол обзора в градусах.</p>
      </div>

      <div class="tabContent hidden" data-tab="animations">
        <div class="row"><div class="lbl"><span>Скорость анимации пистолета</span><b id="animPistolVal">1.00×</b></div>
          <input type="range" id="animPistol" min="0.25" max="3" step="0.05" value="1"></div>
        <p class="hint">Влияет на скорость отдачи и возврата пистолета после выстрела.</p>
      </div>

      <div class="tabContent hidden" data-tab="custom">
        <div class="row"><div class="lbl"><span>Основной цвет (акцент)</span></div>
          <input type="color" id="color1" value="#00e5ff"></div>
        <div class="row row-h"><div class="lbl"><span>Градиент (2 цвета)</span></div>
          <label class="toggle"><input type="checkbox" id="gradToggle"><span class="sl"></span></label></div>
        <div class="row hidden" id="color2Row">
          <div class="lbl"><span>Второй цвет</span></div>
          <input type="color" id="color2" value="#b14cff"></div>
        <div class="divider"></div>
        <div class="row"><div class="lbl"><span>Цвет интерфейса (панели, кнопки)</span></div>
          <input type="color" id="uiColor" value="#1a1230"></div>
        <div class="divider"></div>
        <div class="row row-h"><div class="lbl"><span>Свой фон меню</span></div>
          <label class="toggle"><input type="checkbox" id="menuBgToggle"><span class="sl"></span></label></div>
        <div class="row gated off" id="menuBgGated">
          <div class="lbl"><span>Цвет фона меню</span></div>
          <input type="color" id="menuBgColor" value="#0b0714"></div>
        <div class="divider"></div>
        <div class="row"><div class="lbl"><span>Цвет текста</span></div>
          <input type="color" id="textColor" value="#e8eef7"></div>
        <p class="hint">По умолчанию всё выключено — включай только то, что тебе нужно.</p>
      </div>
    </div>
    <button class="backBtn" id="settingsBackBtn">← Назад</button>
  </div>

  <div id="screen-exit" class="screen hidden">
    <h1 class="logo">ДО ВСТРЕЧИ</h1>
    <p class="sub">Спасибо за игру</p>
    <button class="btn" id="closeTabBtn">Закрыть вкладку</button>
    <button class="backBtn" data-back>← Вернуться</button>
  </div>

  <div id="overlay-time" class="overlay hidden">
    <div class="panel">
      <h3 id="timeTitle">Gridshot</h3>
      <div class="panelRow">
        <div class="panelLabel">Размер мячей</div>
        <div class="sizeOpts">
          <button class="sizeBtn" data-size="xs"><i></i><span>XS</span></button>
          <button class="sizeBtn" data-size="s"><i></i><span>S</span></button>
          <button class="sizeBtn active" data-size="m"><i></i><span>M</span></button>
          <button class="sizeBtn" data-size="l"><i></i><span>L</span></button>
          <button class="sizeBtn" data-size="xl"><i></i><span>XL</span></button>
        </div>
      </div>
      <div class="panelRow">
        <div class="panelLabel">Время матча</div>
        <div class="timeOpts">
          <button class="tbtn" data-time="30">30<span>СЕК</span></button>
          <button class="tbtn" data-time="60">60<span>СЕК</span></button>
          <button class="tbtn" data-time="120">120<span>СЕК</span></button>
          <button class="tbtn" data-time="0">∞<span>БЕЗ ЛИМ.</span></button>
        </div>
      </div>
      <button class="backBtn" id="cancelTime">Отмена</button>
    </div>
  </div>
</div>

<div id="hud" class="hidden">
  <div class="topbar">
    <div class="stat"><span>ОЧКИ</span><b id="hScore">0</b></div>
    <div class="timer" id="hTime">60</div>
    <div class="stat right"><span id="hAccLbl">ТОЧНОСТЬ</span><b id="hAcc">100%</b></div>
  </div>
  <div class="crosshair" id="crosshair">
    <div class="line l1"></div><div class="line l2"></div>
    <div class="line l3"></div><div class="line l4"></div>
    <div class="dot"></div>
  </div>
  <div class="trackBar hidden" id="trackBar"><i></i></div>
  <div class="modeLabel" id="hMode"></div>
</div>

<div id="pause" class="overlay hidden">
  <div class="panel">
    <h3>Пауза</h3>
    <button class="btn" id="resumeBtn">Продолжить</button>
    <button class="btn ghost" id="pauseSettingsBtn">Настройки</button>
    <button class="backBtn" id="pauseExitBtn">В меню</button>
  </div>
</div>

<div id="results" class="overlay hidden">
  <div class="panel">
    <h3>Результат</h3>
    <div class="resGrid">
      <div><span>Очки</span><b id="rScore">0</b></div>
      <div><span id="rAccLbl">Точность</span><b id="rAcc">0%</b></div>
      <div><span>Попадания</span><b id="rHits">0</b></div>
      <div><span>Выстрелы</span><b id="rShots">0</b></div>
    </div>
    <button class="btn" id="againBtn">Ещё раз</button>
    <button class="backBtn" id="menuBtn">В меню</button>
  </div>
</div>

<script type="importmap">
{ "imports": {
  "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
  "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}}
</script>

<script type="module">
import * as THREE from 'three';

/* ============================ РЕЖИМЫ ============================ */
const MODES = {
  gridshot: { name:'Gridshot',  desc:'Три цели одновременно. Скорость и точность.', tag:'ПОПУЛЯРНЫЙ',
              count:3, radius:0.42, duration:60, type:'click', spread:1.0 },
  flick:    { name:'Flick',     desc:'Одна цель за раз. Тренировка резких разворотов.',
              count:1, radius:0.38, duration:60, type:'click', spread:1.0 },
  tracking: { name:'Tracking',  desc:'Удерживай прицел на движущейся цели (зажми ЛКМ).', tag:'НОВОЕ',
              count:1, radius:0.55, duration:30, type:'track' },
  spider:   { name:'Spider Shot', desc:'Цель исчезает через 1.4 сек. Не успел — переехала.', tag:'НОВОЕ',
              count:1, radius:0.40, duration:60, type:'click', spread:1.0, spawnTimeout:1.4 },
  motion:   { name:'Motion',    desc:'Движущиеся цели. Клик по скорости.', tag:'НОВОЕ',
              count:3, radius:0.40, duration:60, type:'click', spread:1.0, moving:true }
};
const SHAPES = ['sphere','cube','heart','star','crystal'];
const BALL_SIZES = [
  { id:'xs', mult:0.55 }, { id:'s', mult:0.75 }, { id:'m', mult:1.00 },
  { id:'l', mult:1.35 },  { id:'xl', mult:1.80 }
];
const VALID_EFFECTS = ['none','circle','stars','explosion','shards','lightning','plasma','sand','sparks','crystals','portals'];
const VALID_TABS = ['graphics','crosshair','camera','animations','custom'];

/* ============================ НАСТРОЙКИ ============================ */
const THEME = { c1:'#00e5ff', c2:'#b14cff', gradient:false,
  uiColor:'#1a1230', menuBgEnabled:false, menuBgColor:'#0b0714', textColor:'#e8eef7' };

const DEFAULT_MENU_GRADIENT =
  'radial-gradient(ellipse at 50% 40%,#140a26 0%,#0b0714 55%,#05030a 100%)';

const CROSSHAIR = { thickness:2, length:10, gap:5, dot:true, dotSize:2,
  fixedColor:false, color:'#00e5ff', opacity:1, syncTheme:true };

const HIT_EFFECT = { mode:'stars', speed:1.0, hdr:1.0 };
const ANIM = { pistolSpeed:1.0 };

const BLOOM = { strength:0.75, radius:0.5, threshold:0.75 };
const EFFECT_HDR = 3.5;
const EFFECT_HDR_MIN = 0.65;

function hdrMult(baseMult = 1){
  const userHdr = Math.max(0, HIT_EFFECT.hdr ?? 1);
  const hdr = EFFECT_HDR_MIN + (EFFECT_HDR - EFFECT_HDR_MIN) * userHdr;
  return hdr * baseMult;
}

const GRAPHICS_PRESETS = {
  low:    { pr:1,    shadows:false, shadowSize:512,  fog:false, bloom:false, detail:'low' },
  medium: { pr:1.25, shadows:true,  shadowSize:512,  fog:true,  bloom:false, detail:'medium' },
  high:   { pr:1.5,  shadows:true,  shadowSize:1024, fog:true,  bloom:false, detail:'high' },
  ultra:  { pr:2,    shadows:true,  shadowSize:2048, fog:true,  bloom:true,  detail:'ultra' }
};
const GRAPHICS = { level:'high', shape:'sphere', particles:700, effectSize:1.0, exposure:1.0 };

const state = {
  phase:'menu', mode:null,
  score:0, shots:0, hits:0, timeLeft:0, elapsed:0, trackTime:0,
  infinite:false, sens:1.0, mouseDown:false,
  recoil:0, recoilRot:0, swayX:0, swayY:0,
  useComposer:false,
  settingsReturnTo:null, activeSettingsTab:'graphics',
  sizeId:'m', sizeMultiplier:1.0,
  lastDuration:60
};

let effectScale = 1.0;
let crosshairHitTimer = null;

function getBallRadius(){
  const m = MODES[state.mode];
  if (!m) return 0.42;
  return m.radius * (state.sizeMultiplier || 1);
}
function hexToRgb(hex){
  const n = parseInt(hex.replace('#',''), 16);
  if (isNaN(n)) return [255,255,255];
  return [(n>>16)&255, (n>>8)&255, n&255];
}
function hexToRgbStr(hex){
  const [r,g,b] = hexToRgb(hex); return `${r},${g},${b}`;
}
function clamp255(x){ return Math.max(0, Math.min(255, Math.round(x))); }
function getC2(){ return THEME.gradient ? THEME.c2 : THEME.c1; }

/* ============================ THREE.JS ============================ */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05030a);
scene.fog = new THREE.FogExp2(0x05030a, 0.02);

const camera = new THREE.PerspectiveCamera(90, innerWidth/innerHeight, 0.01, 300);
camera.rotation.order = 'YXZ';
camera.position.set(0, 1.7, 0);
scene.add(camera);

const renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
document.body.appendChild(renderer.domElement);

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

let composer = null, bloomPass = null, outputPass = null, fxReady = false;

async function initPostFX(){
  try {
    const { EffectComposer }  = await import('three/addons/postprocessing/EffectComposer.js');
    const { RenderPass }      = await import('three/addons/postprocessing/RenderPass.js');
    const { UnrealBloomPass } = await import('three/addons/postprocessing/UnrealBloomPass.js');
    const { OutputPass }      = await import('three/addons/postprocessing/OutputPass.js');

    const rt = new THREE.WebGLRenderTarget(
      innerWidth, innerHeight,
      { type: THREE.HalfFloatType }
    );

    composer = new EffectComposer(renderer, rt);
    composer.addPass(new RenderPass(scene, camera));

    bloomPass = new UnrealBloomPass(
      new THREE.Vector2(innerWidth, innerHeight),
      BLOOM.strength, BLOOM.radius, BLOOM.threshold
    );
    bloomPass.enabled = false;
    composer.addPass(bloomPass);

    outputPass = new OutputPass();
    composer.addPass(outputPass);

    composer.setSize(innerWidth, innerHeight);
    fxReady = true;
    updateComposerState();
  } catch(e){ console.warn('Постобработка недоступна:', e); }
}
function updateComposerState(){
  if (!fxReady || !bloomPass) return;
  const wantBloom = GRAPHICS_PRESETS[GRAPHICS.level].bloom;
  bloomPass.enabled = wantBloom;
  bloomPass.strength = BLOOM.strength;
  bloomPass.threshold = BLOOM.threshold;
  bloomPass.radius = BLOOM.radius;
  state.useComposer = wantBloom;
}

scene.add(new THREE.HemisphereLight(0x9ec8ff, 0x0a0518, 0.85));
const dir = new THREE.DirectionalLight(0xffffff, 2.0);
dir.position.set(6, 14, 6);
dir.castShadow = true;
dir.shadow.mapSize.set(1024, 1024);
Object.assign(dir.shadow.camera, { left:-18, right:18, top:18, bottom:-6, near:1, far:60 });
dir.shadow.camera.updateProjectionMatrix();
scene.add(dir);

const lCyan   = new THREE.PointLight(0x00e5ff, 140, 45, 2); lCyan.position.set(-10, 7, -9);
const lPurple = new THREE.PointLight(0xb14cff, 160, 45, 2); lPurple.position.set(0, 8, -12);
const lPink   = new THREE.PointLight(0xff2d92, 120, 45, 2); lPink.position.set(10, 7, -9);
scene.add(lCyan, lPurple, lPink);

const gunLight = new THREE.PointLight(0xffffff, 4, 4, 2);
gunLight.position.set(0.3, 0.2, 0.4);
camera.add(gunLight);

function gridTexLight(){
  const c = document.createElement('canvas'); c.width = c.height = 512;
  const x = c.getContext('2d');
  x.fillStyle = '#dddddd'; x.fillRect(0,0,512,512);
  x.strokeStyle = '#3a3a3a'; x.lineWidth = 3;
  for (let i=0;i<=512;i+=64){
    x.beginPath(); x.moveTo(i,0); x.lineTo(i,512); x.stroke();
    x.beginPath(); x.moveTo(0,i); x.lineTo(512,i); x.stroke();
  }
  const t = new THREE.CanvasTexture(c);
  t.wrapS = t.wrapT = THREE.RepeatWrapping;
  t.repeat.set(5,3);
  return t;
}

const floor = new THREE.Mesh(new THREE.PlaneGeometry(60,60),
  new THREE.MeshStandardMaterial({ color:0x0a0718, roughness:0.65, metalness:0.35 }));
floor.rotation.x = -Math.PI/2; floor.receiveShadow = true; scene.add(floor);

const wallMat = new THREE.MeshStandardMaterial({
  map: gridTexLight(), color:0x0c0a18, emissive:0x000000,
  emissiveIntensity:0, roughness:0.9, metalness:0.05 });
const wall = new THREE.Mesh(new THREE.PlaneGeometry(60,26), wallMat);
wall.position.set(0, 12, -17); wall.receiveShadow = true; scene.add(wall);

const sideMat = new THREE.MeshStandardMaterial({ color:0x0a0718, roughness:0.95 });
const wallL = new THREE.Mesh(new THREE.PlaneGeometry(40,26), sideMat);
wallL.position.set(-16, 12, -8); wallL.rotation.y = Math.PI/2; scene.add(wallL);
const wallR = wallL.clone(); wallR.position.x = 16; wallR.rotation.y = -Math.PI/2; scene.add(wallR);

const ceil = new THREE.Mesh(new THREE.PlaneGeometry(60,60),
  new THREE.MeshStandardMaterial({ color:0x05030a, roughness:1 }));
ceil.rotation.x = Math.PI/2; ceil.position.y = 14; scene.add(ceil);

const strips = [];
for (let i = 0; i < 3; i++){
  const strip = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.02, 30),
    new THREE.MeshBasicMaterial({ color: 0x00e5ff }));
  strip.position.set((i - 1) * 11, 0.02, -6);
  if (i === 1){
    strip.geometry = new THREE.BoxGeometry(0.05, 0.02, 20);
    strip.position.set(0, 0.02, -12);
  }
  strips.push(strip);
  scene.add(strip);
}

const SHARED_GEOS = new Set();
const detailGroup = new THREE.Group();
scene.add(detailGroup);
let particles = null;

function disposeGroup(g){
  while (g.children.length){
    const c = g.children[0];
    g.remove(c);
    if (c.geometry && !SHARED_GEOS.has(c.geometry)) c.geometry.dispose();
    if (c.material){
      if (Array.isArray(c.material)) c.material.forEach(m => m.dispose());
      else c.material.dispose();
    }
  }
}
function getParticlesCount(level){
  if (level === 'high') return 220;
  if (level === 'ultra') return Math.max(0, Math.min(1500, GRAPHICS.particles));
  return 0;
}
function buildDetail(level){
  disposeGroup(detailGroup);
  particles = null;
  if (level === 'low') return;
  const count = level === 'medium' ? 4 : level === 'high' ? 6 : 8;
  for (let i = 0; i < count; i++){
    const c = (i % 2 === 0) ? themeC1 : themeC2;
    const s = new THREE.Mesh(new THREE.SphereGeometry(0.15, 16, 16),
      new THREE.MeshBasicMaterial({ color: c }));
    s.position.set((i - (count-1)/2) * 3.2, 12.2, -10);
    detailGroup.add(s);
    if (level !== 'medium'){
      const l = new THREE.PointLight(c, 25, 14, 2);
      l.position.copy(s.position);
      detailGroup.add(l);
    }
  }
  const pCount = getParticlesCount(level);
  if (pCount > 0){
    const pos = new Float32Array(pCount * 3);
    const cols = new Float32Array(pCount * 3);
    for (let i = 0; i < pCount; i++){
      pos[i*3]   = (Math.random() - 0.5) * 30;
      pos[i*3+1] = Math.random() * 12 + 1;
      pos[i*3+2] = (Math.random() - 0.5) * 25 - 8;
      const c = Math.random() < 0.5 ? themeC1 : themeC2;
      cols[i*3] = c.r; cols[i*3+1] = c.g; cols[i*3+2] = c.b;
    }
    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
    geo.setAttribute('color', new THREE.BufferAttribute(cols, 3));
    const mat = new THREE.PointsMaterial({
      size: level === 'ultra' ? 0.07 : 0.055,
      vertexColors: true, transparent: true, opacity: 0.5,
      blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true });
    particles = new THREE.Points(geo, mat);
    detailGroup.add(particles);
  }
  if (level === 'ultra'){
    for (let i = 0; i < 7; i++){
      const c = (i % 2 === 0) ? themeC1 : themeC2;
      const stripL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 4.5, 0.06),
        new THREE.MeshBasicMaterial({ color: c }));
      stripL.position.set(-15.85, 2.4 + i*1.4, -3 - i*1.7);
      detailGroup.add(stripL);
      const stripR = stripL.clone(); stripR.position.x = 15.85;
      detailGroup.add(stripR);
    }
  }
}

function createPistol(){
  const g = new THREE.Group();
  const steel = new THREE.MeshStandardMaterial({ color:0x2b313b, metalness:0.92, roughness:0.32 });
  const dark  = new THREE.MeshStandardMaterial({ color:0x12151a, metalness:0.65, roughness:0.6 });
  const gripM = new THREE.MeshStandardMaterial({ color:0x1a1e24, metalness:0.15, roughness:0.95 });
  const glow  = new THREE.MeshStandardMaterial({ color:0x00e5ff, emissive:0x00b8d4,
                    emissiveIntensity:1.6, metalness:0.4, roughness:0.25 });
  const slide = new THREE.Mesh(new THREE.BoxGeometry(0.115, 0.115, 0.54), steel);
  slide.position.set(0, 0, -0.07); slide.castShadow = true; g.add(slide);
  for (let i = 0; i < 6; i++) {
    const s = new THREE.Mesh(new THREE.BoxGeometry(0.122, 0.085, 0.011), dark);
    s.position.set(0, 0, 0.14 - i*0.026); g.add(s);
  }
  const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.029, 0.029, 0.10, 16), dark);
  barrel.rotation.x = Math.PI/2; barrel.position.set(0, 0.005, -0.395); g.add(barrel);
  const muzzle = new THREE.Mesh(new THREE.TorusGeometry(0.033, 0.008, 8, 20), steel);
  muzzle.position.set(0, 0.005, -0.445); g.add(muzzle);
  const frame = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.075, 0.36), steel);
  frame.position.set(0, -0.093, -0.05); g.add(frame);
  const grip = new THREE.Mesh(new THREE.BoxGeometry(0.095, 0.30, 0.135), gripM);
  grip.position.set(0, -0.245, 0.095); grip.rotation.x = -0.18; g.add(grip);
  for (let i = 0; i < 5; i++) {
    const l = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.010, 0.14), steel);
    l.position.set(0, -0.135 - i*0.05, 0.088 + i*0.009);
    l.rotation.x = -0.18; g.add(l);
  }
  const guard = new THREE.Mesh(new THREE.TorusGeometry(0.055, 0.010, 8, 24), steel);
  guard.rotation.y = Math.PI/2; guard.position.set(0, -0.115, -0.015); g.add(guard);
  const trig = new THREE.Mesh(new THREE.BoxGeometry(0.018, 0.058, 0.018), dark);
  trig.position.set(0, -0.118, -0.025); trig.rotation.x = 0.22; g.add(trig);
  const rear = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.022, 0.022), dark);
  rear.position.set(0, 0.068, 0.155); g.add(rear);
  const rl = new THREE.Mesh(new THREE.BoxGeometry(0.018, 0.032, 0.026), dark);
  rl.position.set(-0.041, 0.074, 0.155); g.add(rl);
  const rr = rl.clone(); rr.position.x = 0.041; g.add(rr);
  const front = new THREE.Mesh(new THREE.BoxGeometry(0.016, 0.030, 0.020), dark);
  front.position.set(0, 0.072, -0.325); g.add(front);
  const accent = new THREE.Mesh(new THREE.BoxGeometry(0.118, 0.007, 0.30), glow);
  accent.position.set(0, 0.046, -0.09); g.add(accent);
  const flashMat = new THREE.MeshBasicMaterial({
    color:0xffd27a, transparent:true, opacity:0,
    depthWrite:false, blending:THREE.AdditiveBlending });
  const flash = new THREE.Mesh(new THREE.SphereGeometry(0.085, 12, 12), flashMat);
  flash.position.set(0, 0.005, -0.47); g.add(flash);
  g.userData = { flash, flashMat, accent };
  return g;
}
const gun = createPistol();
const GUN_HOME = new THREE.Vector3(0.28, -0.26, -0.72);
gun.position.copy(GUN_HOME);
gun.rotation.set(0.02, -0.06, 0);
camera.add(gun);

/* ============================ ТЕМА ============================ */
const themeC1 = new THREE.Color(THEME.c1);
const themeC2 = new THREE.Color(THEME.c1);

function makeDimThemeColor(baseColor, satMul = 0.7, light = 0.055){
  const hsl = { h:0, s:0, l:0 };
  baseColor.getHSL(hsl);
  const c = new THREE.Color();
  c.setHSL(hsl.h, Math.min(0.65, hsl.s * satMul), light);
  return c;
}
function applyTheme(){
  themeC1.set(THEME.c1); themeC2.set(getC2());
  const c1 = THEME.c1, c2 = getC2();
  const root = document.documentElement;
  root.style.setProperty('--c1', c1);
  root.style.setProperty('--c2', c2);
  root.style.setProperty('--c1-rgb', hexToRgbStr(c1));
  root.style.setProperty('--c2-rgb', hexToRgbStr(c2));
  root.style.setProperty('--text', THEME.textColor);
  applyUiColor(); applyMenuBg();
  const wallColor = makeDimThemeColor(themeC1, 0.7, 0.055);
  wallMat.color.copy(wallColor);
  wallMat.emissive.copy(wallColor);
  wallMat.emissiveIntensity = 0.35;
  wallMat.needsUpdate = true;
  sideMat.color.setHex(0x0a0718); sideMat.needsUpdate = true;
  lCyan.color.copy(themeC1); lPurple.color.copy(themeC2); lPink.color.copy(themeC2);
  strips.forEach((s, i) => s.material.color.copy(i % 2 === 0 ? themeC1 : themeC2));
  gun.userData.accent.material.color.copy(themeC1);
  gun.userData.accent.material.emissive.copy(themeC1);
  buildDetail(GRAPHICS_PRESETS[GRAPHICS.level].detail);
  if (CROSSHAIR.syncTheme){
    CROSSHAIR.color = c1;
    const chc = $('chColor'); if (chc) chc.value = c1;
  }
  applyCrosshair();
  const m = state.mode ? MODES[state.mode] : null;
  if (m && m.type === 'click'){
    for (const t of targets){
      if (t.mesh && t.mesh.material){
        t.mesh.material.color.copy(themeC1);
        t.mesh.material.emissive.copy(themeC1).multiplyScalar(0.45);
      }
    }
  }
}
function applyUiColor(){
  const [r, g, b] = hexToRgb(THEME.uiColor);
  const bg1 = [clamp255(r*1.2), clamp255(g*1.2), clamp255(b*1.2)];
  const bg2 = [clamp255(r*0.5), clamp255(g*0.5), clamp255(b*0.5)];
  const br  = [clamp255(r*1.6), clamp255(g*1.6), clamp255(b*1.6)];
  const root = document.documentElement;
  root.style.setProperty('--ui-bg-1-rgb', `${bg1[0]},${bg1[1]},${bg1[2]}`);
  root.style.setProperty('--ui-bg-2-rgb', `${bg2[0]},${bg2[1]},${bg2[2]}`);
  root.style.setProperty('--ui-border', `rgb(${br[0]},${br[1]},${br[2]})`);
}
function applyMenuBg(){
  if (THEME.menuBgEnabled){
    const [r,g,b] = hexToRgb(THEME.menuBgColor);
    const lr = Math.min(255, Math.round(r * 1.8));
    const lg = Math.min(255, Math.round(g * 1.8));
    const lb = Math.min(255, Math.round(b * 1.8));
    const dr = Math.round(r * 0.5), dg = Math.round(g * 0.5), db = Math.round(b * 0.5);
    const gradient = `radial-gradient(ellipse at 50% 40%,rgb(${lr},${lg},${lb}) 0%,rgb(${r},${g},${b}) 55%,rgb(${dr},${dg},${db}) 100%)`;
    document.documentElement.style.setProperty('--menu-bg', gradient);
  } else {
    document.documentElement.style.setProperty('--menu-bg', DEFAULT_MENU_GRADIENT);
  }
}

/* ============================ ГРАФИКА ============================ */
function updateParticlesRowVisibility(){
  const isUltra = GRAPHICS.level === 'ultra';
  ['particlesRow','bloomRow','brightnessRow','effBloomRow'].forEach(id => {
    const el = $(id);
    if (el) el.classList.toggle('hidden', !isUltra);
  });
}
function applyGraphics(level){
  if (!GRAPHICS_PRESETS[level]) level = 'high';
  GRAPHICS.level = level;
  const p = GRAPHICS_PRESETS[level];
  renderer.setPixelRatio(Math.min(devicePixelRatio, p.pr));
  renderer.toneMappingExposure = GRAPHICS.exposure;
  renderer.shadowMap.enabled = p.shadows;
  dir.castShadow = p.shadows;
  if (dir.shadow.map){ dir.shadow.map.dispose(); dir.shadow.map = null; }
  dir.shadow.mapSize.set(p.shadowSize, p.shadowSize);
  if (p.fog){ if (!scene.fog) scene.fog = new THREE.FogExp2(0x05030a, 0.02); }
  else scene.fog = null;
  if (p.detail === 'ultra'){ floor.material.metalness = 0.6; floor.material.roughness = 0.22; }
  else if (p.detail === 'high'){ floor.material.metalness = 0.4; floor.material.roughness = 0.5; }
  else { floor.material.metalness = 0.15; floor.material.roughness = 0.85; }
  floor.material.needsUpdate = true;
  buildDetail(p.detail);
  updateComposerState();
  updateParticlesRowVisibility();
  document.querySelectorAll('.preset[data-preset]').forEach(b => {
    b.classList.toggle('active', b.dataset.preset === level);
  });
  const hints = {
    low: 'Максимальный FPS. Без теней, тумана и деталей.',
    medium: 'Баланс производительности и качества. Мягкие тени, потолочный свет.',
    high: 'Красивая графика: детальные тени, частицы, больше света.',
    ultra: 'Максимум качества: HDR-Bloom, отражения, частицы.'
  };
  const hint = $('graphicsHint'); if (hint) hint.textContent = hints[level];
  saveSettings();
}

/* ============================ ПРИЦЕЛ ============================ */
function applyCrosshair(){
  if (!crosshairEl) return;
  crosshairEl.style.setProperty('--gap', CROSSHAIR.gap + 'px');
  crosshairEl.style.setProperty('--len', CROSSHAIR.length + 'px');
  crosshairEl.style.setProperty('--thick', CROSSHAIR.thickness + 'px');
  crosshairEl.style.setProperty('--dotSize', CROSSHAIR.dotSize + 'px');
  const col = CROSSHAIR.color || THEME.c1;
  crosshairEl.style.setProperty('--chColor', col);
  const op = CROSSHAIR.opacity;
  crosshairEl.querySelectorAll('.line, .dot').forEach(el => { el.style.opacity = op; });
  crosshairEl.classList.toggle('noDot', !CROSSHAIR.dot);
}

/* ============================ СОХРАНЕНИЕ ============================ */
function saveSettings(){
  try {
    localStorage.setItem('aimTrainer', JSON.stringify({
      c1: THEME.c1, c2: THEME.c2, gradient: THEME.gradient,
      uiColor: THEME.uiColor, menuBgEnabled: THEME.menuBgEnabled,
      menuBgColor: THEME.menuBgColor, textColor: THEME.textColor,
      sizeId: state.sizeId, sens: state.sens, fov: camera.fov,
      lastDuration: state.lastDuration,
      graphics: { level:GRAPHICS.level, shape:GRAPHICS.shape,
                  particles:GRAPHICS.particles, effectSize:GRAPHICS.effectSize,
                  bloomStrength:BLOOM.strength, exposure:GRAPHICS.exposure },
      hitEffect: HIT_EFFECT.mode, hitEffectSpeed: HIT_EFFECT.speed, hitEffectHdr: HIT_EFFECT.hdr,
      crosshair: CROSSHAIR, anim: ANIM, activeTab: state.activeSettingsTab
    }));
  } catch(e){ console.warn('Не удалось сохранить:', e); }
}
function loadSettings(){
  try {
    const raw = localStorage.getItem('aimTrainer');
    if (!raw) return;
    const s = JSON.parse(raw);
    if (typeof s.c1 === 'string') THEME.c1 = s.c1;
    if (typeof s.c2 === 'string') THEME.c2 = s.c2;
    if (typeof s.gradient === 'boolean') THEME.gradient = s.gradient;
    if (typeof s.uiColor === 'string') THEME.uiColor = s.uiColor;
    if (typeof s.menuBgEnabled === 'boolean') THEME.menuBgEnabled = s.menuBgEnabled;
    if (typeof s.menuBgColor === 'string') THEME.menuBgColor = s.menuBgColor;
    if (typeof s.textColor === 'string') THEME.textColor = s.textColor;
    if (typeof s.sizeId === 'string' && BALL_SIZES.some(b => b.id === s.sizeId)) state.sizeId = s.sizeId;
    if (typeof s.activeTab === 'string' && VALID_TABS.includes(s.activeTab)){
      state.activeSettingsTab = s.activeTab;
    }
    if (typeof s.sens === 'number' && s.sens >= 0.1 && s.sens <= 5) state.sens = s.sens;
    if (typeof s.lastDuration === 'number' && s.lastDuration >= 0) state.lastDuration = s.lastDuration;
    if (typeof s.fov === 'number' && s.fov >= 60 && s.fov <= 120){
      camera.fov = s.fov; camera.updateProjectionMatrix();
    }
    if (typeof s.graphics === 'string' && GRAPHICS_PRESETS[s.graphics]){
      GRAPHICS.level = s.graphics;
    } else if (s.graphics && typeof s.graphics === 'object'){
      if (GRAPHICS_PRESETS[s.graphics.level]) GRAPHICS.level = s.graphics.level;
      if (SHAPES.includes(s.graphics.shape)) GRAPHICS.shape = s.graphics.shape;
      if (typeof s.graphics.particles === 'number'){
        GRAPHICS.particles = Math.max(0, Math.min(1500, s.graphics.particles));
      }
      if (typeof s.graphics.effectSize === 'number'){
        GRAPHICS.effectSize = Math.max(0.3, Math.min(2.5, s.graphics.effectSize));
      }
      if (typeof s.graphics.bloomStrength === 'number'){
        BLOOM.strength = Math.max(0, Math.min(3, s.graphics.bloomStrength));
      }
      if (typeof s.graphics.exposure === 'number'){
        GRAPHICS.exposure = Math.max(0.3, Math.min(3, s.graphics.exposure));
      }
    }
    if (s.hitEffect && VALID_EFFECTS.includes(s.hitEffect)){
      HIT_EFFECT.mode = s.hitEffect;
    } else {
      HIT_EFFECT.mode = 'stars';
    }
    if (typeof s.hitEffectSpeed === 'number' && s.hitEffectSpeed >= 0.25 && s.hitEffectSpeed <= 3){
      HIT_EFFECT.speed = s.hitEffectSpeed;
    }
    if (typeof s.hitEffectHdr === 'number' && s.hitEffectHdr >= 0 && s.hitEffectHdr <= 3){
      HIT_EFFECT.hdr = s.hitEffectHdr;
    }
    if (s.crosshair && typeof s.crosshair === 'object') Object.assign(CROSSHAIR, s.crosshair);
    if (s.anim && typeof s.anim === 'object') Object.assign(ANIM, s.anim);
  } catch(e){ console.warn('Не удалось загрузить:', e); }
}

/* ============================ ЦЕЛИ ============================ */
let targets = [];
const geoCache = {};

function makeHeartShape(size){
  const shape = new THREE.Shape();
  const s = size;
  shape.moveTo(0, -s);
  shape.bezierCurveTo(s * 0.9, -s * 0.3, s * 0.9, s * 0.55, 0, s * 0.15);
  shape.bezierCurveTo(-s * 0.9, s * 0.55, -s * 0.9, -s * 0.3, 0, -s);
  return shape;
}
function makeTargetStarShape(size){
  const shape = new THREE.Shape();
  const outer = size * 1.15, inner = size * 0.48, points = 5, step = Math.PI / points;
  for (let i = 0; i < points * 2; i++){
    const r = (i % 2 === 0) ? outer : inner;
    const a = i * step - Math.PI / 2;
    const x = Math.cos(a) * r, y = Math.sin(a) * r;
    if (i === 0) shape.moveTo(x, y); else shape.lineTo(x, y);
  }
  shape.closePath();
  return shape;
}
function getGeo(radius, shape){
  const key = `${shape}_${radius.toFixed(3)}`;
  if (geoCache[key]) return geoCache[key];
  let g;
  const s = radius;
  switch (shape){
    case 'cube': g = new THREE.BoxGeometry(s * 1.55, s * 1.55, s * 1.55); break;
    case 'heart': {
      const shp = makeHeartShape(s * 1.15);
      g = new THREE.ExtrudeGeometry(shp, { depth:s * 0.7, bevelEnabled:true,
        bevelSize:s * 0.06, bevelThickness:s * 0.06, bevelSegments:2 });
      g.center(); break;
    }
    case 'star': {
      const shp = makeTargetStarShape(s);
      g = new THREE.ExtrudeGeometry(shp, { depth:s * 0.7, bevelEnabled:true,
        bevelSize:s * 0.05, bevelThickness:s * 0.05, bevelSegments:1 });
      g.center(); break;
    }
    case 'crystal': g = new THREE.OctahedronGeometry(s * 1.1, 0); break;
    default: g = new THREE.SphereGeometry(s, 24, 24);
  }
  SHARED_GEOS.add(g);
  geoCache[key] = g;
  return g;
}

function findFreeSpawnPosition(R, m){
  const z = m.depth ?? -13;
  const spread = m.spread ?? 1.0;
  const minY = 1.6;
  const maxY = 1.6 + 5.6 * spread;
  const minDist = R * 2 + R * 0.8;
  const ATTEMPTS = 50;
  let bestPos = null;
  let bestDist = -1;
  for (let i = 0; i < ATTEMPTS; i++){
    const x = (Math.random() - 0.5) * 13 * spread;
    const y = minY + Math.random() * (maxY - minY);
    let minD = Infinity;
    for (const t of targets){
      const dx = t.mesh.position.x - x;
      const dy = t.mesh.position.y - y;
      const dz = t.mesh.position.z - z;
      const d = Math.sqrt(dx*dx + dy*dy + dz*dz);
      if (d < minD) minD = d;
    }
    if (minD >= minDist){
      return { x, y, z };
    }
    if (minD > bestDist){
      bestDist = minD;
      bestPos = { x, y, z };
    }
  }
  return bestPos || { x: 0, y: (minY + maxY) * 0.5, z };
}

function spawnTarget(){
  const m = MODES[state.mode];
  if (!m) return;
  const R = getBallRadius();
  const mat = new THREE.MeshStandardMaterial({
    color: themeC1.clone(),
    emissive: themeC1.clone().multiplyScalar(0.45),
    emissiveIntensity: 0.85,
    roughness: 0.25,
    metalness: 0.15,
    transparent: true,
    opacity: 1
  });
  const mesh = new THREE.Mesh(getGeo(R, GRAPHICS.shape), mat);
  mesh.castShadow = true;

  const z = m.depth ?? -13;
  const spread = m.spread ?? 1.0;
  const t = { mesh, phase: Math.random()*10, life: m.spawnTimeout ?? Infinity, dying: false };

  if (m.type === 'track'){
    const speed = 4.5, ang = Math.random() * Math.PI * 2;
    t.vel = new THREE.Vector3(Math.cos(ang)*speed, Math.sin(ang)*speed, 0);
    t.bounds = { x:6.5, y0:1.9, y1:7.5 };
    mesh.position.set((Math.random()-0.5)*8, 2.5 + Math.random()*4, -11);
  } else if (m.moving){
    const pos = findFreeSpawnPosition(R, m);
    t.vel = new THREE.Vector3((Math.random() < 0.5 ? 1 : -1) * (3 + Math.random()*3), 0, 0);
    t.bounds = { x: 6.5 };
    mesh.position.set(pos.x, pos.y, pos.z);
  } else {
    const pos = findFreeSpawnPosition(R, m);
    mesh.position.set(pos.x, pos.y, pos.z);
  }

  targets.push(t);
  scene.add(mesh);
}
function clearTargets(){
  for (const t of targets){
    scene.remove(t.mesh);
    if (t.mesh.material) t.mesh.material.dispose();
  }
  targets = [];
}
function fillTargets(){
  const m = MODES[state.mode];
  if (!m) return;
  const alive = targets.reduce((n, t) => n + (t.dying ? 0 : 1), 0);
  let toSpawn = m.count - alive;
  while (toSpawn-- > 0) spawnTarget();
}
function removeTarget(t){
  const idx = targets.indexOf(t);
  if (idx === -1) return;
  scene.remove(t.mesh);
  if (t.mesh.material) t.mesh.material.dispose();
  targets.splice(idx, 1);
}
function applyShapeToTargets(){
  const m = state.mode ? MODES[state.mode] : null;
  if (!m) return;
  const newGeo = getGeo(getBallRadius(), GRAPHICS.shape);
  for (const t of targets) t.mesh.geometry = newGeo;
}

/* ============================ ЭФФЕКТЫ ============================ */
const effectGroup = new THREE.Group();
scene.add(effectGroup);
const effects = [];

function makeMat(color, opacity = 1, boost = null){
  const c = color.clone ? color.clone() : new THREE.Color(color);
  const mult = (boost !== null) ? boost : hdrMult();
  c.multiplyScalar(mult);
  return new THREE.MeshBasicMaterial({
    color: c, transparent: true, opacity,
    side: THREE.DoubleSide, depthTest: false, depthWrite: false,
    blending: THREE.AdditiveBlending });
}
function randDir(){
  return new THREE.Vector3(Math.random()-0.5, Math.random()-0.5, Math.random()-0.5).normalize();
}
function pushEffect(mesh, life, update, extra = {}){
  mesh.renderOrder = 1000;
  effectGroup.add(mesh);
  effects.push({ mesh, age: 0, life, update, ...extra });
}
function easeOutBack(t){
  const c1 = 1.70158, c3 = c1 + 1;
  return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}
function dampVel(vel, base, dt){
  vel.multiplyScalar(Math.pow(base, dt * 60));
}
function makeStarShape(outerR, innerR, points = 5){
  const shape = new THREE.Shape();
  const step = Math.PI / points;
  for (let i = 0; i < points * 2; i++){
    const r = (i % 2 === 0) ? outerR : innerR;
    const a = i * step - Math.PI / 2;
    const x = Math.cos(a) * r, y = Math.sin(a) * r;
    if (i === 0) shape.moveTo(x, y); else shape.lineTo(x, y);
  }
  shape.closePath();
  return shape;
}
let _starGeoFlat = null, _starGeoBig = null;
function getStarGeo(size){
  if (!_starGeoFlat){ _starGeoFlat = new THREE.ShapeGeometry(makeStarShape(0.08, 0.032, 5)); SHARED_GEOS.add(_starGeoFlat); }
  if (!_starGeoBig){ _starGeoBig = new THREE.ShapeGeometry(makeStarShape(0.55, 0.24, 5)); SHARED_GEOS.add(_starGeoBig); }
  return size === 'big' ? _starGeoBig : _starGeoFlat;
}

function effNone(pos){}

/* ---------- CIRCLE (только плавное уменьшение и исчезновение шара — без доп. визуала) ---------- */
function effCircle(pos){
  /* Никаких дополнительных частиц, колец или кружков.
     Вся анимация происходит с самим шаром (см. "умирающие" цели в animate()). */
}

/* ---------- STARS ---------- */
function effStars(pos){
  const c1 = themeC1.clone(), c2 = themeC2.clone();
  const S = effectScale;
  const pop = new THREE.Mesh(getStarGeo('big'), makeMat(c1));
  pop.position.copy(pos);
  pop.lookAt(camera.position);
  pop.scale.setScalar(0.05);
  pushEffect(pop, 0.42, (e, dt, t) => {
    const s = (0.05 + 2.55 * easeOutBack(t)) * S;
    e.mesh.scale.setScalar(s);
    e.mesh.rotation.z += dt * 6;
    e.mesh.material.opacity = 1 - t * t;
  });
  for (let i = 0; i < 10; i++){
    const s = new THREE.Mesh(getStarGeo('flat'), makeMat(i % 2 ? c2 : c1));
    s.position.copy(pos);
    s.lookAt(camera.position);
    const v = randDir().multiplyScalar((4 + Math.random() * 5) * S);
    v.y += 1.2 * S;
    const startScale = (0.55 + Math.random() * 0.9) * S;
    s.scale.setScalar(startScale);
    const rSpeed = (Math.random() - 0.5) * 14;
    pushEffect(s, 0.75 + Math.random() * 0.35, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.92, dt);
      e.vel.y -= dt * 5 * S;
      e.mesh.rotation.z += dt * e.rSpeed;
      const sc = startScale * (1 - t) * (1 - t);
      e.mesh.scale.setScalar(Math.max(0.001, sc));
      e.mesh.material.opacity = 1 - t * t;
    }, { vel: v, rSpeed });
  }
}

/* ---------- EXPLOSION ---------- */
function effExplosion(pos){
  const c1 = themeC1.clone(), c2 = themeC2.clone();
  const S = effectScale;
  const bright1 = c1.clone().lerp(new THREE.Color(0xffffff), 0.55);
  const bright2 = c2.clone().lerp(new THREE.Color(0xffffff), 0.55);
  for (let i = 0; i < 6; i++){
    const g = new THREE.SphereGeometry((0.1 + Math.random() * 0.08) * S, 10, 10);
    const col = i < 2 ? bright1 : (i < 4 ? bright2 : c1);
    const f = new THREE.Mesh(g, makeMat(col, 1, hdrMult(1.3)));
    const off = randDir().multiplyScalar(Math.random() * 0.35 * S);
    f.position.copy(pos).add(off);
    const sx = 1 + Math.random() * 0.7, sy = 1 + Math.random() * 0.7, sz = 1 + Math.random() * 0.7;
    f.scale.set(sx, sy, sz);
    const targetScale = 2.5 + Math.random() * 3.5;
    pushEffect(f, 0.32 + Math.random() * 0.18, (e, dt, t) => {
      const s = 1 + t * targetScale;
      e.mesh.scale.set(s * e.sx, s * e.sy, s * e.sz);
      e.mesh.material.opacity = (1 - t) * (1 - t);
    }, { sx, sy, sz });
  }
  for (let i = 0; i < 8; i++){
    const g = new THREE.ConeGeometry(0.08 * S, 0.55 * S, 5);
    const col = i % 2 ? c2 : c1;
    const s = new THREE.Mesh(g, makeMat(col));
    s.position.copy(pos);
    const d = randDir();
    s.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), d);
    const v = d.clone().multiplyScalar((4 + Math.random() * 4) * S);
    pushEffect(s, 0.4 + Math.random() * 0.2, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.9, dt);
      e.mesh.scale.z = 1 + (1 - t) * 2.2;
      e.mesh.scale.x = 1 - t * 0.6;
      e.mesh.scale.y = 1 - t * 0.6;
      e.mesh.material.opacity = 1 - t * t;
    }, { vel: v });
  }
  for (let i = 0; i < 18; i++){
    const useCube = Math.random() < 0.5;
    const g = useCube
      ? new THREE.BoxGeometry((0.06 + Math.random() * 0.04) * S, 0.06 * S, 0.06 * S)
      : new THREE.TetrahedronGeometry((0.07 + Math.random() * 0.04) * S);
    const s = new THREE.Mesh(g, makeMat(i % 2 ? c2 : c1));
    s.position.copy(pos);
    s.rotation.set(Math.random() * 6, Math.random() * 6, Math.random() * 6);
    const v = randDir().multiplyScalar((3 + Math.random() * 7) * S);
    const rSpd = new THREE.Vector3(
      (Math.random() - 0.5) * 18, (Math.random() - 0.5) * 18, (Math.random() - 0.5) * 18);
    pushEffect(s, 0.7 + Math.random() * 0.4, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.93, dt);
      e.vel.y -= dt * 12 * S;
      e.mesh.rotation.x += dt * e.rSpd.x;
      e.mesh.rotation.y += dt * e.rSpd.y;
      e.mesh.rotation.z += dt * e.rSpd.z;
      e.mesh.material.opacity = 1 - t * t;
    }, { vel: v, rSpd });
  }
  for (let i = 0; i < 26; i++){
    const col = i % 3 === 0 ? bright1 : (i % 3 === 1 ? c2 : c1);
    const s = new THREE.Mesh(
      new THREE.BoxGeometry(0.02 * S, 0.02 * S, 0.35 * S),
      makeMat(col, 1, hdrMult(1.4)));
    s.position.copy(pos);
    const d = randDir();
    s.lookAt(pos.clone().add(d));
    const v = d.multiplyScalar((9 + Math.random() * 9) * S);
    const stretch = 1 + Math.random() * 1.5;
    pushEffect(s, 0.35 + Math.random() * 0.3, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.88, dt);
      e.vel.y -= dt * 12 * S;
      e.mesh.scale.z = stretch * (1 + (1 - t) * 2);
      e.mesh.material.opacity = 1 - t * t;
    }, { vel: v });
  }
}

/* ---------- SHARDS ---------- */
function effShards(pos){
  const c1 = themeC1.clone(), c2 = themeC2.clone();
  const S = effectScale;
  const shardGeo = new THREE.ConeGeometry(0.08 * S, 0.22 * S, 3);
  for (let i = 0; i < 14; i++){
    const s = new THREE.Mesh(shardGeo, makeMat(i % 2 ? c2 : c1));
    s.position.copy(pos);
    s.rotation.set(Math.random()*6, Math.random()*6, Math.random()*6);
    const v = randDir().multiplyScalar((3 + Math.random() * 5) * S);
    const rSpeed = new THREE.Vector3((Math.random()-0.5)*12, (Math.random()-0.5)*12, (Math.random()-0.5)*12);
    pushEffect(s, 0.7 + Math.random() * 0.4, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.94, dt);
      e.vel.y -= dt * 8 * S;
      e.mesh.rotation.x += dt * e.rSpeed.x;
      e.mesh.rotation.y += dt * e.rSpeed.y;
      e.mesh.rotation.z += dt * e.rSpeed.z;
      e.mesh.material.opacity = 1 - t * t;
    }, { vel: v, rSpeed });
  }
}

/* ---------- LIGHTNING ---------- */
function makeLightningPath(start, end, segments, jitter){
  const pts = [start.clone()];
  for (let i = 1; i < segments; i++){
    const t = i / segments;
    const p = new THREE.Vector3().lerpVectors(start, end, t);
    p.x += (Math.random() - 0.5) * jitter * 2;
    p.z += (Math.random() - 0.5) * jitter * 2;
    pts.push(p);
  }
  pts.push(end.clone());
  return pts;
}
function createBolt(path, colA, colB, thickness, life){
  const segs = path.length - 1;
  for (let i = 0; i < segs; i++){
    const a = path[i], b = path[i + 1];
    const len = a.distanceTo(b);
    if (len < 0.001) continue;
    const t = segs > 1 ? i / segs : 0;
    const col = colA.clone().lerp(colB, t);
    const geo = new THREE.CylinderGeometry(thickness, thickness, len, 6);
    const mat = makeMat(col);
    const seg = new THREE.Mesh(geo, mat);
    seg.position.copy(a).add(b).multiplyScalar(0.5);
    seg.quaternion.setFromUnitVectors(
      new THREE.Vector3(0, 1, 0),
      b.clone().sub(a).normalize());
    pushEffect(seg, life, (e, dt, t2) => {
      const flick = 0.35 + 0.65 * Math.abs(Math.sin(t2 * 35 + e.mesh.position.x * 8));
      e.mesh.material.opacity = (1 - t2) * flick;
    });
  }
}
function effLightning(pos){
  const c1 = themeC1.clone();
  const c2 = themeC2.clone();
  const WHITE = new THREE.Color(0xffffff);
  const S = effectScale;
  const jitterS = Math.sqrt(S);
  const minThick = 0.012;

  const start = new THREE.Vector3(
    pos.x + (Math.random() - 0.5) * 1.5 * jitterS,
    13.8,
    pos.z + (Math.random() - 0.5) * 1.5 * jitterS);
  const end = pos.clone();

  const mainPath = makeLightningPath(start, end, 12, 0.55 * jitterS);
  createBolt(mainPath, c1, c2, Math.max(minThick, 0.05 * S), 0.55);
  createBolt(mainPath, WHITE, WHITE, Math.max(minThick * 0.55, 0.025 * S), 0.5);

  for (let b = 0; b < 3; b++){
    const idx = 4 + Math.floor(Math.random() * 5);
    const branchStart = mainPath[idx].clone();
    const branchEnd = branchStart.clone().add(new THREE.Vector3(
      (Math.random() - 0.5) * 3 * jitterS,
      (-1.2 - Math.random() * 1.8) * jitterS,
      (Math.random() - 0.5) * 3 * jitterS));
    const branchPath = makeLightningPath(branchStart, branchEnd, 6, 0.3 * jitterS);
    const branchA = b % 2 ? c2 : c1;
    const branchB = b % 2 ? c1 : c2;
    createBolt(branchPath, branchA, branchB, Math.max(minThick * 0.7, 0.03 * S), 0.45);
  }

  const ringInner = Math.max(0.06, 0.35 * S);
  const ringOuter = Math.max(0.1,  0.55 * S);
  const ring = new THREE.Mesh(
    new THREE.RingGeometry(ringInner, ringOuter, 32),
    makeMat(c1));
  ring.position.set(pos.x, 0.06, pos.z);
  ring.rotation.x = -Math.PI / 2;
  pushEffect(ring, 0.55, (e, dt, t) => {
    const maxScale = 5;
    e.mesh.scale.setScalar(1 + t * maxScale);
    e.mesh.material.opacity = (1 - t) * (1 - t);
  });

  for (let i = 0; i < 10; i++){
    const col = i % 2 ? c2 : c1;
    const s = new THREE.Mesh(
      new THREE.BoxGeometry(
        Math.max(0.008, 0.025 * S),
        Math.max(0.008, 0.025 * S),
        Math.max(0.09,  0.28  * S)),
      makeMat(col));
    s.position.copy(pos);
    const d = new THREE.Vector3(
      (Math.random() - 0.5) * 2,
      Math.random() * 1.2 + 0.1,
      (Math.random() - 0.5) * 2).normalize();
    s.lookAt(pos.clone().add(d));
    const v = d.multiplyScalar((5 + Math.random() * 5) * S);
    pushEffect(s, 0.4 + Math.random() * 0.3, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.88, dt);
      e.vel.y -= dt * 9 * S;
      e.mesh.scale.z = 1 + (1 - t) * 2;
      e.mesh.material.opacity = 1 - t * t;
    }, { vel: v });
  }
}

/* ---------- PLASMA (с градиентом при включённой теме) ---------- */
function effPlasma(pos){
  const c1 = themeC1.clone(), c2 = themeC2.clone();
  const S = effectScale;
  const minR = 0.03;
  const COUNT = 10;
  const useGrad = THEME.gradient;

  for (let i = 0; i < COUNT; i++){
    const tN = i / (COUNT - 1);
    const col = useGrad ? c1.clone().lerp(c2, tN) : c1.clone();
    const s = new THREE.Mesh(
      new THREE.SphereGeometry(Math.max(minR, 0.09 * S), 12, 12),
      makeMat(col, 1, hdrMult(1.2)));
    s.position.copy(pos);
    const ang = (i / COUNT) * Math.PI * 2;
    const v = new THREE.Vector3(Math.cos(ang), Math.sin(ang), 0)
      .multiplyScalar((3 + Math.random() * 3) * S);
    pushEffect(s, 0.55 + Math.random() * 0.3, (e, dt, t2) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.92, dt);
      const pulse = 1 + Math.sin(t2 * 20) * 0.3;
      e.mesh.scale.setScalar((1 - t2) * pulse);
      e.mesh.material.opacity = 1 - t2 * t2;
    }, { vel: v });
  }

  const coreCol = useGrad ? c1.clone().lerp(c2, 0.5) : c1.clone();
  const core = new THREE.Mesh(
    new THREE.SphereGeometry(Math.max(0.04, 0.13 * S), 16, 16),
    makeMat(coreCol, 1, hdrMult(1.5)));
  core.position.copy(pos);
  pushEffect(core, 0.4, (e, dt, t) => {
    e.mesh.scale.setScalar(1 + t * 3);
    e.mesh.material.opacity = (1 - t) * (1 - t);
  });
}

/* ---------- SAND ---------- */
let _sandGeos = null;
function getSandGeos(){
  if (_sandGeos) return _sandGeos;
  const geos = [
    new THREE.BoxGeometry(1, 1, 1.35),
    new THREE.TetrahedronGeometry(0.95),
    new THREE.OctahedronGeometry(0.85)
  ];
  geos.forEach(g => SHARED_GEOS.add(g));
  _sandGeos = geos;
  return geos;
}
function effSand(pos){
  const S = effectScale;
  const hdrUser = Math.max(0, HIT_EFFECT.hdr ?? 1);
  const hdrBoost = hdrUser;

  const base1 = themeC1.clone();
  const base2 = THEME.gradient ? themeC2.clone() : themeC1.clone();
  const WHITE = new THREE.Color(0xffffff);
  const BLACK = new THREE.Color(0x000000);
  const palette = [
    base1.clone(),
    base2.clone(),
    base1.clone().lerp(WHITE, 0.35),
    base2.clone().lerp(WHITE, 0.35),
    base1.clone().lerp(BLACK, 0.4),
    base2.clone().lerp(BLACK, 0.4)
  ];

  const FLOOR_Y = 0.02;
  const COUNT = 45;
  const geos = getSandGeos();

  const puffCol = THEME.gradient
    ? themeC1.clone().lerp(themeC2, 0.5)
    : themeC1.clone();
  const puffBrightness = 0.5 + 0.9 * hdrBoost;
  const puff = new THREE.Mesh(
    new THREE.SphereGeometry(0.14 * S, 14, 14),
    new THREE.MeshBasicMaterial({
      color: puffCol.clone().multiplyScalar(puffBrightness),
      transparent: true, opacity: 0.55,
      depthWrite: false,
      blending: THREE.AdditiveBlending
    }));
  puff.position.copy(pos);
  pushEffect(puff, 0.28, (e, dt, t) => {
    e.mesh.scale.setScalar(1 + t * 2.4);
    e.mesh.material.opacity = (1 - t) * 0.55;
  });

  for (let i = 0; i < COUNT; i++){
    const size = (0.02 + Math.random() * 0.03) * S;
    const geo = geos[i % geos.length];
    const col = palette[i % palette.length].clone();
    col.offsetHSL((Math.random()-0.5)*0.03, (Math.random()-0.5)*0.08, (Math.random()-0.5)*0.12);

    const mat = new THREE.MeshStandardMaterial({
      color: col,
      emissive: col.clone().multiplyScalar(0.6 * hdrBoost),
      emissiveIntensity: 0.9 * hdrBoost,
      metalness: 0.15,
      roughness: 0.85,
      flatShading: true,
      transparent: true,
      opacity: 1
    });

    const m = new THREE.Mesh(geo, mat);
    m.position.copy(pos);
    m.scale.setScalar(size);
    m.rotation.set(Math.random()*6, Math.random()*6, Math.random()*6);

    const angle = Math.random() * Math.PI * 2;
    const spread = 0.5 + Math.random() * 2.0;
    const v = new THREE.Vector3(
      Math.cos(angle) * spread * S,
      (0.1 + Math.random() * 1.6) * S,
      Math.sin(angle) * spread * S
    );

    const rSpd = new THREE.Vector3(
      (Math.random()-0.5)*24,
      (Math.random()-0.5)*24,
      (Math.random()-0.5)*24
    );

    const life = 2.2 + Math.random() * 1.2;

    pushEffect(m, life, (e, dt, t) => {
      if (e.grounded){
        const el = e.age - e.groundTime;
        const gt = Math.min(1, el / 0.55);
        e.mesh.material.opacity = e.groundOpacity * (1 - gt);
        return;
      }

      e.mesh.position.addScaledVector(e.vel, dt);
      e.vel.y -= dt * 14;
      e.vel.x *= Math.pow(0.92, dt * 60);
      e.vel.z *= Math.pow(0.92, dt * 60);

      e.mesh.rotation.x += dt * e.rSpd.x;
      e.mesh.rotation.y += dt * e.rSpd.y;
      e.mesh.rotation.z += dt * e.rSpd.z;

      if (e.mesh.position.y <= FLOOR_Y){
        e.mesh.position.y = FLOOR_Y;
        e.mesh.rotation.x = (Math.random() - 0.5) * 0.6;
        e.mesh.rotation.z = (Math.random() - 0.5) * 0.6;
        e.grounded = true;
        e.groundTime = e.age;
        e.groundOpacity = e.mesh.material.opacity;
      }

      e.mesh.material.opacity = Math.max(0, 1 - t * 0.35);
    }, { vel: v, rSpd, grounded: false, groundTime: 0, groundOpacity: 1 });
  }
}

/* ---------- SPARKS ---------- */
function effSparks(pos){
  const c1 = themeC1.clone(), c2 = themeC2.clone();
  const S = effectScale;
  for (let i = 0; i < 22; i++){
    const s = new THREE.Mesh(
      new THREE.BoxGeometry(Math.max(0.008, 0.03 * S), Math.max(0.008, 0.03 * S), Math.max(0.1, 0.5 * S)),
      makeMat(i % 2 ? c2 : c1, 1, hdrMult(1.3)));
    s.position.copy(pos);
    const d = randDir();
    s.lookAt(pos.clone().add(d));
    const v = d.clone().multiplyScalar((6 + Math.random() * 8) * S);
    const stretch = 1 + Math.random() * 1.5;
    pushEffect(s, 0.4 + Math.random() * 0.35, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.9, dt);
      e.vel.y -= dt * 10 * S;
      e.mesh.scale.z = stretch * (1 + (1 - t) * 1.6);
      e.mesh.material.opacity = 1 - t * t;
    }, { vel: v });
  }
}

/* ---------- CRYSTALS ---------- */
let _crystalGeos = null;
function getCrystalGeos(){
  if (_crystalGeos) return _crystalGeos;
  const geos = [
    new THREE.OctahedronGeometry(1, 0),
    new THREE.DodecahedronGeometry(1, 0),
    new THREE.IcosahedronGeometry(1, 0),
    new THREE.TetrahedronGeometry(1, 0)
  ];
  geos.forEach(g => SHARED_GEOS.add(g));
  _crystalGeos = geos;
  return geos;
}
function effCrystals(pos){
  const c1 = themeC1.clone();
  const c2 = themeC2.clone();
  const WHITE = new THREE.Color(0xffffff);
  const S = effectScale;
  const geos = getCrystalGeos();
  const COUNT = 12;
  const hdrBoost = Math.max(0, HIT_EFFECT.hdr ?? 1);

  for (let i = 0; i < COUNT; i++){
    const kind = i % geos.length;
    const baseR = Math.max(0.05, (0.09 + Math.random() * 0.05) * S);
    const col = i % 5 === 0 ? WHITE : (i % 2 ? c2 : c1);
    const darkBase = col.clone().multiplyScalar(0.35);

    const mat = new THREE.MeshStandardMaterial({
      color: darkBase,
      emissive: col.clone().multiplyScalar(hdrBoost),
      emissiveIntensity: 0.8 * hdrBoost,
      metalness: 0.7,
      roughness: 0.15,
      flatShading: true
    });

    const m = new THREE.Mesh(geos[kind], mat);
    m.position.copy(pos);

    const sx0 = baseR * (0.75 + Math.random() * 0.5);
    const sy0 = baseR * (1.15 + Math.random() * 0.95);
    const sz0 = baseR * (0.75 + Math.random() * 0.5);
    m.scale.set(sx0, sy0, sz0);
    m.rotation.set(
      Math.random() * Math.PI * 2,
      Math.random() * Math.PI * 2,
      Math.random() * Math.PI * 2
    );

    const dir = randDir();
    const v = dir.clone().multiplyScalar((2.4 + Math.random() * 4) * S);
    const rSpd = new THREE.Vector3(
      (Math.random() - 0.5) * 14,
      (Math.random() - 0.5) * 14,
      (Math.random() - 0.5) * 14
    );
    const life = 0.85 + Math.random() * 0.5;

    pushEffect(m, life, (e, dt, t) => {
      e.mesh.position.addScaledVector(e.vel, dt);
      dampVel(e.vel, 0.94, dt);
      e.vel.y -= dt * 4.5 * S;
      e.mesh.rotation.x += dt * e.rSpd.x;
      e.mesh.rotation.y += dt * e.rSpd.y;
      e.mesh.rotation.z += dt * e.rSpd.z;
      const grow = 1 + Math.sin(t * Math.PI) * 0.18;
      const fade = Math.max(0.0001, 1 - t * t * 0.85);
      e.mesh.scale.set(e.sx0 * grow * fade, e.sy0 * grow * fade, e.sz0 * grow * fade);
    }, { vel: v, rSpd, sx0, sy0, sz0 });
  }

  const flash = new THREE.Mesh(
    new THREE.SphereGeometry(Math.max(0.08, 0.14 * S), 12, 12),
    makeMat(WHITE, 0.9, hdrMult(1.5)));
  flash.position.copy(pos);
  pushEffect(flash, 0.28, (e, dt, t) => {
    e.mesh.scale.setScalar(1 + t * 4.5);
    e.mesh.material.opacity = (1 - t) * (1 - t) * 0.9;
  });
}

/* ---------- PORTALS ---------- */
function effPortals(pos){
  const R = Math.max(0.15, 0.42 * effectScale);
  const c1 = themeC1.clone(), c2 = themeC2.clone();
  const WHITE = new THREE.Color(0xffffff);
  const envelope = (t) => {
    if (t < 0.15) return t / 0.15;
    if (t > 0.7) return (1 - t) / 0.3;
    return 1;
  };
  const hole = new THREE.Mesh(new THREE.CircleGeometry(R * 1.05, 48),
    new THREE.MeshBasicMaterial({ color:0x000000, transparent:true, opacity:0.95,
      side:THREE.DoubleSide, depthTest:false, depthWrite:false }));
  hole.position.copy(pos);
  hole.lookAt(camera.position);
  pushEffect(hole, 0.95, (e, dt, t) => {
    const env = envelope(t);
    e.mesh.scale.setScalar(env);
    e.mesh.rotation.z += dt * 0.5;
    e.mesh.material.opacity = 0.95 * env;
  });
  const inner = new THREE.Mesh(new THREE.CircleGeometry(R * 0.75, 40),
    new THREE.MeshBasicMaterial({ color: c1.clone().multiplyScalar(0.18 * Math.max(0.5, HIT_EFFECT.hdr ?? 1)),
      transparent:true, opacity:0.75, side:THREE.DoubleSide,
      depthTest:false, depthWrite:false, blending:THREE.AdditiveBlending }));
  inner.position.copy(pos);
  inner.lookAt(camera.position);
  pushEffect(inner, 0.95, (e, dt, t) => {
    const env = envelope(t);
    const pulse = 0.85 + Math.sin(t * 28) * 0.12;
    e.mesh.scale.setScalar(env * pulse);
    e.mesh.rotation.z -= dt * 4;
    e.mesh.material.opacity = (1 - t) * env;
  });
  const rim = new THREE.Mesh(new THREE.RingGeometry(R * 0.95, R * 1.12, 60),
    makeMat(c1, 1, hdrMult(1.2)));
  rim.position.copy(pos);
  rim.lookAt(camera.position);
  pushEffect(rim, 0.95, (e, dt, t) => {
    const env = envelope(t);
    const pulse = 0.8 + Math.sin(t * 32) * 0.2;
    e.mesh.scale.setScalar(env);
    e.mesh.rotation.z += dt * 5;
    e.mesh.material.opacity = env * pulse;
  });
  const rim2 = new THREE.Mesh(new THREE.RingGeometry(R * 1.22, R * 1.35, 60),
    makeMat(c2, 1, hdrMult(1.1)));
  rim2.position.copy(pos);
  rim2.lookAt(camera.position);
  pushEffect(rim2, 0.95, (e, dt, t) => {
    const env = envelope(t);
    e.mesh.scale.setScalar(env);
    e.mesh.rotation.z -= dt * 3;
    e.mesh.material.opacity = env * (1 - t) * 0.8;
  });
  for (let i = 0; i < 8; i++){
    const arc = new THREE.Mesh(
      new THREE.TorusGeometry(R * 1.0, R * 0.055, 6, 20, Math.PI * 0.55),
      makeMat(i % 2 ? c2 : c1));
    arc.position.copy(pos);
    arc.lookAt(camera.position);
    const baseRot = (i / 8) * Math.PI * 2;
    arc.rotation.z = baseRot;
    pushEffect(arc, 0.95, (e, dt, t) => {
      const env = envelope(t);
      const r = 1 - t * 0.65;
      e.mesh.scale.setScalar(env * r);
      e.mesh.rotation.z = baseRot + t * 10;
      e.mesh.material.opacity = env * (1 - t);
    });
  }
  for (let i = 0; i < 2; i++){
    const torus = new THREE.Mesh(
      new THREE.TorusGeometry(R * 1.15, R * 0.045, 8, 40),
      makeMat(i === 0 ? c1 : WHITE, 1, hdrMult(1.3)));
    torus.position.copy(pos);
    const axis = new THREE.Vector3(
      Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
    pushEffect(torus, 0.95, (e, dt, t) => {
      const env = envelope(t);
      e.mesh.scale.setScalar(env);
      e.mesh.rotateOnAxis(axis, dt * 8);
      e.mesh.material.opacity = env * (1 - t) * 0.8;
    });
  }
  const STAR_COUNT = 20;
  for (let i = 0; i < STAR_COUNT; i++){
    const ang0 = (i / STAR_COUNT) * Math.PI * 2 + Math.random() * 0.4;
    const dist0 = R * (1.8 + Math.random() * 1.6);
    const z0 = (Math.random() - 0.5) * R * 1.4;
    const spark = new THREE.Mesh(
      new THREE.BoxGeometry(R * 0.055, R * 0.055, R * 0.35),
      makeMat(i % 3 === 0 ? WHITE : (i % 3 === 1 ? c1 : c2), 1, hdrMult(1.3)));
    spark.position.copy(pos);
    pushEffect(spark, 0.9, (e, dt, t) => {
      const ang = ang0 + t * 7;
      const dist = dist0 * (1 - t * 0.95);
      const z = z0 * (1 - t);
      e.mesh.position.set(
        pos.x + Math.cos(ang) * dist,
        pos.y + Math.sin(ang) * dist,
        pos.z + z);
      e.mesh.lookAt(pos);
      e.mesh.material.opacity = Math.max(0, 1 - t * 1.15);
    });
  }
  const flash = new THREE.Mesh(
    new THREE.SphereGeometry(Math.max(0.12, R * 0.55), 16, 16),
    makeMat(WHITE, 1, hdrMult(1.4)));
  flash.position.copy(pos);
  pushEffect(flash, 0.9, (e, dt, t) => {
    if (t < 0.72){ e.mesh.material.opacity = 0; return; }
    const rt = (t - 0.72) / 0.28;
    e.mesh.scale.setScalar(rt * 2.5);
    e.mesh.material.opacity = (1 - rt) * 0.9;
  });
}

function spawnHitEffect(pos){
  effectScale = GRAPHICS.effectSize || 1;
  const m = HIT_EFFECT.mode;
  if (m === 'none') return effNone(pos);
  if (m === 'circle') return effCircle(pos);
  if (m === 'stars') return effStars(pos);
  if (m === 'explosion') return effExplosion(pos);
  if (m === 'shards') return effShards(pos);
  if (m === 'lightning') return effLightning(pos);
  if (m === 'plasma') return effPlasma(pos);
  if (m === 'sand') return effSand(pos);
  if (m === 'sparks') return effSparks(pos);
  if (m === 'crystals') return effCrystals(pos);
  if (m === 'portals') return effPortals(pos);
}
function updateEffects(dt){
  const sdt = dt * HIT_EFFECT.speed;
  for (let i = effects.length - 1; i >= 0; i--){
    const e = effects[i];
    e.age += sdt;
    const t = e.age / e.life;
    if (t >= 1){
      effectGroup.remove(e.mesh);
      if (e.mesh.material) e.mesh.material.dispose();
      if (e.mesh.geometry && !SHARED_GEOS.has(e.mesh.geometry)) e.mesh.geometry.dispose();
      effects.splice(i, 1);
      continue;
    }
    e.update(e, sdt, t);
  }
}

/* ============================ ЗВУК ============================ */
let actx = null;
function ensureAudio(){
  if (!actx) actx = new (window.AudioContext || window.webkitAudioContext)();
  if (actx.state === 'suspended') actx.resume().catch(()=>{});
  return actx;
}
function beep(freq, dur, type='sine', vol=0.06){
  try {
    const ctx = ensureAudio();
    const o = ctx.createOscillator(), g = ctx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, ctx.currentTime);
    o.frequency.exponentialRampToValueAtTime(freq*0.4, ctx.currentTime+dur);
    g.gain.setValueAtTime(vol, ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+dur);
    o.connect(g); g.connect(ctx.destination);
    o.start(); o.stop(ctx.currentTime+dur);
  } catch(e){}
}
function tabSound(){
  try {
    const ctx = ensureAudio();
    const t0 = ctx.currentTime;

    const o1 = ctx.createOscillator(), g1 = ctx.createGain();
    o1.type = 'sine';
    o1.frequency.setValueAtTime(560, t0);
    o1.frequency.exponentialRampToValueAtTime(900, t0 + 0.11);
    g1.gain.setValueAtTime(0, t0);
    g1.gain.linearRampToValueAtTime(0.045, t0 + 0.012);
    g1.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.18);
    o1.connect(g1); g1.connect(ctx.destination);
    o1.start(t0); o1.stop(t0 + 0.2);

    const o2 = ctx.createOscillator(), g2 = ctx.createGain();
    o2.type = 'triangle';
    o2.frequency.setValueAtTime(1120, t0 + 0.02);
    o2.frequency.exponentialRampToValueAtTime(1500, t0 + 0.09);
    g2.gain.setValueAtTime(0, t0);
    g2.gain.linearRampToValueAtTime(0.02, t0 + 0.03);
    g2.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.15);
    o2.connect(g2); g2.connect(ctx.destination);
    o2.start(t0 + 0.02); o2.stop(t0 + 0.17);
  } catch(e){}
}
function clickSound(kind = 'default'){
  try {
    const ctx = ensureAudio();
    const t0 = ctx.currentTime;
    let f0 = 480, f1 = 780, vol = 0.05, dur = 0.14;

    if (kind === 'exit'){
      f0 = 620; f1 = 300; vol = 0.055; dur = 0.22;
    } else if (kind === 'play'){
      f0 = 420; f1 = 980; vol = 0.06; dur = 0.2;
    } else if (kind === 'back'){
      f0 = 620; f1 = 380; vol = 0.04; dur = 0.14;
    } else if (kind === 'small'){
      f0 = 700; f1 = 1000; vol = 0.03; dur = 0.09;
    }

    const o1 = ctx.createOscillator(), g1 = ctx.createGain();
    o1.type = 'sine';
    o1.frequency.setValueAtTime(f0, t0);
    o1.frequency.exponentialRampToValueAtTime(f1, t0 + dur);
    g1.gain.setValueAtTime(0, t0);
    g1.gain.linearRampToValueAtTime(vol, t0 + 0.008);
    g1.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
    o1.connect(g1); g1.connect(ctx.destination);
    o1.start(t0); o1.stop(t0 + dur + 0.02);

    const o2 = ctx.createOscillator(), g2 = ctx.createGain();
    o2.type = 'triangle';
    o2.frequency.setValueAtTime(f1 * 1.4, t0 + 0.012);
    o2.frequency.exponentialRampToValueAtTime(f1 * 1.9, t0 + 0.06);
    g2.gain.setValueAtTime(0, t0);
    g2.gain.linearRampToValueAtTime(vol * 0.5, t0 + 0.022);
    g2.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.1);
    o2.connect(g2); g2.connect(ctx.destination);
    o2.start(t0 + 0.012); o2.stop(t0 + 0.12);
  } catch(e){}
}

/* ============================ UI ============================ */
const $ = id => document.getElementById(id);
const screens = {
  main: $('screen-main'), play: $('screen-play'),
  settings: $('screen-settings'), exit: $('screen-exit')
};
const hudEl = $('hud'), pauseEl = $('pause'), resEl = $('results');
const overlay = $('overlay-time'), crosshairEl = $('crosshair'), trackBar = $('trackBar');

function showScreen(name){
  for (const k in screens) screens[k].classList.toggle('hidden', k !== name);
}

document.querySelectorAll('[data-go]').forEach(btn => {
  btn.onclick = () => {
    const target = btn.dataset.go;
    clickSound(target === 'settings' ? 'default' : 'play');
    showScreen(target);
  };
});
document.querySelectorAll('[data-back]').forEach(btn => {
  btn.onclick = () => {
    clickSound('back');
    showScreen('main');
  };
});
$('settingsBackBtn').onclick = () => {
  clickSound('back');
  if (state.settingsReturnTo === 'pause' && state.phase === 'paused'){
    screens.settings.classList.add('hidden');
    pauseEl.classList.remove('hidden');
    state.settingsReturnTo = null;
  } else {
    state.settingsReturnTo = null;
    showScreen('main');
  }
};
function tryExitGame(){
  clickSound('exit');
  try { window.close(); } catch(e){}
  setTimeout(() => { if (!window.closed) showScreen('exit'); }, 200);
}
$('exitBtnMain').onclick = tryExitGame;
$('closeTabBtn').onclick = tryExitGame;

function activateTab(tabName){
  if (!VALID_TABS.includes(tabName)) tabName = 'graphics';
  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
  const contents = document.querySelectorAll('.tabContent');
  contents.forEach(c => {
    const match = c.dataset.tab === tabName;
    if (match){
      c.classList.add('hidden');
      void c.offsetWidth;
      c.classList.remove('hidden');
    } else {
      c.classList.add('hidden');
    }
  });
  state.activeSettingsTab = tabName;
}
document.querySelectorAll('.tab').forEach(tab => {
  tab.onclick = () => {
    const name = tab.dataset.tab;
    if (name === state.activeSettingsTab) return;
    tab.classList.add('ripple');
    setTimeout(() => tab.classList.remove('ripple'), 20);
    tabSound();
    activateTab(name);
    saveSettings();
  };
});

const modesGrid = $('modesGrid');
for (const key in MODES){
  const m = MODES[key];
  const card = document.createElement('div');
  card.className = 'card';
  card.innerHTML = `${m.tag ? `<span class="tag">${m.tag}</span>` : ''}
    <h3>${m.name}</h3><p>${m.desc}</p>`;
  card.onclick = () => { clickSound('small'); openTimePicker(key); };
  modesGrid.appendChild(card);
}

let pendingMode = null;
let pendingSize = 'm';
function updateSizeButtons(){
  document.querySelectorAll('.sizeBtn').forEach(b => b.classList.toggle('active', b.dataset.size === pendingSize));
}
function openTimePicker(modeKey){
  pendingMode = modeKey;
  pendingSize = state.sizeId || 'm';
  $('timeTitle').textContent = MODES[modeKey].name;
  updateSizeButtons();
  overlay.classList.remove('hidden');
}
document.querySelectorAll('.sizeBtn').forEach(b => {
  b.onclick = () => { clickSound('small'); pendingSize = b.dataset.size; updateSizeButtons(); };
});
document.querySelectorAll('.tbtn[data-time]').forEach(btn => {
  btn.onclick = () => {
    clickSound('play');
    const t = parseInt(btn.dataset.time);
    overlay.classList.add('hidden');
    if (pendingMode) startGame(pendingMode, t, pendingSize);
  };
});
$('cancelTime').onclick = () => { clickSound('back'); overlay.classList.add('hidden'); };

document.querySelectorAll('.preset[data-preset]').forEach(b => {
  b.onclick = () => { clickSound('small'); applyGraphics(b.dataset.preset); };
});
function applyEffectMode(mode){
  if (!VALID_EFFECTS.includes(mode)) mode = 'stars';
  HIT_EFFECT.mode = mode;
  document.querySelectorAll('.effectBtn').forEach(b => b.classList.toggle('active', b.dataset.effect === mode));
  saveSettings();
}
document.querySelectorAll('.effectBtn').forEach(b => {
  b.onclick = () => { clickSound('small'); applyEffectMode(b.dataset.effect); };
});
function applyShape(shape){
  if (!SHAPES.includes(shape)) return;
  GRAPHICS.shape = shape;
  document.querySelectorAll('.shapeBtn').forEach(b => b.classList.toggle('active', b.dataset.shape === shape));
  applyShapeToTargets();
  saveSettings();
}
document.querySelectorAll('.shapeBtn').forEach(b => {
  b.onclick = () => { clickSound('small'); applyShape(b.dataset.shape); };
});

function updateParticlesLabel(v){ $('particlesVal').textContent = v === 0 ? 'Выкл' : v; }
$('particlesSlider').oninput = e => {
  GRAPHICS.particles = +e.target.value;
  updateParticlesLabel(GRAPHICS.particles);
  if (GRAPHICS.level === 'ultra') buildDetail('ultra');
  saveSettings();
};

$('bloomSlider').oninput = e => {
  BLOOM.strength = +e.target.value;
  $('bloomVal').textContent = BLOOM.strength.toFixed(2);
  if (bloomPass) bloomPass.strength = BLOOM.strength;
  saveSettings();
};
$('brightnessSlider').oninput = e => {
  GRAPHICS.exposure = +e.target.value;
  $('brightnessVal').textContent = GRAPHICS.exposure.toFixed(2);
  renderer.toneMappingExposure = GRAPHICS.exposure;
  saveSettings();
};
$('effBloomSlider').oninput = e => {
  HIT_EFFECT.hdr = +e.target.value;
  const v = HIT_EFFECT.hdr;
  $('effBloomVal').textContent = v === 0 ? 'ВЫКЛ' : v.toFixed(2) + '×';
  saveSettings();
};

$('effSizeSlider').oninput = e => {
  GRAPHICS.effectSize = +e.target.value;
  $('effSizeVal').textContent = GRAPHICS.effectSize.toFixed(2) + '×';
  saveSettings();
};

$('chThick').oninput = e => { CROSSHAIR.thickness = +e.target.value; $('chThickVal').textContent = e.target.value; applyCrosshair(); saveSettings(); };
$('chLen').oninput = e => { CROSSHAIR.length = +e.target.value; $('chLenVal').textContent = e.target.value; applyCrosshair(); saveSettings(); };
$('chGap').oninput = e => { CROSSHAIR.gap = +e.target.value; $('chGapVal').textContent = e.target.value; applyCrosshair(); saveSettings(); };
$('chDot').oninput = e => { CROSSHAIR.dotSize = +e.target.value; $('chDotVal').textContent = e.target.value; applyCrosshair(); saveSettings(); };
$('chDotToggle').onchange = e => { CROSSHAIR.dot = e.target.checked; applyCrosshair(); saveSettings(); };
$('chFixed').onchange = e => {
  CROSSHAIR.fixedColor = e.target.checked;
  if (CROSSHAIR.fixedColor) crosshairEl.classList.remove('hit','onTarget');
  saveSettings();
};
$('chColor').oninput = e => {
  CROSSHAIR.color = e.target.value;
  CROSSHAIR.syncTheme = false;
  $('chSyncTheme').checked = false;
  applyCrosshair();
  saveSettings();
};
$('chOpacity').oninput = e => {
  CROSSHAIR.opacity = +e.target.value / 100;
  $('chOpacityVal').textContent = e.target.value + '%';
  applyCrosshair();
  saveSettings();
};
$('chSyncTheme').onchange = e => {
  CROSSHAIR.syncTheme = e.target.checked;
  if (CROSSHAIR.syncTheme){
    CROSSHAIR.color = THEME.c1;
    $('chColor').value = THEME.c1;
  }
  applyCrosshair();
  saveSettings();
};
$('sens').oninput = e => { state.sens = +e.target.value; $('sensVal').textContent = state.sens.toFixed(2); saveSettings(); };
$('fov').oninput = e => {
  camera.fov = +e.target.value;
  camera.updateProjectionMatrix();
  $('fovVal').textContent = e.target.value;
  saveSettings();
};
$('effSpeed').oninput = e => {
  HIT_EFFECT.speed = +e.target.value;
  $('effSpeedVal').textContent = HIT_EFFECT.speed.toFixed(2) + '×';
  saveSettings();
};
$('animPistol').oninput = e => {
  ANIM.pistolSpeed = +e.target.value;
  $('animPistolVal').textContent = ANIM.pistolSpeed.toFixed(2) + '×';
  saveSettings();
};
$('color1').oninput = e => { THEME.c1 = e.target.value; applyTheme(); saveSettings(); };
$('color2').oninput = e => { THEME.c2 = e.target.value; applyTheme(); saveSettings(); };
$('gradToggle').onchange = e => {
  THEME.gradient = e.target.checked;
  $('color2Row').classList.toggle('hidden', !THEME.gradient);
  applyTheme();
  saveSettings();
};
$('uiColor').oninput = e => { THEME.uiColor = e.target.value; applyUiColor(); saveSettings(); };
$('menuBgToggle').onchange = e => {
  THEME.menuBgEnabled = e.target.checked;
  $('menuBgGated').classList.toggle('off', !THEME.menuBgEnabled);
  applyMenuBg();
  saveSettings();
};
$('menuBgColor').oninput = e => {
  THEME.menuBgColor = e.target.value;
  if (THEME.menuBgEnabled) applyMenuBg();
  saveSettings();
};
$('textColor').oninput = e => {
  THEME.textColor = e.target.value;
  document.documentElement.style.setProperty('--text', THEME.textColor);
  saveSettings();
};

/* ============================ УПРАВЛЕНИЕ ============================ */
document.addEventListener('mousemove', e => {
  if (state.phase !== 'playing') return;
  if (document.pointerLockElement !== renderer.domElement) return;
  const s = 0.0022 * state.sens;
  camera.rotation.y -= e.movementX * s;
  camera.rotation.x -= e.movementY * s;
  camera.rotation.x = Math.max(-Math.PI/2 + 0.02, Math.min(Math.PI/2 - 0.02, camera.rotation.x));
  state.swayX += (-e.movementX * 0.00035 - state.swayX) * 0.12;
  state.swayY += ( e.movementY * 0.00035 - state.swayY) * 0.12;
});
document.addEventListener('mousedown', e => {
  if (e.button !== 0) return;
  if (state.phase !== 'playing') return;
  if (document.pointerLockElement !== renderer.domElement) return;
  state.mouseDown = true;
  if (state.mode && MODES[state.mode].type === 'click') shoot();
});
document.addEventListener('mouseup', () => { state.mouseDown = false; });
document.addEventListener('pointerlockchange', () => {
  if (document.pointerLockElement !== renderer.domElement && state.phase === 'playing'){
    state.phase = 'paused';
    pauseEl.classList.remove('hidden');
    hudEl.classList.add('hidden');
    state.mouseDown = false;
    crosshairEl.classList.remove('hit','onTarget');
  }
});
document.addEventListener('keydown', e => {
  if (e.key === 'Escape' && state.phase === 'playing') document.exitPointerLock();
});
document.addEventListener('visibilitychange', () => {
  if (document.hidden && state.phase === 'playing') document.exitPointerLock();
});

/* ============================ СТРЕЛЬБА ============================ */
function shoot(){
  state.shots++;
  raycaster.setFromCamera(CENTER, camera);
  const activeTargets = targets.filter(t => !t.dying);
  const hits = raycaster.intersectObjects(activeTargets.map(t => t.mesh), false);
  if (hits.length){
    const obj = hits[0].object;
    const t = targets.find(x => x.mesh === obj);
    if (t){
      state.hits++;
      state.score += 100;
      spawnHitEffect(hits[0].point);

      if (HIT_EFFECT.mode === 'circle'){
        t.dying = true;
        t.deathAge = 0;
        t.deathDuration = 0.4;
        fillTargets();
      } else {
        removeTarget(t);
        fillTargets();
      }
      fireFeedback(true);
      return;
    }
  }
  fireFeedback(false);
}
function fireFeedback(hit){
  state.recoil = 1;
  state.recoilRot = 1;
  gun.userData.flashMat.opacity = 1;
  beep(hit ? 1000 : 160, 0.08, hit ? 'sine' : 'square', hit ? 0.08 : 0.04);
  if (hit && !CROSSHAIR.fixedColor){
    crosshairEl.classList.add('hit');
    if (crosshairHitTimer) clearTimeout(crosshairHitTimer);
    crosshairHitTimer = setTimeout(() => {
      crosshairEl.classList.remove('hit');
      crosshairHitTimer = null;
    }, 70);
  }
}

/* ============================ ИГРА ============================ */
function startGame(modeKey, duration, sizeId){
  const m = MODES[modeKey];
  if (!m) return;
  state.mode = modeKey;
  state.score = 0; state.shots = 0; state.hits = 0;
  state.infinite = duration === 0;
  state.timeLeft = state.infinite ? 0 : duration;
  state.elapsed = 0; state.trackTime = 0;
  state.phase = 'playing';
  state.settingsReturnTo = null;
  state.mouseDown = false;
  state.lastDuration = duration;
  if (sizeId && BALL_SIZES.some(b => b.id === sizeId)) state.sizeId = sizeId;
  const sz = BALL_SIZES.find(b => b.id === state.sizeId) || BALL_SIZES[2];
  state.sizeMultiplier = sz.mult;
  camera.rotation.set(0, 0, 0);
  camera.position.set(0, 1.7, 0);
  gun.position.copy(GUN_HOME);
  gun.rotation.set(0.02, -0.06, 0);
  clearTargets();
  fillTargets();
  document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
  resEl.classList.add('hidden');
  pauseEl.classList.add('hidden');
  hudEl.classList.remove('hidden');
  $('hMode').textContent = m.name;
  $('hAccLbl').textContent = m.type === 'track' ? 'УДЕРЖАНИЕ' : 'ТОЧНОСТЬ';
  trackBar.classList.toggle('hidden', m.type !== 'track');
  $('hTime').classList.remove('warn');
  const pl = renderer.domElement.requestPointerLock();
  if (pl && pl.catch) pl.catch(() => {});
  updateHUD();
  saveSettings();
}
function endGame(){
  state.phase = 'results';
  document.exitPointerLock();
  hudEl.classList.add('hidden');
  state.mouseDown = false;
  crosshairEl.classList.remove('hit','onTarget');
  const m = MODES[state.mode];
  if (!m) return;
  const acc = state.shots > 0 ? Math.round(state.hits / state.shots * 100) : 0;
  const trackAcc = state.elapsed > 0 ? Math.round(state.trackTime / state.elapsed * 100) : 0;
  $('rScore').textContent = Math.floor(state.score);
  $('rAcc').textContent = (m.type === 'track' ? trackAcc : acc) + '%';
  $('rAccLbl').textContent = m.type === 'track' ? 'Удержание' : 'Точность';
  $('rHits').textContent = m.type === 'track' ? '—' : state.hits;
  $('rShots').textContent = m.type === 'track' ? '—' : state.shots;
  resEl.classList.remove('hidden');
}
function updateHUD(){
  if (!state.mode) return;
  $('hScore').textContent = Math.floor(state.score);
  if (state.infinite){
    const t = Math.floor(state.elapsed);
    $('hTime').textContent = `${String(Math.floor(t/60)).padStart(2,'0')}:${String(t%60).padStart(2,'0')}`;
  } else {
    $('hTime').textContent = Math.ceil(Math.max(0, state.timeLeft));
  }
  if (state.timeLeft <= 5 && !state.infinite) $('hTime').classList.add('warn');
  const m = MODES[state.mode];
  if (!m) return;
  if (m.type === 'track'){
    const acc = state.elapsed > 0 ? Math.round(state.trackTime / state.elapsed * 100) : 100;
    $('hAcc').textContent = acc + '%';
    trackBar.querySelector('i').style.width = acc + '%';
  } else {
    const acc = state.shots > 0 ? Math.round(state.hits / state.shots * 100) : 100;
    $('hAcc').textContent = acc + '%';
  }
}
function toMenu(){
  state.phase = 'menu';
  state.settingsReturnTo = null;
  state.mouseDown = false;
  document.exitPointerLock();
  pauseEl.classList.add('hidden');
  resEl.classList.add('hidden');
  hudEl.classList.add('hidden');
  crosshairEl.classList.remove('hit','onTarget');
  clearTargets();
  showScreen('main');
}
$('resumeBtn').onclick = () => {
  clickSound('play');
  pauseEl.classList.add('hidden');
  hudEl.classList.remove('hidden');
  state.phase = 'playing';
  state.mouseDown = false;
  const pl = renderer.domElement.requestPointerLock();
  if (pl && pl.catch) pl.catch(() => {});
};
$('pauseSettingsBtn').onclick = () => {
  clickSound('default');
  state.settingsReturnTo = 'pause';
  pauseEl.classList.add('hidden');
  showScreen('settings');
};
$('pauseExitBtn').onclick = () => { clickSound('back'); toMenu(); };
$('againBtn').onclick = () => {
  clickSound('play');
  startGame(state.mode, state.lastDuration, state.sizeId);
};
$('menuBtn').onclick = () => { clickSound('back'); toMenu(); };

/* ============================ ЦИКЛ ============================ */
const clock = new THREE.Clock();
const raycaster = new THREE.Raycaster();
const CENTER = new THREE.Vector2(0, 0);
const tmpV = new THREE.Vector3();
const camDir = new THREE.Vector3();

function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);

  if (state.phase === 'playing' && state.mode){
    state.elapsed += dt;
    if (!state.infinite){
      state.timeLeft -= dt;
      if (state.timeLeft <= 0){ state.timeLeft = 0; endGame(); }
    }
    updateHUD();

    const m = MODES[state.mode];
    if (m.spawnTimeout){
      let needRespawn = false;
      for (let i = targets.length - 1; i >= 0; i--){
        if (targets[i].dying) continue;
        targets[i].life -= dt;
        if (targets[i].life <= 0){
          removeTarget(targets[i]);
          needRespawn = true;
        }
      }
      if (needRespawn) fillTargets();
    }
    if (m.moving){
      for (const t of targets){
        if (t.dying) continue;
        t.mesh.position.x += t.vel.x * dt;
        if (t.mesh.position.x < -t.bounds.x || t.mesh.position.x > t.bounds.x){
          t.vel.x *= -1;
          t.mesh.position.x = THREE.MathUtils.clamp(t.mesh.position.x, -t.bounds.x, t.bounds.x);
        }
        t.mesh.rotation.y += dt * 1.2;
      }
    }
    if (m.type === 'track'){
      for (const t of targets){
        if (t.dying) continue;
        t.mesh.position.x += t.vel.x * dt;
        t.mesh.position.y += t.vel.y * dt;
        if (t.mesh.position.x < -t.bounds.x || t.mesh.position.x > t.bounds.x){
          t.vel.x *= -1;
          t.mesh.position.x = THREE.MathUtils.clamp(t.mesh.position.x, -t.bounds.x, t.bounds.x);
        }
        if (t.mesh.position.y < t.bounds.y0 || t.mesh.position.y > t.bounds.y1){
          t.vel.y *= -1;
          t.mesh.position.y = THREE.MathUtils.clamp(t.mesh.position.y, t.bounds.y0, t.bounds.y1);
        }
        if (Math.random() < 0.02){
          t.vel.x += (Math.random()-0.5) * 7;
          t.vel.y += (Math.random()-0.5) * 7;
          if (t.vel.length() > 8) t.vel.setLength(8);
          if (t.vel.length() < 2.5) t.vel.setLength(2.5);
        }
        t.phase += dt * 3;
        t.mesh.scale.setScalar(1 + Math.sin(t.phase) * 0.05);
      }
      if (targets.length){
        const t = targets[0];
        camera.getWorldDirection(camDir);
        tmpV.subVectors(t.mesh.position, camera.position);
        const dist = tmpV.length();
        const angle = camDir.angleTo(tmpV.normalize());
        const ballR = getBallRadius();
        const hitAngle = Math.atan(ballR / dist) * 1.25;
        const onTarget = angle < hitAngle;
        if (onTarget && state.mouseDown){
          state.score += dt * 150;
          state.trackTime += dt;
          t.mesh.material.color.copy(themeC2);
          t.mesh.material.emissive.copy(themeC2);
          t.mesh.material.emissiveIntensity = 2.0;
          if (!CROSSHAIR.fixedColor) crosshairEl.classList.add('onTarget');
          if (Math.random() < 0.25) gun.userData.flashMat.opacity = 0.5;
        } else if (onTarget){
          t.mesh.material.color.copy(themeC2);
          t.mesh.material.emissive.copy(themeC2).multiplyScalar(0.7);
          t.mesh.material.emissiveIntensity = 1.3;
          if (!CROSSHAIR.fixedColor) crosshairEl.classList.add('onTarget');
        } else {
          t.mesh.material.color.copy(themeC1);
          t.mesh.material.emissive.copy(themeC1).multiplyScalar(0.45);
          t.mesh.material.emissiveIntensity = 0.85;
          crosshairEl.classList.remove('onTarget');
        }
      }
    }
    else if (!m.moving){
      const p = performance.now() * 0.004;
      for (const t of targets){
        if (t.dying) continue;
        t.mesh.material.emissiveIntensity = 0.7 + Math.sin(p + t.mesh.position.x) * 0.3;
        t.mesh.rotation.y += dt * 0.6;
      }
    } else {
      for (const t of targets){
        if (!t.dying) t.mesh.material.emissiveIntensity = 1.0;
      }
    }

    /* Анимация "умирающих" шаров (эффект кружок — плавное уменьшение и исчезновение) */
    for (let i = targets.length - 1; i >= 0; i--){
      const t = targets[i];
      if (t.dying){
        t.deathAge += dt;
        const k = Math.min(1, t.deathAge / t.deathDuration);
        const s = Math.max(0.001, 1 - k);
        t.mesh.scale.setScalar(s);
        if (t.mesh.material) t.mesh.material.opacity = Math.max(0, 1 - k);
        if (k >= 1) removeTarget(t);
      }
    }
  }

  const animSpeed = ANIM.pistolSpeed;
  state.recoil    = Math.max(0, state.recoil    - dt * 9  * animSpeed);
  state.recoilRot = Math.max(0, state.recoilRot - dt * 11 * animSpeed);
  const kick = state.recoil * state.recoil;

  gun.position.set(
    GUN_HOME.x + state.swayX,
    GUN_HOME.y + state.swayY - kick * 0.012,
    GUN_HOME.z + kick * 0.07);
  gun.rotation.x = 0.02 + kick * 0.35 + state.recoilRot * 0.02;
  gun.rotation.y = -0.06 + state.swayX * 0.4;
  gun.rotation.z = state.swayX * 0.3;

  const swayDecay = Math.pow(0.90, dt * 60);
  state.swayX *= swayDecay;
  state.swayY *= swayDecay;

  const fm = gun.userData.flashMat;
  fm.opacity = Math.max(0, fm.opacity - dt * 22 * animSpeed);
  gun.userData.flash.scale.setScalar(0.7 + fm.opacity * 1.6);

  updateEffects(dt);

  if (state.phase === 'playing'){
    const t = performance.now() * 0.0012;
    camera.position.y = 1.7 + Math.sin(t) * 0.006;
  }

  if (state.useComposer && composer){
    composer.render();
  } else {
    renderer.setRenderTarget(null);
    renderer.render(scene, camera);
  }
}

/* ============================ СТАРТ ============================ */
loadSettings();

$('color1').value = THEME.c1;
$('color2').value = THEME.c2;
$('gradToggle').checked = THEME.gradient;
$('color2Row').classList.toggle('hidden', !THEME.gradient);
$('uiColor').value = THEME.uiColor;

$('menuBgToggle').checked = THEME.menuBgEnabled;
$('menuBgColor').value = THEME.menuBgColor;
$('menuBgGated').classList.toggle('off', !THEME.menuBgEnabled);
$('textColor').value = THEME.textColor;

$('sens').value = state.sens;
$('sensVal').textContent = state.sens.toFixed(2);
$('fov').value = camera.fov;
$('fovVal').textContent = camera.fov;

$('particlesSlider').value = GRAPHICS.particles;
updateParticlesLabel(GRAPHICS.particles);

$('bloomSlider').value = BLOOM.strength;
$('bloomVal').textContent = BLOOM.strength.toFixed(2);
$('brightnessSlider').value = GRAPHICS.exposure;
$('brightnessVal').textContent = GRAPHICS.exposure.toFixed(2);

$('effBloomSlider').value = HIT_EFFECT.hdr;
$('effBloomVal').textContent = HIT_EFFECT.hdr === 0 ? 'ВЫКЛ' : HIT_EFFECT.hdr.toFixed(2) + '×';

$('effSizeSlider').value = GRAPHICS.effectSize;
$('effSizeVal').textContent = GRAPHICS.effectSize.toFixed(2) + '×';

document.querySelectorAll('.shapeBtn').forEach(b => {
  b.classList.toggle('active', b.dataset.shape === GRAPHICS.shape);
});

$('chThick').value = CROSSHAIR.thickness;   $('chThickVal').textContent = CROSSHAIR.thickness;
$('chLen').value   = CROSSHAIR.length;      $('chLenVal').textContent   = CROSSHAIR.length;
$('chGap').value   = CROSSHAIR.gap;         $('chGapVal').textContent   = CROSSHAIR.gap;
$('chDot').value   = CROSSHAIR.dotSize;     $('chDotVal').textContent   = CROSSHAIR.dotSize;
$('chDotToggle').checked = CROSSHAIR.dot;
$('chFixed').checked = CROSSHAIR.fixedColor;
$('chColor').value = CROSSHAIR.color;
$('chOpacity').value = Math.round(CROSSHAIR.opacity * 100);
$('chOpacityVal').textContent = Math.round(CROSSHAIR.opacity * 100) + '%';
$('chSyncTheme').checked = CROSSHAIR.syncTheme;

$('animPistol').value = ANIM.pistolSpeed;
$('animPistolVal').textContent = ANIM.pistolSpeed.toFixed(2) + '×';

$('effSpeed').value = HIT_EFFECT.speed;
$('effSpeedVal').textContent = HIT_EFFECT.speed.toFixed(2) + '×';

applyTheme();
applyCrosshair();
applyGraphics(GRAPHICS.level);
applyEffectMode(HIT_EFFECT.mode);
activateTab(state.activeSettingsTab);

initPostFX();

showScreen('main');
animate();
</script>
</body>
</html>