<!-- ORBIT & RAYS VIEW — Orchid Continuum Weather/Habitat Widget -->
<style>
  .oc-orbit-wrap{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#111}
  .oc-toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0}
  .oc-toolbar button,.oc-toolbar label{font-size:14px}
  .oc-btn{border:1px solid #ccc;background:#fff;border-radius:6px;padding:6px 10px;cursor:pointer}
  .oc-btn[aria-pressed="true"]{background:#111;color:#fff;border-color:#111}
  .oc-slider{width:320px}
  .oc-grid{display:grid;grid-template-columns:1fr;gap:12px}
  @media (min-width:960px){.oc-grid{grid-template-columns: 2fr 1fr}}
  .oc-card{border:1px solid #e7e7e7;border-radius:10px;padding:12px}
  .oc-sr-only{position:absolute;left:-9999px}
  .oc-caption{font-size:13px;color:#333}
  .oc-insights h4{margin:.3rem 0}
  .oc-insights p{margin:.3rem 0;font-size:14px}
  .oc-legend{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:#333;margin-top:6px}
  .oc-legend span{display:inline-flex;align-items:center;gap:6px}
  .oc-swatch{width:16px;height:2px;background:#000;display:inline-block}
  .sw-sun{background:#ff9800}.sw-orbit{background:#6b9fff}.sw-rays{background:#777}.sw-lat{background:#2f7d32}
  /* high-contrast strokes */
  .hc-stroke{stroke:#111;stroke-width:1.25}
  .hc-thick{stroke-width:1.75}
</style>

<div id="oc-orbitRays" class="oc-orbit-wrap" role="region" aria-label="Orbit and solar rays visualization" hidden>
  <div class="oc-toolbar" role="toolbar" aria-label="Orbit view controls">
    <!-- Toggle (if you want to visually show Charts vs Orbit & Rays) -->
    <button class="oc-btn" id="oc-toggleCharts" aria-pressed="false" title="Switch to Charts">Charts</button>
    <button class="oc-btn" id="oc-toggleOrbit" aria-pressed="true" title="Orbit & Rays">Orbit & Rays</button>

    <!-- Date slider -->
    <label for="oc-day" style="margin-left:8px;">Date (DOY):</label>
    <input id="oc-day" class="oc-slider" type="range" min="0" max="365" step="1" value="172"
           aria-label="Day of year slider (0 to 365)" />
    <button class="oc-btn" id="oc-play" aria-pressed="false" aria-label="Play animation (space)" title="Play/Pause (space)">▶</button>

    <!-- Overlays -->
    <span style="margin-left:auto"></span>
    <label><input type="checkbox" id="oc-lat0" checked/> 0°</label>
    <label><input type="checkbox" id="oc-lat23" checked/> 23.4°</label>
    <label><input type="checkbox" id="oc-lat35" checked/> 35°</label>
    <label><input type="checkbox" id="oc-lat50" checked/> 50°</label>
  </div>

  <div class="oc-grid">
    <div class="oc-card">
      <!-- SVG canvas -->
      <svg id="oc-svg" viewBox="0 0 900 520" width="100%" height="auto" role="img"
           aria-label="Earth orbit with seasons, axial tilt, and solar rays">
        <desc>Sun at left focus; Earth moves along elliptical orbit with axial tilt shown.
          Parallel sun rays hit Earth; selectable latitude lines drawn on the globe.</desc>

        <!-- Sun -->
        <defs>
          <radialGradient id="sunG" cx="50%" cy="50%" r="50%">
            <stop offset="0%" stop-color="#fff7d1"/><stop offset="60%" stop-color="#ffd54d"/>
            <stop offset="100%" stop-color="#ff9800"/>
          </radialGradient>
          <clipPath id="earthClip"><circle id="earthClipCircle" cx="0" cy="0" r="66"/></clipPath>
        </defs>

        <!-- Orbit ellipse guide -->
        <g id="orbitLayer">
          <ellipse id="orbitPath" cx="240" cy="260" rx="300" ry="180"
                   fill="none" stroke="#6b9fff" stroke-width="1.5" class="hc-stroke"/>
          <!-- Sun at focus (left) -->
          <circle cx="240" cy="260" r="48" fill="url(#sunG)" stroke="#ff9800" class="hc-stroke hc-thick"/>
        </g>

        <!-- Rays layer (parallel to Sun->Earth vector) -->
        <g id="raysLayer"></g>

        <!-- Earth + axis + latitude lines -->
        <g id="earthLayer"></g>

        <!-- Season labels -->
        <g id="seasonLabels" font-size="12" fill="#111">
          <text x="560" y="70">≈ Jun 21 (N Summer Solstice)</text>
          <text x="560" y="460">≈ Dec 21 (N Winter Solstice)</text>
          <text x="835" y="260" text-anchor="end">≈ Mar / Sep Equinoxes</text>
        </g>
      </svg>

      <div class="oc-legend">
        <span><i class="oc-swatch sw-sun"></i> Sun</span>
        <span><i class="oc-swatch sw-orbit"></i> Orbit</span>
        <span><i class="oc-swatch sw-rays"></i> Sun Rays</span>
        <span><i class="oc-swatch sw-lat"></i> Latitude Lines (click to select)</span>
      </div>

      <div class="oc-caption" id="oc-caption" aria-live="polite" style="margin-top:8px;">
        <!-- Filled by JS -->
      </div>
    </div>

    <div class="oc-card oc-insights">
      <h4>Insights</h4>
      <p><strong>Hemisphere offset:</strong> Perihelion is ~early January (DOY ≈ 3), aphelion ~early July (≈ 185).
         Northern seasons are driven by <em>tilt</em>, not distance: N-summer happens at aphelion.</p>
      <p><strong>Photoperiod:</strong> Day length depends on latitude and solar declination.
         Short days + cool nights in autumn are classic floral triggers in 20–40° bands.</p>
      <p><strong>Why 35° is seasonally rich:</strong> Large swing in solar-noon altitude and day length
         gives strong “season signals” without polar extremes. Great for teaching spike induction logic.</p>
      <p class="oc-caption">Keyboard: <kbd>Space</kbd> play/pause, <kbd>←/→</kbd> adjust date, <kbd>0/2/3/5</kbd> jump to 0°, 23.4°, 35°, 50°.</p>
    </div>
  </div>
</div>

<script>
(function(){
  // ---- math helpers ----
  const DEG = Math.PI/180, RAD2DEG = 180/Math.PI;
  const OBLIQ = 23.44;                 // axial tilt in degrees
  const ECC = 0.0167;                  // orbital eccentricity
  const PERI_DOY = 3;                  // perihelion ~ Jan 3
  function solarDeclination(doy){ // Cooper's formula
    return 23.44 * Math.sin(2*Math.PI*(284 + doy)/365);
  }
  function dayLengthHours(latDeg, declDeg){
    const phi = latDeg*DEG, d = declDeg*DEG;
    const x = -Math.tan(phi)*Math.tan(d);
    const clamped = Math.max(-1, Math.min(1, x));
    const H0 = Math.acos(clamped); // radians
    return 24 * H0/Math.PI;
  }
  function solarNoonAltitude(latDeg, declDeg){
    const alt = 90 - Math.abs(latDeg - declDeg);
    return Math.max(0, Math.min(90, alt));
  }

  // ---- simple Kepler solver (for Earth position on ellipse) ----
  function earthState(doy){
    const a = 300, b = 180; // ellipse radii (SVG units)
    const cx = 240, cy = 260; // center of ellipse
    const M = 2*Math.PI * ((doy - PERI_DOY) / 365); // mean anomaly
    // Solve E - e sin E = M (Newton)
    let E = M, i=0;
    for(; i<8; i++){
      const f = E - ECC*Math.sin(E) - M;
      const fp = 1 - ECC*Math.cos(E);
      E = E - f/fp;
    }
    const cosE = Math.cos(E), sinE = Math.sin(E);
    const r = a*(1 - ECC*cosE);
    // True anomaly
    const nu = 2*Math.atan2(Math.sqrt(1+ECC)*sinE/Math.sqrt(1-ECC), (Math.sqrt(1+ECC)*cosE - Math.sqrt(1-ECC)));
    // Parametric ellipse angle relative to center
    // We align perihelion near the rightmost point of the ellipse visually (Sun at left focus).
    // Convert polar at focus -> approximate param angle along ellipse:
    const ex = a*Math.cos(nu); // ellipse x from center (approx)
    const ey = b*Math.sin(nu);
    // Place Sun at left focus (cx - c, cy), c = sqrt(a^2 - b^2)
    const c = Math.sqrt(a*a - b*b);
    const sun = {x: cx - c, y: cy};
    const earth = {x: cx + ex, y: cy + ey};
    // Vector Sun->Earth
    const vx = earth.x - sun.x, vy = earth.y - sun.y;
    const vlen = Math.hypot(vx, vy);
    const vhat = {x: vx/vlen, y: vy/vlen};
    return {sun, earth, dir:vhat};
  }

  // ---- drawing ----
  const root = document.getElementById('oc-orbitRays');
  const svg  = document.getElementById('oc-svg');
  const earthLayer = document.getElementById('earthLayer');
  const raysLayer  = document.getElementById('raysLayer');
  const caption = document.getElementById('oc-caption');
  const daySlider = document.getElementById('oc-day');
  const playBtn = document.getElementById('oc-play');

  // state
  let focusedLat = 35;     // default focused latitude
  let playing = false;
  let lastTs = 0;
  let fpsTarget = 60;
  let doy = +daySlider.value;
  // overlays
  const latChecks = {0: document.getElementById('oc-lat0'),
                     23.4: document.getElementById('oc-lat23'),
                     35: document.getElementById('oc-lat35'),
                     50: document.getElementById('oc-lat50')};

  // build earth group once; update transforms per frame
  const earthGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
  earthLayer.appendChild(earthGroup);

  // earth body
  const earthCircle = document.createElementNS('http://www.w3.org/2000/svg','circle');
  earthCircle.setAttribute('r','66');
  earthCircle.setAttribute('fill','#dff2ff');
  earthCircle.setAttribute('stroke','#111');
  earthCircle.setAttribute('stroke-width','1.2');
  earthCircle.setAttribute('class','hc-stroke');
  earthGroup.appendChild(earthCircle);

  // axis line (tilt 23.44° relative to Earth's orbital plane)
  const axis = document.createElementNS('http://www.w3.org/2000/svg','line');
  axis.setAttribute('x1',0); axis.setAttribute('y1',-75);
  axis.setAttribute('x2',0); axis.setAttribute('y2',75);
  axis.setAttribute('stroke','#111'); axis.setAttribute('stroke-width','2');
  axis.setAttribute('class','hc-stroke hc-thick');
  earthGroup.appendChild(axis);

  // latitude lines (paths inside earth clip)
  const latGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
  latGroup.setAttribute('clip-path','url(#earthClip)');
  earthGroup.appendChild(latGroup);

  const LAT_LIST = [0, 23.4, 35, 50];
  const latPaths = {};
  LAT_LIST.forEach(lat => {
    const p = document.createElementNS('http://www.w3.org/2000/svg','line');
    p.setAttribute('x1',-72); p.setAttribute('x2',72);
    p.setAttribute('y1',0);   p.setAttribute('y2',0);
    p.setAttribute('stroke', lat===focusedLat ? '#2f7d32' : '#2f7d32');
    p.setAttribute('stroke-width', lat===focusedLat ? '3' : '2');
    p.setAttribute('opacity', latChecks[lat].checked ? '1' : '0');
    p.style.cursor='pointer';
    p.addEventListener('click', ()=>{
      setFocusedLat(lat);
      if (lat === 35) {
        window.dispatchEvent(new CustomEvent('parallel35', {detail: {enabled:true, doy}}));
      }
    });
    latGroup.appendChild(p);
    latPaths[lat]=p;
  });

  // Earth clip circle follows Earth position
  const clipCircle = document.getElementById('earthClipCircle');

  // rays
  function drawRays(sun, earth, dir){
    raysLayer.innerHTML = '';
    const rayCount = 7;
    // Build rays across a band that envelopes the Earth
    for (let i=-3; i<=3; i++){
      const off = i*16;
      const sx = sun.x + off * (-dir.y); // offset rays perpendicular to direction
      const sy = sun.y + off * ( dir.x);
      const ex = earth.x - dir.x*90 + off * (-dir.y);
      const ey = earth.y - dir.y*90 + off * ( dir.x);
      const line = document.createElementNS('http://www.w3.org/2000/svg','line');
      line.setAttribute('x1', sx); line.setAttribute('y1', sy);
      line.setAttribute('x2', ex); line.setAttribute('y2', ey);
      line.setAttribute('stroke', '#777'); line.setAttribute('stroke-width','1.2');
      line.setAttribute('class','hc-stroke');
      raysLayer.appendChild(line);
    }
  }

  function setFocusedLat(lat){
    focusedLat = lat;
    Object.entries(latPaths).forEach(([L, path])=>{
      const isF = +L === focusedLat;
      path.setAttribute('stroke-width', isF ? '3' : '2');
      path.setAttribute('opacity', latChecks[+L].checked ? '1' : '0');
    });
    if (typeof window.onLatitudeFocusChanged === 'function'){
      window.onLatitudeFocusChanged(focusedLat);
    }
    updateCaption();
  }

  // update visibility from checkboxes
  Object.entries(latChecks).forEach(([L, box])=>{
    box.addEventListener('change', ()=>{
      const p = latPaths[+L];
      p.setAttribute('opacity', box.checked ? '1' : '0');
      // Keep focus line visible even if toggled off? We respect the checkbox.
      updateCaption();
    });
  });

  function updateScene(){
    const {sun, earth, dir} = earthState(doy);
    // position Earth group
    earthGroup.setAttribute('transform', `translate(${earth.x},${earth.y}) rotate(${OBLIQ})`);
    clipCircle.setAttribute('cx', earth.x); clipCircle.setAttribute('cy', earth.y);

    // y-positions for latitude lines: simple planar model
    LAT_LIST.forEach(lat=>{
      const y = -66 * Math.sin(lat*DEG); // negative is "up"
      const p = latPaths[lat];
      p.setAttribute('y1', y);
      p.setAttribute('y2', y);
    });

    // axis orientation: keep drawn at OBLIQ within Earth group (already rotated)

    // rays
    drawRays(sun, earth, dir);

    updateCaption();
  }

  function updateCaption(){
    const dec = solarDeclination(doy);
    const dl  = dayLengthHours(focusedLat, dec);
    const alt = solarNoonAltitude(focusedLat, dec);
    caption.textContent =
      `DOY ${doy} — Solar declination: ${dec.toFixed(2)}°, `
      + `Day length @ ${focusedLat}°: ${dl.toFixed(2)} h, `
      + `Solar-noon altitude: ${alt.toFixed(1)}°.`;
  }

  // slider + keyboard
  daySlider.addEventListener('input', ()=>{ doy = +daySlider.value; updateScene(); });
  function setPlay(p){
    playing = p;
    playBtn.setAttribute('aria-pressed', playing?'true':'false');
    playBtn.textContent = playing ? '⏸' : '▶';
  }
  playBtn.addEventListener('click', ()=> setPlay(!playing));
  // space toggles play; arrows nudge
  root.addEventListener('keydown', (e)=>{
    if (e.code === 'Space'){ e.preventDefault(); setPlay(!playing); }
    if (e.key === 'ArrowRight'){ doy = Math.min(365, doy+1); daySlider.value = doy; updateScene(); }
    if (e.key === 'ArrowLeft'){  doy = Math.max(0,   doy-1); daySlider.value = doy; updateScene(); }
    if (e.key === '0'){ setFocusedLat(0); }
    if (e.key === '2'){ setFocusedLat(23.4); }
    if (e.key === '3'){ setFocusedLat(35); window.dispatchEvent(new CustomEvent('parallel35',{detail:{enabled:true, doy}})); }
    if (e.key === '5'){ setFocusedLat(50); }
  });

  // animation loop with graceful throttle (60 → 30 fps if slow)
  let frameAccum = 0, slowFrames = 0;
  function tick(ts){
    if (!playing) return;
    const dt = Math.min(64, ts - (lastTs || ts)); lastTs = ts;
    frameAccum += dt;

    const targetMs = (slowFrames >= 20) ? (1000/30) : (1000/fpsTarget); // degrade if 20 consecutive slow frames
    if (frameAccum >= targetMs){
      // advance ~0.5 day per 16ms @60fps → ~30 days in 1s; tune speed here
      const speed = 0.5 * (frameAccum / (1000/60));
      doy = (doy + speed) % 366; daySlider.value = Math.round(doy);
      updateScene();
      frameAccum = 0;
    }
    // detect slowness
    if (dt > (1000/fpsTarget)+6) slowFrames++; else slowFrames = Math.max(0, slowFrames-1);
    requestAnimationFrame(tick);
  }

  // public show/hide
  window.showOrbitRays = function(){
    document.getElementById('oc-orbitRays').hidden = false;
    document.getElementById('oc-orbitRays').tabIndex = 0; // allow key focus
    document.getElementById('oc-orbitRays').focus();
    updateScene();
  };
  // optional hook for your Charts toggle
  document.getElementById('oc-toggleCharts').addEventListener('click', ()=>{
    // hide this view; your app can show Charts view
    document.getElementById('oc-orbitRays').hidden = true;
  });
  document.getElementById('oc-toggleOrbit').addEventListener('click', ()=>{
    showOrbitRays();
  });

  // Respect prefers-reduced-motion: start paused
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  setPlay(!reduced);

  // Start animation if playing
  if (playing) requestAnimationFrame(tick);

  // Initial render
  updateScene();
})();
</script>