/**
 * Файл содержит все исходные коды шейдеров для проекта Matrix.
 * Разделение помогает фокусироваться на графике отдельно от логики.
 */

/**
 * Вершинный шейдер капель матричного дождя. Рисует каждую каплю как
 * инстансированный quad (2 треугольника, позиция берётся из a_position —
 * см. STRIDE=5 в lib.rs и раскладку атрибутов в initWebGL()). Неочевидное:
 * - UV-координаты в атлас (v_uv) считаются прямо здесь из a_charIdx и
 *   размера шрифта, а не передаются готовыми — атлас "нарезается" по
 *   формуле char_idx * fontSize по X, конкретная строка добавляется уже во
 *   фрагментном шейдере через a_atlasRow.
 * - Лёгкое "притяжение" символов к курсору (pushOffset/hoverScale): каждый
 *   инстанс сам проверяет расстояние до u_mouse в clip-пространстве с учётом
 *   аспекта экрана и слегка сдвигается/увеличивается — это чисто визуальный
 *   эффект на уровне вершин, независимый от физики капель в WASM.
 */
export const vsMatrix = `#version 300 es
precision mediump float;
in vec2 a_quad;
in vec2 a_position;
in float a_charIdx;
in float a_atlasRow;
in float a_scale;

uniform vec2 u_resolution;
uniform float u_fontSize;
uniform vec2 u_mouse;

out vec2 v_uv;
out float v_atlasRow;
out float v_dist;

void main() {
    float size = u_fontSize * a_scale;
    
    vec2 res = max(u_resolution, vec2(1.0));
    vec2 centerPos = a_position + vec2(size * 0.5);
    vec2 zeroToOneCenter = centerPos / res;
    vec2 aspect = vec2(res.x / res.y, 1.0);
    
    vec2 diff = zeroToOneCenter * aspect - u_mouse * aspect;
    float dist = length(diff);
    v_dist = dist;
    
    vec2 pushOffset = vec2(0.0);
    float hoverScale = 1.0;
    
    if (u_mouse.x > 0.001 && u_mouse.y > 0.001 && dist < 0.15 && dist > 0.0001) {
        float force = 1.0 - smoothstep(0.0, 0.15, dist);
        vec2 dir = diff / dist;
        pushOffset = dir * force * 40.0;
        hoverScale = 1.0 + force * 0.8;
    }

    vec2 finalPos = a_position + pushOffset + a_quad * (size * hoverScale);
    
    vec2 zeroToOne = finalPos / res;
    vec2 zeroToTwo = zeroToOne * 2.0;
    vec2 clipSpace = zeroToTwo - 1.0;
    
    gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
    
    v_uv = vec2(a_charIdx * u_fontSize + a_quad.x * u_fontSize, a_quad.y * u_fontSize);
    v_atlasRow = a_atlasRow;
}`.trim();

/**
 * Вершинный шейдер полноэкранного прохода пост-процессинга. Рисует один
 * quad на весь clip space (-1..1) без всякой трансформации — вся работа
 * пост-процесса происходит во фрагментном шейдере (postProcessSource),
 * этот шейдер лишь превращает clip-координаты в UV (0..1) для выборки из
 * промежуточной текстуры postTexture.
 */
export const vsOverlay = `#version 300 es
precision mediump float;
in vec2 a_position;
out vec2 v_uv;
void main() {
    v_uv = a_position * 0.5 + 0.5;
    gl_Position = vec4(a_position, 0.0, 1.0);
}`.trim();

/**
 * Вершинный шейдер глитч-блоков (VHS-подобные цветные прямоугольники,
 * см. GlitchBlock в lib.rs, буфер stride=5: x, y, w, h, color_type).
 * Неочевидное: newSize = originalSize * 3.0 — геометрия рисуется втрое
 * крупнее размеров w/h, посчитанных в Rust, а сдвиг offset центрирует
 * увеличенный quad вокруг исходной позиции. Так мелкие "осколки" из lib.rs
 * визуально превращаются в заметные, хорошо читаемые полосы-глитчи без
 * необходимости считать увеличенный размер уже на стороне симуляции.
 */
export const vsGlitch = `#version 300 es
in vec2 a_quad;
in vec4 a_glitchData;
in float a_colorType;
uniform vec2 u_resolution;
out float v_colorType;
void main() {
    vec2 originalSize = vec2(a_glitchData.z, a_glitchData.w);
    vec2 newSize = originalSize * 3.0; 
    vec2 offset = (newSize - originalSize) * 0.5;
    vec2 pixelPos = vec2(a_glitchData.x, a_glitchData.y) - offset + a_quad * newSize;
    vec2 zeroToOne = pixelPos / u_resolution;
    vec2 clipSpace = (zeroToOne * 2.0) - 1.0;
    gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
    v_colorType = a_colorType;
}`.trim();

