legacy.html

<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sky Hero — Shader Portfolio</title>
<style>
  :root {
    --ink: #ffffff;
    --ink-soft: rgba(255, 255, 255, 0.85);
    --ink-faint: rgba(255, 255, 255, 0.65);
    --glass: rgba(255, 255, 255, 0.14);
    --glass-border: rgba(255, 255, 255, 0.35);
    --pill-dark: #0e1526;
    --card-ink: #101828;
    --card-soft: #475467;
    --headline-shadow: 0 3px 30px rgba(9, 40, 90, 0.35);
    --nav-shadow: 0 8px 32px rgba(9, 40, 90, 0.18);
  }
  html[data-theme="dark"] {
    --ink: #f2f6ff;
    --ink-soft: rgba(242, 246, 255, 0.85);
    --ink-faint: rgba(242, 246, 255, 0.6);
    --glass: rgba(10, 16, 34, 0.45);
    --glass-border: rgba(150, 170, 220, 0.22);
    --headline-shadow: 0 3px 30px rgba(0, 0, 0, 0.5);
    --nav-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { height: 100%; }
  body {
    font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
    color: var(--ink);
    overflow-x: hidden;
    background: #1877d2;
    transition: background 0.9s ease;
  }
  html[data-theme="dark"] body { background: #060a18; }

  #sky-canvas { position: fixed; inset: 0; width: 100%; height: 100%; display: block; z-index: 0; }

  .sky-fallback { position: fixed; inset: 0; z-index: 0; display: none;
    background: linear-gradient(180deg, #0d5fc4 0%, #2b8ae8 55%, #7cc0f4 100%); }
  html[data-theme="dark"] .sky-fallback { background: linear-gradient(180deg, #050914 0%, #0b1230 60%, #1a2350 100%); }
  .no-webgl .sky-fallback { display: block; }

  /* Fade the sky into a soft blur at the viewport edges. */
  .sky-edge-blur {
    position: fixed;
    left: 0;
    right: 0;
    z-index: 1;
    pointer-events: none;
    -webkit-backdrop-filter: blur(18px);
    backdrop-filter: blur(18px);
  }
  .sky-edge-blur.top {
    top: 0;
    height: 18svh;
    -webkit-mask-image: linear-gradient(to bottom, #000, transparent);
    mask-image: linear-gradient(to bottom, #000, transparent);
  }
  .sky-edge-blur.bottom {
    bottom: 0;
    height: 22svh;
    -webkit-mask-image: linear-gradient(to top, #000, transparent);
    mask-image: linear-gradient(to top, #000, transparent);
  }

  /* ---------- Nav ---------- */
  .nav {
    position: fixed;
    top: 18px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10;
    display: flex;
    align-items: center;
    gap: clamp(14px, 3vw, 34px);
    padding: 12px 14px 12px 22px;
    border-radius: 18px;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(18px) saturate(1.3);
    -webkit-backdrop-filter: blur(18px) saturate(1.3);
    box-shadow: var(--nav-shadow);
    white-space: nowrap;
    opacity: 0;
    animation: fade-down 0.9s ease 0.1s forwards;
  }
  .nav-logo { font-weight: 800; font-size: 17px; display: flex; align-items: center; gap: 8px; color: var(--ink); text-decoration: none; }
  .nav-logo span { font-size: 15px; }
  .nav a.link { color: var(--ink-soft); text-decoration: none; font-size: 14.5px; font-weight: 500; transition: color 0.2s; }
  .nav a.link:hover { color: var(--ink); }
  .nav .login {
    background: var(--glass);
    border: 1px solid var(--glass-border);
    color: var(--ink);
    padding: 9px 18px;
    border-radius: 12px;
    font-size: 14.5px;
    font-weight: 600;
    text-decoration: none;
    transition: background 0.2s;
  }
  .nav .login:hover { background: rgba(255, 255, 255, 0.25); }
  html[data-theme="dark"] .nav .login:hover { background: rgba(120, 150, 220, 0.2); }

  @media (max-width: 760px) {
    .nav a.link { display: none; }
    .nav a.link:nth-child(2) { display: inline; }
  }

  /* ---------- Hero ---------- */
  .hero {
    position: relative;
    z-index: 2;
    min-height: 100svh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 110px 20px 60px;
  }

  h1 {
    font-weight: 700;
    font-size: clamp(2.4rem, 6.5vw, 5.2rem);
    line-height: 1.06;
    letter-spacing: -0.025em;
    text-shadow: var(--headline-shadow);
    max-width: 14ch;
  }
  .line { display: block; overflow: hidden; padding-bottom: 0.1em; margin-bottom: -0.1em; }
  .word {
    display: inline-block;
    transform: translateY(115%);
    animation: rise 1s cubic-bezier(0.22, 0.68, 0.14, 1) forwards;
    animation-delay: var(--d, 0s);
  }

  /* ---------- Floating cards ---------- */
  .cards {
    position: relative;
    width: min(680px, 92vw);
    height: clamp(240px, 34vw, 330px);
    margin-top: clamp(28px, 5vh, 56px);
    opacity: 0;
    animation: fade-up 1s ease 0.9s forwards;
  }
  .card {
    position: absolute;
    background: #ffffff;
    border-radius: 20px;
    padding: 20px;
    box-shadow: 0 24px 60px rgba(8, 35, 80, 0.28), 0 4px 14px rgba(8, 35, 80, 0.12);
    text-align: left;
    color: var(--card-ink);
    animation: bob 7s ease-in-out infinite;
  }
  .card h3 { font-size: 17px; font-weight: 700; line-height: 1.35; }
  .card p { font-size: 12.5px; color: var(--card-soft); margin-top: 4px; }
  .card .label { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: #98a2b3; font-weight: 600; }
  .card .big { font-size: clamp(26px, 4vw, 38px); font-weight: 800; letter-spacing: -0.02em; }
  .pill {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    background: var(--pill-dark);
    color: #fff;
    border: none;
    font-size: 13.5px;
    font-weight: 600;
    padding: 10px 16px;
    border-radius: 999px;
    cursor: pointer;
    margin-top: 14px;
    text-decoration: none;
    transition: transform 0.2s;
  }
  .pill:hover { transform: scale(1.05); }
  .orb {
    width: 38px; height: 38px;
    border-radius: 999px;
    background: var(--pill-dark);
    color: #fff;
    display: grid;
    place-items: center;
    font-size: 15px;
    cursor: pointer;
    transition: transform 0.2s;
  }
  .orb:hover { transform: scale(1.1); }

  .card-a { left: 0%; top: 8%; width: 250px; transform: rotate(-7deg); z-index: 1; animation-delay: 0s; }
  .card-b { left: 50%; top: 22%; width: 280px; transform: translateX(-50%) rotate(2deg); z-index: 3; background: #10182b; color: #fff; animation-delay: 1.2s; }
  .card-b p { color: #98a2b3; }
  .card-b .row { display: flex; justify-content: space-between; align-items: flex-end; margin-top: 18px; }
  .card-b .row span { font-size: 12px; color: #98a2b3; }
  .card-b .orb { background: #232f4b; }
  .card-c { right: 0%; top: 12%; width: 230px; transform: rotate(7deg); z-index: 2; animation-delay: 2.2s; }
  .card-c .top { display: flex; justify-content: space-between; align-items: flex-start; }

  @keyframes bob {
    0%, 100% { margin-top: 0; }
    50% { margin-top: -12px; }
  }

  /* ---------- Bottom copy ---------- */
  .hero-sub {
    margin-top: clamp(30px, 6vh, 60px);
    max-width: 40ch;
    font-size: clamp(1rem, 1.6vw, 1.25rem);
    line-height: 1.6;
    color: var(--ink-soft);
    text-shadow: 0 1px 12px rgba(9, 40, 90, 0.25);
    opacity: 0;
    animation: fade-up 0.9s ease 1.25s forwards;
  }
  .cta {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    margin-top: 22px;
    background: #ffffff;
    color: #0e1526;
    font-size: 15px;
    font-weight: 700;
    padding: 15px 28px;
    border-radius: 999px;
    text-decoration: none;
    box-shadow: 0 14px 40px rgba(8, 35, 80, 0.3);
    transition: transform 0.25s ease, box-shadow 0.25s ease;
    opacity: 0;
    animation: fade-up 0.9s ease 1.4s forwards;
  }
  .cta:hover { transform: translateY(-3px); box-shadow: 0 20px 50px rgba(8, 35, 80, 0.4); }

  /* Theme toggle */
  .theme-toggle {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 11;
    width: 46px;
    height: 46px;
    border-radius: 999px;
    border: 1px solid var(--glass-border);
    background: var(--glass);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    display: grid;
    place-items: center;
    cursor: pointer;
    color: var(--ink);
    transition: transform 0.25s ease, box-shadow 0.25s ease;
  }
  .theme-toggle:hover { transform: scale(1.08); }
  .theme-toggle svg { width: 21px; height: 21px; position: absolute; transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease; }
  html[data-theme="light"] .icon-moon { transform: rotate(-90deg) scale(0.4); opacity: 0; }
  html[data-theme="dark"] .icon-sun { transform: rotate(90deg) scale(0.4); opacity: 0; }

  @keyframes rise { to { transform: translateY(0); } }
  @keyframes fade-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
  @keyframes fade-down { from { opacity: 0; transform: translate(-50%, -14px); } to { opacity: 1; transform: translate(-50%, 0); } }

  @media (max-width: 640px) {
    .cards { height: auto; display: flex; flex-direction: column; gap: 14px; align-items: center; }
    .card { position: static; transform: none !important; width: min(300px, 88vw) !important; }
    .card-b { transform: none !important; }
  }

  @media (prefers-reduced-motion: reduce) {
    .word { animation: none; transform: none; }
    .nav, .cards, .hero-sub, .cta { animation: none; opacity: 1; }
    .card { animation: none; }
    * { transition-duration: 0.01ms !important; }
  }
</style>
</head>
<body>
  <canvas id="sky-canvas" aria-hidden="true"></canvas>
  <div class="sky-fallback" aria-hidden="true"></div>
  <div class="sky-edge-blur top" aria-hidden="true"></div>
  <div class="sky-edge-blur bottom" aria-hidden="true"></div>

  <nav class="nav">
    <a class="nav-logo" href="#"><span>✈</span> Folio</a>
    <a class="link" href="#work">Work</a>
    <a class="link" href="#about">About</a>
    <a class="link" href="#writing">Writing</a>
    <a class="link" href="#contact">Contact</a>
    <a class="login" href="#contact">Hire me</a>
  </nav>

  <main class="hero">
    <h1 aria-label="Building immersive web experiences.">
      <span class="line" aria-hidden="true"><span class="word" style="--d: 0.2s">Building</span></span>
      <span class="line" aria-hidden="true"><span class="word" style="--d: 0.32s">immersive</span></span>
      <span class="line" aria-hidden="true"><span class="word" style="--d: 0.44s">web&nbsp;experiences.</span></span>
    </h1>

    <div class="cards" aria-hidden="false">
      <div class="card card-a">
        <h3>Design, code &amp; motion — crafted in one studio.</h3>
        <a class="pill" href="#work"><span>+</span> View my work</a>
      </div>

      <div class="card card-b">
        <div class="row" style="margin-top:0; align-items:center;">
          <span style="color:#fff; font-weight:600; font-size:13.5px;">✦ Aurora</span>
          <span>•••</span>
        </div>
        <div class="big">WebGL</div>
        <p style="text-align:right; margin-top:-6px;">Immersive 3D experience</p>
        <div class="row">
          <span>2026 · Case study</span>
          <button class="orb" aria-label="Open project">↗</button>
        </div>
      </div>

      <div class="card card-c">
        <div class="top">
          <div>
            <div class="label">Status</div>
            <h3 style="margin-top:6px;">Open for freelance</h3>
          </div>
          <button class="orb" aria-label="Book a call">↗</button>
        </div>
        <a class="pill" href="#contact">Book a call</a>
      </div>
    </div>

    <p class="hero-sub">
      Everything I build — projects, experiments and writing,
      gathered in one place under a sky that changes with your theme.
    </p>
    <a class="cta" href="#work"> See my work</a>
  </main>

  <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode" aria-pressed="false">
    <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
      <circle cx="12" cy="12" r="4"/>
      <path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>
    </svg>
    <svg class="icon-moon" viewBox="0 0 24 24" fill="currentColor">
      <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"/>
    </svg>
  </button>

<script>
(() => {
  "use strict";

  const root = document.documentElement;
  const canvas = document.getElementById("sky-canvas");
  const toggleBtn = document.getElementById("theme-toggle");
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
  const systemDark = window.matchMedia("(prefers-color-scheme: dark)");

  /* ---------------- Theme state ---------------- */
  const state = { day: 1, from: 1, to: 1, t0: 0, animating: false, duration: reducedMotion.matches ? 0 : 1900 };
  const clamp01 = (v) => Math.min(1, Math.max(0, v));
  const easeInOutCubic = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);

  function applyTheme(dark, { animate = true, persist = true } = {}) {
    root.dataset.theme = dark ? "dark" : "light";
    toggleBtn.setAttribute("aria-pressed", String(dark));
    if (persist) { try { localStorage.setItem("sky-theme", dark ? "dark" : "light"); } catch {} }
    const target = dark ? 0 : 1;
    if (animate && state.duration > 0) {
      state.from = state.day; state.to = target; state.t0 = performance.now(); state.animating = true;
    } else { state.day = target; state.animating = false; }
  }

  let saved = null;
  try { saved = localStorage.getItem("sky-theme"); } catch {}
  applyTheme(saved ? saved === "dark" : systemDark.matches, { animate: false, persist: false });

  toggleBtn.addEventListener("click", () => applyTheme(root.dataset.theme !== "dark"));
  systemDark.addEventListener("change", (e) => {
    let manual = false;
    try { manual = !!localStorage.getItem("sky-theme"); } catch {}
    if (!manual) applyTheme(e.matches);
  });

  /* ---------------- WebGL sky ---------------- */
  const VERT = `
    attribute vec2 aPos;
    varying vec2 vUv;
    void main() {
      vUv = aPos * 0.5 + 0.5;
      gl_Position = vec4(aPos, 0.0, 1.0);
    }
  `;

  const FRAG = `
    precision highp float;
    varying vec2 vUv;
    uniform float uTime;
    uniform float uDay;   // 0 night -> 1 day
    uniform vec2  uRes;

    float hash(vec2 p) {
      p = fract(p * vec2(123.34, 456.21));
      p += dot(p, p + 45.32);
      return fract(p.x * p.y);
    }
    float noise(vec2 p) {
      vec2 i = floor(p), f = fract(p);
      vec2 u = f * f * (3.0 - 2.0 * f);
      return mix(mix(hash(i), hash(i + vec2(1,0)), u.x),
                 mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y);
    }
    float fbm(vec2 p) {
      float v = 0.0, amp = 0.5;
      mat2 rot = mat2(0.8, 0.6, -0.6, 0.8);
      for (int i = 0; i < 4; i++) { v += amp * noise(p); p = rot * p * 2.03; amp *= 0.5; }
      return v;
    }

    // Billowing cumulus field in cloud-local space (q, y-up).
    // Radial envelope + fbm billows + flat-bottom carve = fluffy top, flat base.
    float cloudMap(vec2 q, float seedT) {
      float f = fbm(q * 1.35 + vec2(seedT, seedT * 0.13));
      float env = 1.0 - smoothstep(0.30, 1.30, length(q * vec2(0.60, 1.05)));
      float d = f * 1.35 * env;
      float carve = 1.0 - smoothstep(-0.45, -0.05, q.y);  // carve flat bottoms
      d -= carve * 0.75;
      return d;
    }

    // One cumulus cluster centered at c with world radius s. Returns alpha,
    // writes lit shading factor (top-lit vs base shade).
    float cloud(vec2 p, vec2 c, float s, float seed, out float lit) {
      vec2 q = (p - c) / s;
      float env = 1.0 - smoothstep(0.30, 1.30, length(q * vec2(0.60, 1.05)));
      lit = 0.0;
      if (env < 0.004) return 0.0;
      float d = cloudMap(q, seed + uTime * 0.05);
      float a = smoothstep(0.30, 0.55, d);
      float dUp = cloudMap(q + vec2(0.0, 0.30), seed + uTime * 0.05);
      lit = clamp(0.55 + (d - dUp) * 2.4, 0.0, 1.0);
      return a;
    }

    void main() {
      float aspect = uRes.x / uRes.y;
      vec2 p = vec2((vUv.x - 0.5) * aspect, vUv.y - 0.5);
      float h = clamp(vUv.y, 0.0, 1.0);
      float day = uDay;
      float t = uTime;

      // Sunset factor peaks mid-transition
      float s = smoothstep(0.05, 0.5, day) * smoothstep(1.0, 0.55, day);

      // --- Vivid sky gradient (reference-style azure) ---
      vec3 nightTop = vec3(0.012, 0.020, 0.060);
      vec3 nightHor = vec3(0.050, 0.080, 0.170);
      vec3 dayTop   = vec3(0.075, 0.400, 0.870);
      vec3 dayHor   = vec3(0.480, 0.720, 0.970);
      vec3 duskTop  = vec3(0.14, 0.10, 0.28);
      vec3 duskHor  = vec3(0.99, 0.44, 0.20);
      vec3 top = mix(nightTop, dayTop, day);
      vec3 hor = mix(nightHor, dayHor, day);
      top = mix(top, duskTop, s * 0.9);
      hor = mix(hor, duskHor, s);
      vec3 sky = mix(hor, top, pow(h, 0.80));

      // --- Sun: soft glow + small disc, mostly behind the big cloud (day) ---
      vec2 sunPos = vec2(0.44 * aspect, 0.36);
      float sd = length(p - sunPos);
      float sunDisc = smoothstep(0.052, 0.040, sd);
      float sunGlow = pow(max(0.0, 1.0 - sd * 1.25), 3.0);
      sky += (sunDisc * vec3(1.0, 0.98, 0.88) * 0.9 + sunGlow * vec3(1.0, 0.88, 0.6) * 0.30) * day;

      // --- Moon: crescent + glow (night) ---
      vec2 moonPos = vec2(-0.30 * aspect, 0.32);
      float md = length(p - moonPos);
      float moonDisc = smoothstep(0.060, 0.050, md);
      float carve = smoothstep(0.050, 0.038, length(p - (moonPos + vec2(0.025, 0.013))));
      float moonShape = clamp(moonDisc - carve * 0.92, 0.0, 1.0);
      float moonGlow = pow(max(0.0, 1.0 - md * 2.1), 3.0) * 0.35;
      sky += (moonShape * vec3(0.93, 0.96, 1.0) + moonGlow * vec3(0.60, 0.72, 1.0)) * (1.0 - day);

      // --- Stars (night, above horizon) ---
      vec2 cell = floor(p * 95.0);
      vec2 f = fract(p * 95.0);
      float rnd = hash(cell);
      float star = smoothstep(0.965, 1.0, rnd);
      vec2 starPos = vec2(hash(cell + 7.3), hash(cell + 13.7)) * 0.8 + 0.1;
      float twinkle = 0.55 + 0.45 * sin(uTime * 2.2 + rnd * 47.0);
      star *= smoothstep(0.13, 0.0, length(f - starPos)) * twinkle;
      sky += star * (1.0 - day) * smoothstep(0.04, 0.45, h) * vec3(0.85, 0.92, 1.0);

      // --- Cloud colors: sunlit white / gray-blue bases (day), moonlit navy (night) ---
      vec3 litDay     = vec3(1.00, 1.00, 1.00);
      vec3 shadeDay   = vec3(0.58, 0.70, 0.86);
      vec3 litNight   = vec3(0.34, 0.40, 0.56);
      vec3 shadeNight = vec3(0.050, 0.070, 0.140);
      vec3 cLit  = mix(litNight, litDay, day);
      vec3 cShade = mix(shadeNight, shadeDay, day);
      cLit   = mix(cLit,   vec3(1.00, 0.62, 0.38), s * 0.85);  // sunset rim
      cShade = mix(cShade, vec3(0.42, 0.22, 0.30), s * 0.55);

      // --- Distant haze clouds near the horizon (atmospheric perspective) ---
      {
        float band = exp(-pow((p.y + 0.16) * 5.5, 2.0));
        vec2 dq = vec2(p.x * 3.2 + t * 0.010, p.y * 9.0);
        float dd = fbm(dq) * band * 1.35;
        float da = smoothstep(0.40, 0.62, dd);
        float dUp2 = fbm(dq + vec2(0.0, 0.55)) * band * 1.35;
        float lit2 = clamp(0.55 + (dd - dUp2) * 2.2, 0.0, 1.0);
        vec3 dcol = mix(cShade, cLit, lit2);
        dcol = mix(dcol, hor, 0.35);  // fade into sky (distance haze)
        sky = mix(sky, dcol, da * 0.75);
      }

      // --- Cumulus clusters, far -> near (parallax drift + wrap) ---
      float cw = 1.35 * aspect;
      #define WRAPX(x, spd) (mod((x) + t * (spd) + cw, 2.0 * cw) - cw)

      // Big clusters hug the corners (like the reference); center stays clear.
      vec2 c6pos = vec2(WRAPX(-0.80 * aspect, 0.010), 0.06);
      vec2 c5pos = vec2(WRAPX( 0.55 * aspect, 0.007), 0.12);
      vec2 c4pos = vec2(WRAPX(-0.55 * aspect, 0.012), -0.33);
      vec2 c3pos = vec2(WRAPX( 0.74 * aspect, 0.014),  0.32);
      vec2 c2pos = vec2(WRAPX( 0.16 * aspect, 0.009), -0.38);
      vec2 c1pos = vec2(WRAPX(-0.52 * aspect, 0.006),  0.42);

      float lit; float a;
      a = cloud(p, c6pos, 0.20, 31.7, lit); sky = mix(sky, mix(cShade, cLit, lit), a * 0.9);
      a = cloud(p, c5pos, 0.13, 52.1, lit); sky = mix(sky, mix(cShade, cLit, lit), a * 0.8);
      a = cloud(p, c4pos, 0.24, 27.9, lit); sky = mix(sky, mix(cShade, cLit, lit), a * 0.95);
      a = cloud(p, c3pos, 0.42,  4.7, lit); sky = mix(sky, mix(cShade, cLit, lit), a * 0.97);
      a = cloud(p, c2pos, 0.16, 63.3, lit); sky = mix(sky, mix(cShade, cLit, lit), a);
      a = cloud(p, c1pos, 0.55,  7.7, lit); sky = mix(sky, mix(cShade, cLit, lit), a);

      // --- Vignette + dither ---
      vec2 q2 = vUv - 0.5;
      sky *= 1.0 - dot(q2, q2) * 0.18;
      sky += (hash(vUv * uRes + fract(uTime)) - 0.5) / 160.0;

      gl_FragColor = vec4(sky, 1.0);
    }
  `;

  let gl = null;
  let uniforms = { uTime: null, uDay: null, uRes: null };

  const GL_OPTS = { antialias: false, alpha: false, powerPreference: "high-performance" };

  function initGL() {
    gl = canvas.getContext("webgl", GL_OPTS) || canvas.getContext("experimental-webgl", GL_OPTS);
    if (!gl) return false;
    const compile = (type, src) => {
      const sh = gl.createShader(type);
      gl.shaderSource(sh, src);
      gl.compileShader(sh);
      if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { console.error("Shader error:", gl.getShaderInfoLog(sh)); return null; }
      return sh;
    };
    const vs = compile(gl.VERTEX_SHADER, VERT);
    const fs = compile(gl.FRAGMENT_SHADER, FRAG);
    if (!vs || !fs) return false;
    const prog = gl.createProgram();
    gl.attachShader(prog, vs); gl.attachShader(prog, fs);
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.error("Link error:", gl.getProgramInfoLog(prog)); return false; }
    gl.useProgram(prog);
    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
    const loc = gl.getAttribLocation(prog, "aPos");
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    uniforms.uTime = gl.getUniformLocation(prog, "uTime");
    uniforms.uDay = gl.getUniformLocation(prog, "uDay");
    uniforms.uRes = gl.getUniformLocation(prog, "uRes");
    return true;
  }

  function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 1.75);
    const w = Math.floor(window.innerWidth * dpr);
    const hgt = Math.floor(window.innerHeight * dpr);
    if (canvas.width !== w || canvas.height !== hgt) {
      canvas.width = w; canvas.height = hgt;
      if (gl) gl.viewport(0, 0, w, hgt);
    }
  }

  const hasWebGL = (() => { try { return initGL(); } catch (e) { console.error(e); return false; } })();

  function drawOne() {
    if (!gl) return;
    gl.uniform1f(uniforms.uTime, 12.0);
    gl.uniform1f(uniforms.uDay, state.day);
    gl.uniform2f(uniforms.uRes, canvas.width, canvas.height);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
  }

  if (hasWebGL) {
    // Size first (resizing clears the buffer), then draw one frame immediately
    // so the sky is visible before the first rAF.
    resize();
    drawOne();
  } else {
    // Context creation can transiently fail (e.g. GPU/webview not ready yet).
    // Retry a few times before committing to the static fallback.
    document.documentElement.classList.add("no-webgl");
    let attempts = 0;
    const retry = setInterval(() => {
      attempts++;
      let ok = false;
      try { ok = initGL(); } catch {}
      if (ok) {
        clearInterval(retry);
        document.documentElement.classList.remove("no-webgl");
        resize();
        drawOne();
      } else if (attempts >= 12) {
        clearInterval(retry);
        canvas.remove();
        console.warn("WebGL unavailable — using static sky fallback");
      }
    }, 400);
  }
  canvas.addEventListener("webglcontextlost", (e) => e.preventDefault());
  canvas.addEventListener("webglcontextrestored", () => { if (initGL()) { resize(); drawOne(); } });
  window.addEventListener("resize", resize);
  resize();

  // Debug hook: redraw synchronously so tests/probes can readPixels in the same task.
  window.__skyDraw = () => {
    if (!gl) return false;
    gl.uniform1f(uniforms.uTime, reducedMotion.matches ? 12.0 : performance.now() / 1000);
    gl.uniform1f(uniforms.uDay, state.day);
    gl.uniform2f(uniforms.uRes, canvas.width, canvas.height);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    return true;
  };

  let running = true;
  document.addEventListener("visibilitychange", () => { running = !document.hidden; });

  function frame(now) {
    requestAnimationFrame(frame);
    if (!running) return;
    if (state.animating) {
      const k = state.duration > 0 ? clamp01((now - state.t0) / state.duration) : 1;
      state.day = state.from + (state.to - state.from) * easeInOutCubic(k);
      if (k >= 1) state.animating = false;
    }
    if (gl) {
      gl.uniform1f(uniforms.uTime, reducedMotion.matches ? 12.0 : now / 1000);
      gl.uniform1f(uniforms.uDay, state.day);
      gl.uniform2f(uniforms.uRes, canvas.width, canvas.height);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    }
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>