/**
 * Фрагментный шейдер капель. Выбирает пиксель из текстурного атласа символов
 * по v_uv (X внутри символа/колонки) и v_atlasRow (какая из 11 строк —
 * градация яркости 0..9 или "белая голова" 10, см. buildAtlas() в
 * matrix_worker.js), затем discard-ит полностью прозрачные пиксели (пустое
 * пространство вокруг глифа в атласе), чтобы не тратить fill rate и не
 * ломать блендинг перекрывающихся капель. Дополнительно перекрашивает
 * символы рядом с курсором (v_dist из вершинного шейдера) в акцентный цвет.
 */
export const fsMatrix = `#version 300 es
precision mediump float;
in vec2 v_uv;
in float v_atlasRow;
in float v_dist;

uniform sampler2D u_atlas;
uniform float u_fontSize;
uniform vec2 u_atlasSize;
uniform vec2 u_mouse;

out vec4 outColor;

void main() {
    float yOffset = v_atlasRow * u_fontSize;
    vec2 finalUV = vec2(v_uv.x, v_uv.y + yOffset) / max(u_atlasSize, vec2(1.0));
    vec4 texColor = texture(u_atlas, finalUV);
    
    if (texColor.a < 0.001) discard; 
    
    if (u_mouse.x > 0.001 && u_mouse.y > 0.001 && v_dist < 0.15) {
        float intensity = 1.0 - smoothstep(0.0, 0.15, v_dist);
        vec3 targetColor = vec3(0.7, 0.0, 1.0); 
        texColor.rgb = mix(texColor.rgb, targetColor * texColor.a, intensity);
    }

    outColor = texColor;
}`.trim();

/**
 * Фрагментный шейдер глитч-блоков. Цвет блока целиком определяется
 * v_colorType (0..4), переданным из Rust (GlitchBlock::color_type,
 * распределение см. в GlitchBlock::new): фон темы, основной цвет темы,
 * инвертированный цвет, чистый белый и акцентный "красный сбой" —
 * никакой процедурной генерации цвета здесь нет, только ветвление по типу.
 */
export const fsGlitch = `#version 300 es
precision mediump float;
in float v_colorType;
uniform vec3 u_themeMain; 
uniform vec3 u_themeBg;   
out vec4 outColor;
void main() {
    int type = int(v_colorType + 0.1);
    if (type == 0) { outColor = vec4(u_themeBg, 1.0); } 
    else if (type == 1) { outColor = vec4(u_themeMain, 0.9); } 
    else if (type == 2) { outColor = vec4(vec3(1.0) - u_themeMain, 0.7); } 
    else if (type == 3) { outColor = vec4(1.0, 1.0, 1.0, 0.95); } 
    else if (type == 4) { outColor = vec4(1.0, 0.08, 0.31, 0.95); } 
    else { discard; }
}`.trim();

/**
 * Фоновый проход глубины: рисуется ПЕРВЫМ внутри postFramebuffer, до капель
 * дождя (см. render() в matrix_worker.js) — минималистичная сцена глубины
 * (небо и уходящий к линии горизонта тоннель данных: веер световых линий с
 * бегущими по ним в обе стороны частицами-пакетами). Существует отдельным
 * проходом, а не
 * как часть постпроцесса, по одной причине: канвас матрицы создаётся с
 * `alpha: false` и каждый кадр полностью перезаписывается сверху, поэтому
 * любой слой, размещённый в DOM ПОЗАДИ канваса, был бы невидим — единственный
 * способ дать дождю "глубину сзади" — нарисовать эту сцену первой внутри того
 * же фреймбуфера, чтобы капли (которые уже блендятся поверх фона через
 * SRC_ALPHA/ONE_MINUS_SRC_ALPHA) частично её перекрывали, а не полностью
 * скрывали.
 * Использует vsOverlay (тот же полноэкранный quad, что и постпроцесс).
 * Переиспользует vsOverlay — отдельный вершинный шейдер не нужен, оба прохода
 * рисуют один и тот же fullscreen-quad в clip space.
 */
export const fsBackground = `#version 300 es
precision highp float;

in vec2 v_uv;
out vec4 outColor;

uniform float u_time;
uniform vec2 u_resolution;
uniform float u_isDarkMode;
uniform float u_isVpnOn;
uniform float u_isLowQuality;

float hash11(float p) { return fract(sin(p * 127.1) * 43758.5453123); }

void main() {
    vec2 uv = v_uv; // 0..1, y=0 низ экрана, y=1 верх (см. vsOverlay: v_uv = a_position*0.5+0.5)
    bool isLight = u_isDarkMode < 0.5;
    // Акцентный цвет синхронизирован с палитрой капель/глаз: циан выкл, фиолет вкл.
    vec3 accent = mix(vec3(0.0, 0.85, 1.0), vec3(0.65, 0.15, 1.0), u_isVpnOn);
    // Коррекция соотношения сторон (тот же приём, что в vsMatrix/postProcessSource):
    // без него линии тоннеля и частицы-пакеты искажаются при смене aspect
    // ratio экрана, потому что uv.x и uv.y покрывают разное физическое
    // расстояние на пиксель. Умножаем x-компоненту локальных координат на
    // aspect.x перед любым distance-вычислением — тогда 1 условная единица по
    // x и y соответствует одному и тому же физическому расстоянию независимо
    // от формы экрана.
    vec2 aspect = vec2(u_resolution.x / max(u_resolution.y, 1.0), 1.0);

    float horizon = 0.46;
    vec3 skyTop = isLight ? vec3(0.85, 0.87, 0.93) : vec3(0.03, 0.035, 0.06);
    vec3 skyBottom = isLight ? vec3(0.90, 0.88, 0.90) : vec3(0.05, 0.04, 0.07);
    vec3 col = mix(skyBottom, skyTop, smoothstep(horizon, 1.0, uv.y));

    // Светящаяся полоса на линии горизонта — приглушена, чтобы не спорить с дождём.
    col += accent * exp(-abs(uv.y - horizon) * 60.0) * 0.28;

    if (uv.y < horizon) {
        // --- Тоннель данных: минималистичная метафора вместо дороги и машин —
        // веер сходящихся к горизонту световых линий (несущие "полосы"
        // тоннеля), редкие поперечные кольца глубины и бегущие по линиям
        // частицы-пакеты в обе стороны (входящий/исходящий трафик). Вся
        // геометрия — это abs()/exp()-затухание по расстоянию до линии/точки,
        // без SDF-фигур и без per-pixel шума текстуры, поэтому рендерится
        // заметно дешевле прежней дороги с машинами.
        float t = clamp(1.0 - uv.y / horizon, 0.0, 1.0);

        // "Пол" тоннеля — лёгкое затемнение к низу экрана вместо отдельной
        // текстуры асфальта.
        col = mix(col, skyBottom * 0.6, t * 0.55);

        // Веер несущих линий: N_LINES лучей от горизонта по той же кривой
        // перспективы pow(t, 1.6), что раньше задавала края дороги.
        const int N_LINES = 7;
        float spread = mix(0.006, 0.42, pow(t, 1.6));
        for (int li = 0; li < N_LINES; li++) {
            float lineT = (float(li) / float(N_LINES - 1)) * 2.0 - 1.0;
            float xLine = 0.5 + lineT * spread;
            float distX = abs(uv.x - xLine) * aspect.x;
            float lineWidth = mix(0.0025, 0.006, t);
            col += accent * exp(-distX / lineWidth) * 0.16;
        }

        // Поперечные "кольца глубины" — горизонтальные полосы во всю ширину
        // веера, бегущие от горизонта на зрителя (та же идея, что у прежней
        // прерывистой осевой линии дороги, но во всю ширину тоннеля). При
        // включённом VPN кольца бегут быстрее — метафора возросшей скорости.
        float ringSpeed = mix(0.5, 1.1, u_isVpnOn);
        float ringPhase = fract(t * 3.0 - u_time * ringSpeed);
        float ringProximity = abs(ringPhase - 0.5) * 2.0;
        float ringMask = smoothstep(0.06, 0.0, ringProximity) * smoothstep(0.0, 0.12, t);
        float distFromCenterX = abs(uv.x - 0.5) * aspect.x;
        if (distFromCenterX < spread * aspect.x) {
            col += accent * ringMask * 0.22;
        }

        // --- Пакеты данных: бегут по линиям веера в обе стороны — чётные
        // летят НА зрителя (входящий трафик), нечётные — ОТ зрителя
        // (исходящий). При включённом VPN пакетов больше и летят быстрее —
        // метафора возросшей защищённой пропускной способности. На слабых
        // устройствах (u_isLowQuality) активных пакетов вдвое меньше.
        float maxPackets = mix(6.0, 10.0, u_isVpnOn) * mix(1.0, 0.5, u_isLowQuality);
        float speedMul = mix(1.0, 2.0, u_isVpnOn);
        for (int i = 0; i < 10; i++) {
            float fi = float(i);
            if (fi >= maxPackets) continue;

            bool incoming = mod(fi, 2.0) < 0.5;
            float speed = (0.14 + hash11(fi * 7.0) * 0.06) * speedMul;
            float seed = hash11(fi * 13.0) * 10.0;
            float tc = fract(u_time * speed + seed);
            if (!incoming) tc = 1.0 - tc;

            // Пакет летит вдоль ТОЙ ЖЕ кривой веера, что и несущие линии
            // выше, — визуально "едет по рельсу", а не просто где-то рядом.
            float lineT = (mod(fi, float(N_LINES)) / float(N_LINES - 1)) * 2.0 - 1.0;
            float pSpread = mix(0.006, 0.42, pow(tc, 1.6));
            vec2 pos = vec2(0.5 + lineT * pSpread, horizon * (1.0 - tc));

            vec2 d = vec2((uv.x - pos.x) * aspect.x, uv.y - pos.y);
            float d2 = dot(d, d);
            float size = mix(0.0025, 0.012, tc);

            // Ранний выход: считаем ядро/блум пакета только рядом с ним самим.
            float boundR = size * 5.0;
            if (d2 > boundR * boundR) continue;

            float fade = smoothstep(0.0, 0.06, tc);
            vec3 packetCol = incoming ? mix(vec3(0.9, 0.97, 1.0), accent, 0.35) : accent * 0.75;

            float core = (1.0 - smoothstep(size * size * 0.3, size * size, d2)) * fade;
            col = mix(col, packetCol, core);
            float glow = exp(-d2 / (size * size * 2.5)) * fade * (incoming ? 0.5 : 0.3);
            col += packetCol * glow;
        }
    }

    outColor = vec4(col, 1.0);
}
`.trim();

/**
 * Полноэкранный фрагментный шейдер второго прохода — самый плотный файл
 * пайплайна. Читает готовую картинку матрицы+глитчей из postTexture
 * (см. render() в matrix_worker.js) и накладывает поверх все "объективные"
 * эффекты разом: CRT-дисторсию координат, хроматическую аберрацию,
 * VHS-тиры/разрывы кадра, ударную волну-рябь и процедурный рендер двух
 * кибер-глаз. Два прохода (матрица в текстуру, потом пост-процесс на экран)
 * нужны именно потому, что многие эффекты здесь (линзы, дисторсия, CA)
 * обязаны сэмплить УЖЕ готовое изображение матрицы со смещёнными
 * координатами — сделать это в один проход поверх инстансированных капель
 * невозможно, т.к. у каждой капли нет доступа к соседним пикселям.
 *
 * Самое неочевидное:
 * - crt_coords(uv, bend) — классическая barrel-дисторсия (координаты
 *   разъезжаются от центра к краям пропорционально offset^2), имитирует
 *   выпуклость старого CRT-монитора; отключается на isLowQuality для мобильных.
 * - applyClassicEye() — SDF-подобная функция века: createLidCurve() строит
 *   верхнее/нижнее веко смесью cos-дуги и степенной кривой (pow(1-|t|^p,1/p)),
 *   eyeSDF = max(y - upperY, lowerY - y) — расстояние до формы глаза одним
 *   числом (>0 снаружи века, <=0 внутри), дальше эта дистанция переиспользуется
 *   и для свечения века, и для ресниц (через hash-based "шипы"), и для радужки.
 * - Зрачок-"солнце": внутри радужки рисуется треугольник (правый глаз) или
 *   ромб (левый) через ручные SDF-выражения (dTri/dRom) — геометрическая
 *   заглушка вместо текстуры, чтобы не тащить лишний спрайт и держать эффект
 *   параметрическим (масштабируется вместе с scale/pixelScale).
 * - themePrimary/themeSecondary/themeBgDark/themeAccent и caruncleArea —
 *   захардкоженные цвета темы и "область слёзного мясца" (внутренний уголок
 *   глаза, caruncleArea), дающие глазам анатомическую достоверность без
 *   текстур; числа синхронизированы вручную с палитрой Tailwind/темой сайта
 *   (see themePrimary ~ violet-900, themeAccent ~ rose-600).
 * - u_vpnTimer/vpnTear — при переключении VPN на 1.0 секунды экран рвётся
 *   горизонтальными блоками с сдвигом (аналог VHS tracking error), давая
 *   ощутимый визуальный отклик на THEME-сообщение из воркера.
 */
export const postProcessSource = `#version 300 es
precision highp float;

in vec2 v_uv;
out vec4 outColor;

uniform sampler2D u_mainTex;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_eyeClosedness;
uniform float u_isVpnOn;
uniform float u_eyeActive;
uniform vec2 u_eyePosL;
uniform vec2 u_eyePosR;
uniform vec2 u_gaze; 
uniform vec3 u_shockwave;
uniform float u_isLowQuality;
uniform float u_distortion;
uniform float u_scanlines;
uniform float u_noise;
uniform float u_isDarkMode;
uniform float u_vpnTimer;
uniform float u_pixelScale; 

float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }

float noise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f*f*(3.0-2.0*f);
    float a = hash(i);
    float b = hash(i + vec2(1.0, 0.0));
    float c = hash(i + vec2(0.0, 1.0));
    float d = hash(i + vec2(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

vec2 crt_coords(vec2 uv, float bend) {
    uv = uv * 2.0 - 1.0;
    vec2 offset = abs(uv.yx) / vec2(bend);
    uv = uv + uv * offset * offset;
    return uv * 0.5 + 0.5;
}

vec2 createLidCurve(float t, float tilt, float sideSign, float closed, float powerUpper, float powerLower, float baseHeightUpper, float baseHeightLower) {
    float curveU = pow(1.0 - pow(abs(t), powerUpper), 1.0/powerUpper);
    float curveL = pow(1.0 - pow(abs(t), powerLower), 1.0/powerLower);
    
    float skewedT = t - tilt * (1.0 - t*t);
    float cosCurve = cos(skewedT * 1.57079632);
    
    float verticalTilt = skewedT * 2.5 * sideSign * (1.0 - closed); 
    
    float finalUpper = mix(cosCurve, curveU, 0.4) * baseHeightUpper + verticalTilt;
    float finalLower = mix(cosCurve, curveL, 0.5) * baseHeightLower - verticalTilt;
    
    return vec2(finalUpper, finalLower);
}

void applyClassicEye(vec2 uv, vec2 eyePosNormalized, vec2 gaze, float rawClosed, bool isVpn, bool isRightEye, inout vec3 col) {
    vec2 pixelPos = uv * u_resolution;
    vec2 center = eyePosNormalized * u_resolution;
    vec2 p = pixelPos - center;
    float sideSign = isRightEye ? 1.0 : -1.0;
    
    // ИСПРАВЛЕННЫЙ РАСЧЕТ МАСШТАБА ДЛЯ XIAOMI
    float logicalWidth = u_resolution.x;
    float scale = mix(0.75, 1.1, smoothstep(320.0, 1000.0, logicalWidth));
    // ----------------------------------------
    
    bool isLight = u_isDarkMode < 0.5;

    vec3 themePrimary   = vec3(0.3, 0.0, 0.6);   
    vec3 themeSecondary = vec3(0.0, 0.4, 0.5);   
    vec3 themeBgDark    = vec3(0.043, 0.051, 0.090); 
    vec3 themeBgLight   = vec3(0.973, 0.980, 0.988); 
    vec3 themeAccent    = vec3(1.0, 0.0, 0.2);   
    
    vec3 colorPurple = isVpn ? vec3(0.8, 0.0, 1.0) : themePrimary;
    vec3 colorCyan   = isVpn ? vec3(1.0, 0.0, 0.0) : themeSecondary; 
    vec3 eyeScleraColor = isLight ? themeBgLight : vec3(0.02); 
    vec3 eyeLinerColor  = isLight ? themePrimary * 0.3 : vec3(0.005); 

    float baseWidth = 72.0 * scale; 
    float baseHeightUpper = 32.0 * scale; 
    float baseHeightLower = 18.0 * scale; 
    
    float xNorm = clamp(p.x / baseWidth, -1.0, 1.0);
    float tilt = 0.15 * sideSign; 
    
    float bounce = sin(rawClosed * 3.1415 * 2.5) * 0.05 * sin(rawClosed * 3.1415);
    float closed = clamp(rawClosed - bounce, 0.0, 1.0);
    
    vec2 curves = createLidCurve(xNorm, tilt, sideSign, closed, 2.0, 1.8, baseHeightUpper, baseHeightLower);

    float meetY = mix(curves.x, -curves.y, 0.75); 
    
    float currentUpperY = mix(curves.x, meetY, closed);
    float currentLowerY = mix(-curves.y, meetY, closed); 

    float eyeSDF = max(p.y - currentUpperY, currentLowerY - p.y);
    float sdfScale = scale * mix(1.0, 0.6, closed);
    
    float horizontalFade = 1.0 - smoothstep(0.9, 1.0, abs(xNorm));
    if (horizontalFade == 0.0 || (p.x / baseWidth) < -1.0 || (p.x / baseWidth) > 1.0) return;

    if (eyeSDF > 0.0) {
        float glow = exp(-eyeSDF / (5.0 * sdfScale)) * horizontalFade; 
        if (isLight) {
            col = mix(col, colorPurple, glow * (isVpn ? 0.6 : 0.25));
        } else {
            col += colorPurple * glow * (isVpn ? 0.7 : 0.4);
        }
        
        float linerMask = (1.0 - smoothstep(0.0, 2.5 * sdfScale, eyeSDF)) * horizontalFade;
        col = mix(col, eyeLinerColor, linerMask);
        
        float edge = (1.0 - smoothstep(0.0, 1.2 * sdfScale, eyeSDF)) * horizontalFade;
        vec3 edgeColor = isLight ? themeSecondary : themeAccent;
        col = mix(col, edgeColor, edge * 0.9);
        
        float lashFreq = 22.0; 
        float ySign = sign(p.y - meetY);
        float lashFract = fract(xNorm * lashFreq + ySign * 0.2); 
        float spikeShape = 1.0 - smoothstep(0.0, 0.3, abs(lashFract - 0.5)); 
        
        float maxSpikeLen = (8.0 + hash(vec2(floor(xNorm * lashFreq), ySign)) * 12.0) * scale * horizontalFade;
        if (isVpn) maxSpikeLen *= 1.3; 
        
        float lashMask = 1.0 - smoothstep(0.0, 1.5 * sdfScale, eyeSDF - maxSpikeLen * spikeShape);
        
        if (lashMask > 0.0) {
            vec3 lashBaseCol = isLight ? themePrimary * 0.8 : vec3(0.01);
            vec3 lashTipCol = isVpn ? themeAccent : themeSecondary;
            float tipGradient = smoothstep(0.0, maxSpikeLen, eyeSDF);
            vec3 finalLashCol = mix(lashBaseCol, lashTipCol, tipGradient * 1.5);
            col = mix(col, finalLashCol, lashMask);
        }
        return;
    }

    vec3 innerCol = eyeScleraColor;
    
    float caruncleArea = isRightEye ? (1.0 - smoothstep(-1.0, -0.7, xNorm)) : smoothstep(0.7, 1.0, xNorm);
    vec3 caruncleCol = isLight ? vec3(0.8, 0.4, 0.4) : vec3(0.15, 0.02, 0.02);
    innerCol = mix(innerCol, caruncleCol, caruncleArea * smoothstep(0.0, 10.0*scale, curves.x - abs(p.y)) * 0.4);

    float gridSpeed = isVpn ? u_time * 12.0 : u_time * 3.0; 
    float currentFullHeight = p.y > 0.0 ? abs(currentUpperY) : abs(currentLowerY);
    float perspY = p.y / max(currentFullHeight, 0.1 * scale); 
    
    float gridY = fract((perspY * 10.0 * scale - gridSpeed) / (4.0 * scale)); 
    float gridLines = smoothstep(0.85, 1.0, gridY);
    float gridX = fract((p.x - (isVpn ? u_time * 15.0 : 0.0)) / (8.0 * scale));
    float gridLinesX = smoothstep(0.9, 1.0, gridX);
    
    float gridIntensity = gridLines + gridLinesX;
    vec3 gridActiveColor = isLight ? themePrimary * 0.6 : colorPurple * 0.5;
    float gridMix = isLight ? (gridIntensity * 0.5) : (gridIntensity * (0.2 + 0.8 * abs(perspY)));
    innerCol = mix(innerCol, gridActiveColor, gridMix * (1.0 - caruncleArea * 0.8));

    vec2 gazeOffset = gaze * vec2(15.0, 6.0) * scale;
    vec2 pupilP = p - gazeOffset;
    float r = length(pupilP);
    float irisRadius = 20.0 * scale; 
    
    float irisMask = 1.0 - smoothstep(irisRadius - 1.5 * scale, irisRadius, r);
    
    if (irisMask > 0.0) {
        innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.01), irisMask);
        float stripeDensity = isVpn ? 1.8 : 3.2;
        float sunStripes = step(0.4, fract((pupilP.y - u_time * 4.0) / (stripeDensity * scale)));
        
        vec3 colorBottom = isVpn ? vec3(1.0, 0.0, 0.4) : vec3(0.0, 0.7, 1.0); 
        vec3 colorTop    = isVpn ? vec3(0.8, 0.0, 1.0) : vec3(0.6, 0.0, 1.0); 
        
        vec3 sunGradient = mix(colorBottom, colorTop, smoothstep(-irisRadius, irisRadius, pupilP.y));
        float sunGlow = 1.0 - smoothstep(0.0, irisRadius, r);
        
        vec3 sunBaseColor;
        if (isLight) {
            sunBaseColor = sunGradient * (sunStripes * 0.5 + 0.5); 
        } else {
            sunBaseColor = sunGradient * (sunStripes * 0.7 + 0.3) * sunGlow * 2.0;
        }
        innerCol = mix(innerCol, sunBaseColor, irisMask);

        float rotAngle = u_time * 1.5 * (isRightEye ? 1.0 : -1.0);
        float s = sin(rotAngle), c = cos(rotAngle);
        vec2 rotPupil = vec2(pupilP.x * c - pupilP.y * s, pupilP.x * s + pupilP.y * c);
        float pupilRadius = isVpn ? 8.0 * scale : 5.0 * scale; 
        
        if (isRightEye) {
            float dTri = max(abs(rotPupil.x) * 0.866025 + rotPupil.y * 0.5, -rotPupil.y) - pupilRadius;
            float triMask = 1.0 - smoothstep(0.0, 1.5*scale, dTri);
            innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.0), triMask); 
            vec3 triNeon = isLight ? themePrimary : colorBottom * 2.5; 
            innerCol = mix(innerCol, triNeon, (1.0 - smoothstep(0.0, 1.5*scale, abs(dTri))) * triMask); 
        } else {
            float dRom = abs(rotPupil.x) + abs(rotPupil.y) - pupilRadius * 1.2;
            float romMask = 1.0 - smoothstep(0.0, 1.5*scale, dRom);
            innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.0), romMask);
            vec3 romNeon = isLight ? themePrimary : colorTop * 2.5; 
            innerCol = mix(innerCol, romNeon, (1.0 - smoothstep(0.0, 1.5*scale, abs(dRom))) * romMask);
        }
        float specH = (1.0 - smoothstep(0.0, 1.5*scale, abs(pupilP.y - 6.0*scale))) * (1.0 - smoothstep(0.0, 9.0*scale, abs(pupilP.x + 4.5*scale)));
        innerCol = mix(innerCol, vec3(1.0), specH * irisMask * (isLight ? 0.8 : 1.0));
    }

    if (isVpn) {
        float scanY = sin(u_time * 4.0) * baseHeightUpper;
        float scanMask = exp(-abs(p.y - scanY) / (1.2 * scale));
        innerCol = mix(innerCol, colorCyan, scanMask * (isLight ? 0.4 : 0.6));
    }

    float shadowOcclusion = smoothstep(0.0, 16.0 * scale, currentUpperY - p.y);
    if (p.y > -8.0*scale) { 
        if (isLight) {
            innerCol = mix(innerCol, themePrimary * 0.2, (1.0 - shadowOcclusion) * 0.15); 
        } else {
            innerCol *= (0.2 + 0.8 * shadowOcclusion);
        }
    }

    col = innerCol;
}

void main() {
    vec2 baseUv = (u_isLowQuality > 0.5) ? v_uv : crt_coords(v_uv, u_distortion);
    bool isLight = u_isDarkMode < 0.5;
    
    // ИСПРАВЛЕННЫЙ РАСЧЕТ МАСШТАБА ДЛЯ XIAOMI
    float pScale = max(u_pixelScale, 0.1);
    float logicalWidth = u_resolution.x;
    float scale = mix(0.75, 1.1, smoothstep(320.0, 1000.0, logicalWidth)) * pScale;
    // ----------------------------------------

    vec2 lensOffset = vec2(0.0);
    if (u_eyeActive > 0.5 && u_isLowQuality < 0.5) {
        vec2 eL = u_eyePosL / u_resolution; eL.y = 1.0 - eL.y;
        vec2 eR = u_eyePosR / u_resolution; eR.y = 1.0 - eR.y;
        
        vec2 pL = baseUv * u_resolution - eL * u_resolution;
        vec2 pR = baseUv * u_resolution - eR * u_resolution;
        
        float rL = length(pL);
        float rR = length(pR);
        float lensRad = 45.0 * scale; 
        
        float refrForce = (u_isVpnOn > 0.5) ? 0.15 : 0.04;
        
        if (rL < lensRad) {
            float profile = 1.0 - smoothstep(0.0, lensRad, rL);
            if (u_isVpnOn > 0.5) profile = floor(profile * 4.0) / 4.0; 
            lensOffset -= (pL / u_resolution) * refrForce * profile;
        }
        if (rR < lensRad) {
            float profile = 1.0 - smoothstep(0.0, lensRad, rR);
            if (u_isVpnOn > 0.5) profile = floor(profile * 4.0) / 4.0; 
            lensOffset -= (pR / u_resolution) * refrForce * profile;
        }
    }

    vec2 glitchOffset = vec2(0.0);
    
    vec2 grid = floor(baseUv * u_resolution / vec2(80.0, 40.0));
    float glitchVal = hash(grid + floor(u_time * 12.0)); 
    if (glitchVal > 0.998) glitchOffset.x += (hash(grid) - 0.5) * 0.05; 
    
    float tearHash = hash(vec2(floor(u_time * 8.0), 13.37));
    float isTearing = step(0.97, tearHash);
    float caOffset = 0.0;
    
    if (isTearing > 0.5) {
        float tearY = hash(vec2(floor(u_time * 8.0), 24.68));
        float tearBand = 1.0 - smoothstep(0.0, 0.04, abs(baseUv.y - tearY));
        glitchOffset.x += tearBand * (hash(vec2(baseUv.y * 100.0, u_time)) - 0.5) * 0.2;
        caOffset = tearBand * 0.05;
    }

    float vpnTear = 0.0;
    if (u_vpnTimer > 0.0) {
        float intensity = smoothstep(0.0, 1.0, u_vpnTimer);
        float tearBlock = floor(baseUv.y * 20.0 + u_time * 15.0);
        float blockHash = hash(vec2(tearBlock, floor(u_time * 10.0)));
        if (blockHash > 0.4) { 
            float shift = (hash(vec2(tearBlock, 2.0)) - 0.5) * 0.08 * intensity; 
            glitchOffset.x += shift;
            vpnTear = intensity; 
        }
    }

    float wave = 0.0;
    vec2 swDir = vec2(0.0);
    
    if (u_shockwave.z > 0.01) {
        vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);
        vec2 centerDiff = (baseUv + glitchOffset) * aspect - u_shockwave.xy * aspect;
        float baseDist = length(centerDiff);
        
        swDir = normalize(centerDiff);
        
        float angle = atan(swDir.y, swDir.x);
        float digitalNoise = hash(vec2(floor(angle * 12.0), floor(u_time * 8.0)));
        float spike = step(0.8, digitalNoise) * (digitalNoise - 0.8) * 0.4;
        float wobble = noise(vec2(angle * 6.0, u_time * 5.0)) * 0.04;
        float distortedDist = baseDist + spike - wobble;
        
        float ringWidth = 0.015;
        wave = (1.0 - smoothstep(u_shockwave.z, u_shockwave.z + ringWidth, distortedDist)) * smoothstep(u_shockwave.z - ringWidth, u_shockwave.z, distortedDist);
        
        glitchOffset -= swDir * wave * 0.025; 
    }

    vec2 bgUv = baseUv + lensOffset + glitchOffset;
    vec2 eyeUv = baseUv + glitchOffset;

    vec3 color = texture(u_mainTex, bgUv).rgb;
    
    vec3 themePrimary   = vec3(0.3, 0.0, 0.6);
    vec3 themeSecondary = vec3(0.0, 0.4, 0.5);
    vec3 themeAccent    = vec3(1.0, 0.0, 0.2);
    
    if (vpnTear > 0.0) {
        color.r = texture(u_mainTex, bgUv + vec2(0.015 * vpnTear, 0.0)).r;
        color.g = texture(u_mainTex, bgUv).g;
        color.b = texture(u_mainTex, bgUv - vec2(0.015 * vpnTear, 0.0)).b;
        
        float colorBand = fract(baseUv.y * 3.0 - u_time * 10.0);
        vec3 glitchColor = mix(themeSecondary, themeAccent, step(0.33, colorBand));
        glitchColor = mix(glitchColor, themePrimary, step(0.66, colorBand));
        
        color = mix(color, glitchColor, vpnTear * (isLight ? 0.25 : 0.35)); 
        color += glitchColor * vpnTear * 0.15;
    }

    if (caOffset > 0.0) {
        color.r = texture(u_mainTex, bgUv + vec2(caOffset, 0.0)).r;
        color.b = texture(u_mainTex, bgUv - vec2(caOffset, 0.0)).b;
        color += (isLight ? themePrimary : vec3(0.7, 0.2, 1.0)) * caOffset * 10.0;
    }

    if (wave > 0.0) {
        color.r = texture(u_mainTex, bgUv + swDir * wave * 0.005).r;
        color.b = texture(u_mainTex, bgUv - swDir * wave * 0.005).b;
    }

    if (u_eyeActive > 0.5) {
        vec2 eyeL = u_eyePosL / u_resolution;
        vec2 eyeR = u_eyePosR / u_resolution;
        eyeL.y = 1.0 - eyeL.y;
        eyeR.y = 1.0 - eyeR.y;
        
        applyClassicEye(eyeUv, eyeL, u_gaze, u_eyeClosedness, u_isVpnOn > 0.5, false, color);
        applyClassicEye(eyeUv, eyeR, u_gaze, u_eyeClosedness, u_isVpnOn > 0.5, true,  color);
    }

    if (wave > 0.0) {
        vec3 shockwaveColor = (u_isVpnOn > 0.5) ? vec3(1.0, 0.0, 0.3) : vec3(0.4, 0.0, 1.0);
        color += wave * 0.4 * shockwaveColor;
    }

    float scanline = sin(v_uv.y * u_resolution.y * 2.2) * 0.02;
    color -= isLight ? scanline * 0.5 : scanline; 
    float vignette = clamp(pow(16.0 * v_uv.x * v_uv.y * (1.0 - v_uv.x) * (1.0 - v_uv.y), 0.25), 0.0, 1.0);
    outColor = vec4(color * mix(0.9, 1.0, vignette), 1.0);
}
`.trim();
