const { useState, useEffect, useMemo, useRef, useCallback } = React;
const uS = useState, uE = useEffect, uM = useMemo, uR = useRef;
const hUS = useState, hUE = useEffect, hUM = useMemo;
const pUS = useState, pUE = useEffect, pUM = useMemo, pUR = useRef;
const mrUS = useState, mrUE = useEffect, mrUM = useMemo;
const tjUS = useState, tjUE = useEffect, tjUM = useMemo, tjUR = useRef;
const rrFUS = useState, rrFUE = useEffect, rrFUC = useCallback;


// === deploy/creatures.jsx ===
// Creature SVGs, simple, abstract, never anatomically-specific
// All reusable across hero scene and species cards
const Creature = {
  clownfish: ({ size = 60, color = "var(--coral)" }) => (
    <svg width={size} height={size * 0.7} viewBox="0 0 100 70" fill="none">
      <ellipse cx="45" cy="35" rx="30" ry="18" fill={color} />
      <path d="M75 35 L95 20 L95 50 Z" fill={color} />
      <rect x="30" y="17" width="6" height="36" fill="white" opacity="0.9" rx="1"/>
      <rect x="52" y="17" width="6" height="36" fill="white" opacity="0.9" rx="1"/>
      <rect x="28" y="16" width="10" height="38" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" rx="1"/>
      <rect x="50" y="16" width="10" height="38" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" rx="1"/>
      <path d="M75 35 L95 20 L95 50 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" strokeLinejoin="round"/>
      <ellipse cx="45" cy="35" rx="30" ry="18" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5"/>
      <circle cx="25" cy="32" r="3" fill="oklch(0.15 0.03 240)" />
      <circle cx="24" cy="31" r="1" fill="white" />
    </svg>
  ),
  tang: ({ size = 80, color = "var(--amber)" }) => (
    <svg width={size} height={size * 0.9} viewBox="0 0 100 90" fill="none">
      <path d="M20 45 Q50 10 80 45 Q50 80 20 45 Z" fill={color} />
      <path d="M80 45 L95 25 L92 45 L95 65 Z" fill={color} />
      <path d="M20 45 Q50 10 80 45 Q50 80 20 45 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5"/>
      <path d="M80 45 L95 25 L92 45 L95 65 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" strokeLinejoin="round"/>
      <circle cx="30" cy="42" r="3" fill="oklch(0.15 0.03 240)"/>
      <circle cx="29" cy="41" r="1" fill="white"/>
    </svg>
  ),
  cardinal: ({ size = 60, color = "var(--ink)" }) => (
    <svg width={size} height={size * 1.1} viewBox="0 0 80 90" fill="none">
      <ellipse cx="40" cy="45" rx="22" ry="28" fill="oklch(0.4 0.03 230)" />
      <path d="M40 15 L30 0 L50 0 Z" fill="oklch(0.4 0.03 230)" />
      <path d="M40 75 L25 90 L55 90 Z" fill="oklch(0.4 0.03 230)" />
      <rect x="25" y="25" width="3" height="40" fill="oklch(0.15 0.03 240)" opacity="0.7"/>
      <rect x="45" y="25" width="3" height="40" fill="oklch(0.15 0.03 240)" opacity="0.7"/>
      <rect x="60" y="25" width="3" height="40" fill="oklch(0.15 0.03 240)" opacity="0.7"/>
      <ellipse cx="40" cy="45" rx="22" ry="28" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5"/>
      <circle cx="32" cy="38" r="3" fill="white"/>
      <circle cx="32" cy="38" r="1.5" fill="oklch(0.15 0.03 240)"/>
    </svg>
  ),
  gramma: ({ size = 60, color1 = "var(--violet)", color2 = "var(--amber)" }) => (
    <svg width={size} height={size * 0.6} viewBox="0 0 100 60" fill="none">
      <defs>
        <linearGradient id="gramma-grad" x1="0" x2="1">
          <stop offset="40%" stopColor="var(--violet)"/>
          <stop offset="60%" stopColor="var(--amber)"/>
        </linearGradient>
      </defs>
      <ellipse cx="48" cy="30" rx="32" ry="14" fill="url(#gramma-grad)" />
      <path d="M80 30 L95 18 L95 42 Z" fill="var(--amber)" />
      <ellipse cx="48" cy="30" rx="32" ry="14" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5"/>
      <path d="M80 30 L95 18 L95 42 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" strokeLinejoin="round"/>
      <circle cx="28" cy="27" r="2.5" fill="white"/>
      <circle cx="28" cy="27" r="1.2" fill="oklch(0.15 0.03 240)"/>
    </svg>
  ),
  firefish: ({ size = 70, color = "oklch(0.95 0.02 60)" }) => (
    <svg width={size} height={size * 0.5} viewBox="0 0 120 60" fill="none">
      <defs>
        <linearGradient id="ff-grad" x1="0" x2="1">
          <stop offset="60%" stopColor={color}/>
          <stop offset="100%" stopColor="var(--coral)"/>
        </linearGradient>
      </defs>
      <path d="M15 30 Q60 15 95 30 Q60 45 15 30 Z" fill="url(#ff-grad)" />
      <path d="M95 30 L115 20 L110 30 L115 42 Z" fill="var(--coral)" />
      <path d="M45 20 L52 3 L55 20 Z" fill="var(--coral)" />
      <path d="M15 30 Q60 15 95 30 Q60 45 15 30 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5"/>
      <path d="M95 30 L115 20 L110 30 L115 42 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" strokeLinejoin="round"/>
      <path d="M45 20 L52 3 L55 20 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" strokeLinejoin="round"/>
      <circle cx="25" cy="29" r="2" fill="oklch(0.15 0.03 240)"/>
    </svg>
  ),
  mandarin: ({ size = 60 }) => (
    <svg width={size} height={size * 0.65} viewBox="0 0 100 65" fill="none">
      <defs>
        <pattern id="m-swirl" width="8" height="8" patternUnits="userSpaceOnUse">
          <circle cx="4" cy="4" r="2" fill="var(--cyan)" opacity="0.6"/>
        </pattern>
      </defs>
      <ellipse cx="42" cy="32" rx="28" ry="16" fill="var(--lime)" />
      <ellipse cx="42" cy="32" rx="28" ry="16" fill="url(#m-swirl)" />
      <path d="M70 32 L90 20 L90 44 Z" fill="var(--cyan)" />
      <path d="M22 20 L40 14 L35 25 Z" fill="var(--cyan)" opacity="0.8"/>
      <ellipse cx="42" cy="32" rx="28" ry="16" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5"/>
      <path d="M70 32 L90 20 L90 44 Z" fill="none" stroke="oklch(0.15 0.03 240)" strokeWidth="1.5" strokeLinejoin="round"/>
      <circle cx="22" cy="30" r="2.5" fill="oklch(0.15 0.03 240)"/>
      <circle cx="21" cy="29" r="1" fill="white"/>
    </svg>
  ),
  jellyfish: ({ size = 80 }) => (
    <svg width={size} height={size * 1.3} viewBox="0 0 80 110" fill="none">
      <path d="M10 40 Q40 5 70 40 L70 48 Q40 58 10 48 Z" fill="var(--violet)" opacity="0.75"/>
      <path d="M18 48 Q20 80 16 105" stroke="var(--violet)" strokeWidth="1.5" fill="none" opacity="0.6"/>
      <path d="M30 50 Q34 85 28 108" stroke="var(--violet)" strokeWidth="1.5" fill="none" opacity="0.6"/>
      <path d="M40 52 Q40 85 40 108" stroke="var(--violet)" strokeWidth="1.5" fill="none" opacity="0.6"/>
      <path d="M50 50 Q46 85 52 108" stroke="var(--violet)" strokeWidth="1.5" fill="none" opacity="0.6"/>
      <path d="M62 48 Q60 80 64 105" stroke="var(--violet)" strokeWidth="1.5" fill="none" opacity="0.6"/>
      <path d="M10 40 Q40 5 70 40" fill="none" stroke="var(--cyan)" strokeWidth="1.2" opacity="0.8"/>
    </svg>
  ),
  coral_frond: ({ size = 120, color = "var(--coral)" }) => (
    <svg width={size} height={size * 1.5} viewBox="0 0 80 120" fill="none">
      <path d="M40 120 Q35 90 30 70 Q25 55 28 40 Q32 30 38 20" stroke={color} strokeWidth="6" strokeLinecap="round" fill="none"/>
      <path d="M30 70 Q20 62 15 52" stroke={color} strokeWidth="5" strokeLinecap="round" fill="none"/>
      <path d="M28 50 Q38 38 48 30" stroke={color} strokeWidth="5" strokeLinecap="round" fill="none"/>
      <path d="M34 80 Q50 72 58 60" stroke={color} strokeWidth="5" strokeLinecap="round" fill="none"/>
      <circle cx="38" cy="20" r="4" fill={color}/>
      <circle cx="48" cy="30" r="3.5" fill={color}/>
      <circle cx="15" cy="52" r="3.5" fill={color}/>
      <circle cx="58" cy="60" r="4" fill={color}/>
    </svg>
  ),
  anemone: ({ size = 100, color = "var(--lime)" }) => (
    <svg width={size} height={size} viewBox="0 0 100 100" fill="none">
      <ellipse cx="50" cy="85" rx="25" ry="10" fill="oklch(0.3 0.03 230)"/>
      {[...Array(14)].map((_, i) => {
        const angle = (i / 14) * Math.PI + Math.PI;
        const x2 = 50 + Math.cos(angle) * 38;
        const y2 = 70 + Math.sin(angle) * 34;
        return (
          <line key={i} x1="50" y1="70" x2={x2} y2={y2}
            stroke={color} strokeWidth="4" strokeLinecap="round" opacity="0.85"/>
        );
      })}
      <ellipse cx="50" cy="70" rx="14" ry="8" fill={color} opacity="0.9"/>
    </svg>
  ),
  acro: ({ size = 100, color = "oklch(0.75 0.15 270)" }) => (
    <svg width={size} height={size} viewBox="0 0 100 100" fill="none">
      <path d="M50 100 L50 50" stroke={color} strokeWidth="10" strokeLinecap="round"/>
      <path d="M50 70 L30 45" stroke={color} strokeWidth="8" strokeLinecap="round"/>
      <path d="M50 65 L70 40" stroke={color} strokeWidth="8" strokeLinecap="round"/>
      <path d="M50 55 L40 25" stroke={color} strokeWidth="7" strokeLinecap="round"/>
      <path d="M50 55 L60 25" stroke={color} strokeWidth="7" strokeLinecap="round"/>
      <path d="M30 45 L18 30" stroke={color} strokeWidth="6" strokeLinecap="round"/>
      <path d="M70 40 L82 25" stroke={color} strokeWidth="6" strokeLinecap="round"/>
      <circle cx="50" cy="50" r="4" fill={color}/>
      <circle cx="40" cy="25" r="3" fill={color}/>
      <circle cx="60" cy="25" r="3" fill={color}/>
      <circle cx="18" cy="30" r="3" fill={color}/>
      <circle cx="82" cy="25" r="3" fill={color}/>
    </svg>
  ),
  mushroom: ({ size = 90, color = "oklch(0.75 0.17 20)" }) => (
    <svg width={size} height={size * 0.6} viewBox="0 0 100 60" fill="none">
      <ellipse cx="50" cy="45" rx="42" ry="14" fill={color}/>
      <ellipse cx="50" cy="40" rx="38" ry="12" fill={color}/>
      {[...Array(20)].map((_, i) => {
        const x = 15 + (i / 20) * 70;
        return <circle key={i} cx={x} cy={35 + Math.sin(i) * 4} r="2" fill="oklch(0.85 0.17 20)"/>;
      })}
    </svg>
  ),
  zoa: ({ size = 100 }) => (
    <svg width={size} height={size * 0.7} viewBox="0 0 100 70" fill="none">
      {[[20,40,"var(--lime)"],[48,30,"var(--cyan)"],[75,42,"oklch(0.7 0.17 30)"],[32,55,"var(--amber)"],[62,55,"var(--violet)"]].map(([cx,cy,col],i) => (
        <g key={i}>
          <circle cx={cx} cy={cy} r="12" fill={col}/>
          <circle cx={cx} cy={cy} r="7" fill="oklch(0.2 0.03 230)"/>
          <circle cx={cx} cy={cy} r="3" fill={col}/>
          {[...Array(12)].map((_,j) => {
            const a = (j/12) * Math.PI * 2;
            return <line key={j} x1={cx+Math.cos(a)*7} y1={cy+Math.sin(a)*7} x2={cx+Math.cos(a)*11} y2={cy+Math.sin(a)*11} stroke={col} strokeWidth="1.5"/>;
          })}
        </g>
      ))}
    </svg>
  ),
  snail: ({ size = 50, color = "oklch(0.5 0.05 60)" }) => (
    <svg width={size} height={size * 0.9} viewBox="0 0 60 54" fill="none">
      <ellipse cx="25" cy="40" rx="25" ry="10" fill="oklch(0.6 0.04 40)"/>
      <path d="M25 35 m-14 0 a14 14 0 1 1 28 0 a14 14 0 1 1 -28 0 m4 0 a10 10 0 1 1 20 0 a10 10 0 1 1 -20 0 m4 0 a6 6 0 1 1 12 0 a6 6 0 1 1 -12 0"
        fill={color} fillRule="evenodd"/>
      <line x1="5" y1="40" x2="2" y2="32" stroke="oklch(0.6 0.04 40)" strokeWidth="1.5" strokeLinecap="round"/>
      <line x1="9" y1="40" x2="8" y2="30" stroke="oklch(0.6 0.04 40)" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  ),
  shrimp: ({ size = 90, color = "oklch(0.82 0.08 25)" }) => (
    <svg width={size} height={size * 0.5} viewBox="0 0 120 60" fill="none">
      <path d="M10 35 Q30 15 60 20 Q90 25 110 30 L105 35 Q85 40 65 37 Q40 38 15 40 Z" fill={color}/>
      <rect x="40" y="22" width="32" height="10" fill="white" opacity="0.8" rx="4"/>
      <line x1="110" y1="30" x2="118" y2="10" stroke={color} strokeWidth="2" strokeLinecap="round"/>
      <line x1="110" y1="30" x2="118" y2="22" stroke={color} strokeWidth="2" strokeLinecap="round"/>
      <line x1="15" y1="40" x2="2" y2="55" stroke={color} strokeWidth="2.5" strokeLinecap="round"/>
      <line x1="15" y1="40" x2="10" y2="58" stroke={color} strokeWidth="2.5" strokeLinecap="round"/>
      <circle cx="14" cy="32" r="2" fill="oklch(0.15 0.03 240)"/>
    </svg>
  ),
  rockbed: ({ w = 800, h = 200 }) => (
    <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} fill="none" preserveAspectRatio="none">
      <defs>
        <linearGradient id="rock-grad" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor="oklch(0.25 0.04 240)"/>
          <stop offset="100%" stopColor="oklch(0.12 0.03 250)"/>
        </linearGradient>
      </defs>
      <path d={`M0 ${h} L0 ${h*0.6} Q${w*0.1} ${h*0.4} ${w*0.22} ${h*0.55} Q${w*0.35} ${h*0.35} ${w*0.5} ${h*0.5} Q${w*0.62} ${h*0.3} ${w*0.78} ${h*0.48} Q${w*0.9} ${h*0.38} ${w} ${h*0.55} L${w} ${h} Z`}
        fill="url(#rock-grad)"/>
    </svg>
  )
};

window.Creature = Creature;


// === deploy/creature-resolver.jsx ===
// Maps every species in the library to the best-fit Creature SVG + color
// Falls back gracefully by category/kind when id isn't explicitly mapped.

function resolveCreature(sp, size) {
  const C = window.Creature;
  if (!C) return null;

  // Explicit per-id map (most specific)
  const idMap = {
    // Clowns & related
    ocellaris: () => <C.clownfish size={size} color="var(--coral)"/>,
    blackice: () => <C.clownfish size={size} color="oklch(0.35 0.02 240)"/>,
    maroon: () => <C.clownfish size={size} color="oklch(0.45 0.15 20)"/>,
    percula: () => <C.clownfish size={size} color="var(--amber)"/>,

    // Tangs
    yellowtang: () => <C.tang size={size} color="var(--amber)"/>,
    blueregal: () => <C.tang size={size} color="oklch(0.55 0.18 250)"/>,
    kolei: () => <C.tang size={size} color="oklch(0.38 0.04 40)"/>,
    powderblue: () => <C.tang size={size} color="oklch(0.72 0.12 230)"/>,
    purpletang: () => <C.tang size={size} color="oklch(0.55 0.15 300)"/>,
    foxface: () => <C.tang size={size} color="oklch(0.85 0.14 85)"/>,

    // Angels & butterflies
    flameangel: () => <C.tang size={size} color="oklch(0.65 0.22 30)"/>,
    coralbeauty: () => <C.tang size={size} color="oklch(0.55 0.15 340)"/>,
    copperband: () => <C.tang size={size} color="oklch(0.85 0.14 75)"/>,

    // Wrasses
    sixline: () => <C.gramma size={size} color1="var(--coral)" color2="var(--cyan)"/>,
    melanurus: () => <C.gramma size={size} color1="var(--lime)" color2="var(--violet)"/>,
    mccosker: () => <C.gramma size={size} color1="var(--coral)" color2="var(--amber)"/>,
    fairywrasse: () => <C.gramma size={size} color1="oklch(0.6 0.18 340)" color2="var(--amber)"/>,
    leopardwrasse: () => <C.gramma size={size} color1="var(--amber)" color2="oklch(0.35 0.04 40)"/>,

    // Cardinals
    banggai: () => <C.cardinal size={size}/>,
    pyjamacardinal: () => <C.cardinal size={size} color="var(--amber)"/>,

    // Grammas & dottybacks
    royalgramma: () => <C.gramma size={size}/>,
    purpledotty: () => <C.gramma size={size} color1="var(--violet)" color2="var(--violet)"/>,
    royaldotty: () => <C.gramma size={size} color1="oklch(0.55 0.2 330)" color2="var(--amber)"/>,
    orchiddotty: () => <C.gramma size={size} color1="var(--violet)" color2="oklch(0.55 0.2 310)"/>,

    // Gobies
    firefish: () => <C.firefish size={size}/>,
    watchman: () => <C.firefish size={size} color="oklch(0.85 0.12 80)"/>,
    diamondgoby: () => <C.firefish size={size} color="oklch(0.9 0.04 80)"/>,
    neongoby: () => <C.firefish size={size} color="var(--cyan)"/>,

    // Dragonets
    mandarin: () => <C.mandarin size={size}/>,
    scooter: () => <C.mandarin size={size}/>,

    // Damsels / chromis
    chromis: () => <C.gramma size={size} color1="var(--cyan)" color2="var(--cyan)"/>,
    starrydamsel: () => <C.gramma size={size} color1="var(--cyan)" color2="var(--amber)"/>,

    // Anthias / basslets
    anthias: () => <C.firefish size={size} color="oklch(0.7 0.18 20)"/>,
    basslet: () => <C.firefish size={size} color="var(--violet)"/>,

    // Blennies
    lawnmower: () => <C.firefish size={size} color="oklch(0.5 0.05 80)"/>,
    midasblenny: () => <C.firefish size={size} color="var(--amber)"/>,
    tailspot: () => <C.firefish size={size} color="oklch(0.6 0.06 60)"/>,

    // Hawkfish
    longnosehawk: () => <C.firefish size={size} color="oklch(0.9 0.1 80)"/>,

    // Corals - Euphyllia
    hammer: () => <C.anemone size={size} color="oklch(0.75 0.14 100)"/>,
    torch: () => <C.anemone size={size} color="var(--amber)"/>,
    frogspawn: () => <C.anemone size={size} color="oklch(0.75 0.15 140)"/>,

    // Corals - LPS
    duncan: () => <C.anemone size={size} color="var(--lime)"/>,
    candycane: () => <C.anemone size={size} color="oklch(0.75 0.14 25)"/>,
    acanlord: () => <C.zoa size={size}/>,
    blastomussa: () => <C.anemone size={size} color="oklch(0.55 0.2 20)"/>,
    brain: () => <C.mushroom size={size} color="oklch(0.55 0.18 20)"/>,
    goniopora: () => <C.anemone size={size} color="oklch(0.7 0.14 60)"/>,
    chalice: () => <C.mushroom size={size} color="oklch(0.65 0.2 330)"/>,

    // Corals - SPS
    acropora: () => <C.acro size={size} color="oklch(0.70 0.17 275)"/>,
    stylophora: () => <C.acro size={size} color="oklch(0.75 0.14 340)"/>,
    birdsnest: () => <C.acro size={size} color="oklch(0.7 0.14 20)"/>,
    montipora: () => <C.acro size={size} color="oklch(0.6 0.18 30)"/>,
    "plating-monti": () => <C.mushroom size={size} color="oklch(0.45 0.18 260)"/>,

    // Corals - Soft
    zoa: () => <C.zoa size={size}/>,
    greenstar: () => <C.zoa size={size}/>,
    gsp: () => <C.zoa size={size}/>,
    xenia: () => <C.anemone size={size} color="oklch(0.85 0.04 180)"/>,
    leather: () => <C.mushroom size={size} color="oklch(0.55 0.05 80)"/>,
    mushroom: () => <C.mushroom size={size}/>,
    ricordea: () => <C.mushroom size={size} color="oklch(0.75 0.18 150)"/>,

    // Inverts
    cleaner: () => <C.shrimp size={size}/>,
    fireshrimp: () => <C.shrimp size={size} color="oklch(0.55 0.22 20)"/>,
    peppermint: () => <C.shrimp size={size} color="oklch(0.85 0.1 20)"/>,
    pistol: () => <C.shrimp size={size} color="oklch(0.75 0.14 340)"/>,
    turbosnail: () => <C.snail size={size}/>,
    trochus: () => <C.snail size={size} color="oklch(0.55 0.08 60)"/>,
    cerith: () => <C.snail size={size} color="oklch(0.4 0.04 40)"/>,
    nassarius: () => <C.snail size={size} color="oklch(0.7 0.05 70)"/>,
    tuxedo: () => <C.anemone size={size} color="oklch(0.25 0.03 230)"/>,
    hermit: () => <C.snail size={size} color="oklch(0.55 0.15 30)"/>,
    emerald: () => <C.shrimp size={size} color="var(--lime)"/>,
    seahare: () => <C.mushroom size={size} color="oklch(0.4 0.1 90)"/>,
    serpentstar: () => <C.acro size={size} color="oklch(0.6 0.08 60)"/>,
    maximaclam: () => <C.mushroom size={size} color="var(--cyan)"/>,
  };

  if (idMap[sp.id]) return idMap[sp.id]();

  // Category-based fallback
  const cat = (sp.category || "").toLowerCase();
  if (cat.includes("clown")) return <C.clownfish size={size}/>;
  if (cat.includes("tang") || cat.includes("rabbit")) return <C.tang size={size}/>;
  if (cat.includes("angel") || cat.includes("butterfly")) return <C.tang size={size} color="var(--amber)"/>;
  if (cat.includes("wrasse")) return <C.gramma size={size} color1="var(--lime)" color2="var(--cyan)"/>;
  if (cat.includes("cardinal")) return <C.cardinal size={size}/>;
  if (cat.includes("dottyback") || cat.includes("gramma")) return <C.gramma size={size}/>;
  if (cat.includes("goby")) return <C.firefish size={size} color="var(--amber)"/>;
  if (cat.includes("blenny")) return <C.firefish size={size} color="oklch(0.55 0.08 60)"/>;
  if (cat.includes("damsel") || cat.includes("chromis")) return <C.gramma size={size} color1="var(--cyan)" color2="var(--cyan)"/>;
  if (cat.includes("anthias") || cat.includes("basslet")) return <C.firefish size={size} color="var(--coral)"/>;
  if (cat.includes("dragonet")) return <C.mandarin size={size}/>;
  if (cat.includes("hawk")) return <C.firefish size={size} color="oklch(0.9 0.1 80)"/>;

  // Corals
  if (cat.includes("sps")) return <C.acro size={size}/>;
  if (cat.includes("lps")) return <C.anemone size={size} color="oklch(0.75 0.14 100)"/>;
  if (cat.includes("mushroom")) return <C.mushroom size={size}/>;
  if (cat.includes("zoa") || cat.includes("soft")) return <C.zoa size={size}/>;
  if (cat.includes("anemone")) return <C.anemone size={size}/>;

  // Inverts
  if (cat.includes("shrimp")) return <C.shrimp size={size}/>;
  if (cat.includes("snail")) return <C.snail size={size}/>;
  if (cat.includes("crab")) return <C.snail size={size} color="oklch(0.55 0.15 30)"/>;
  if (cat.includes("urchin") || cat.includes("star")) return <C.anemone size={size} color="oklch(0.35 0.1 260)"/>;
  if (cat.includes("clam")) return <C.mushroom size={size} color="var(--cyan)"/>;

  // Kind fallback
  if (sp.kind === "fish") return <C.clownfish size={size} color="oklch(0.55 0.1 40)"/>;
  if (sp.kind === "coral") return <C.zoa size={size}/>;
  if (sp.kind === "invert") return <C.snail size={size}/>;

  return <C.clownfish size={size}/>;
}

window.resolveCreature = resolveCreature;


// === deploy/favorites.jsx ===
// ReefReads; favorites system
//
// Stars species and articles into "My Reef" — a local-only list of things
// the user wants to come back to. No accounts; localStorage only.
// Same architecture as the Tank Journal — see JOURNAL_SYNC.md if/when
// you want to add cross-device sync later.

const RR_FAV_KEY = "rr-favorites-v1";

function rrFavLoad() {
  try { return JSON.parse(localStorage.getItem(RR_FAV_KEY) || '{"species":[],"articles":[]}'); }
  catch { return { species: [], articles: [] }; }
}
function rrFavSave(state) {
  localStorage.setItem(RR_FAV_KEY, JSON.stringify(state));
  window.dispatchEvent(new Event("rr-favorites-changed"));
}

// React hook + helpers

function useFavorites() {
  const [state, setState] = rrFUS(() => rrFavLoad());
  rrFUE(() => {
    const onChange = () => setState(rrFavLoad());
    window.addEventListener("rr-favorites-changed", onChange);
    window.addEventListener("storage", e => { if (e.key === RR_FAV_KEY) setState(rrFavLoad()); });
    return () => window.removeEventListener("rr-favorites-changed", onChange);
  }, []);

  const toggle = rrFUC((kind, id) => {
    const next = rrFavLoad();
    const list = next[kind] || (next[kind] = []);
    const i = list.indexOf(id);
    if (i >= 0) list.splice(i, 1);
    else list.unshift(id); // newest first
    rrFavSave(next);
  }, []);

  const has = rrFUC((kind, id) => (state[kind] || []).includes(id), [state]);

  return { state, toggle, has };
}

// Star button — used inline on cards and as a large variant on detail pages.
function StarButton({ kind, id, size = "sm", label }) {
  const fav = useFavorites();
  const on = fav.has(kind, id);
  const handle = (e) => {
    e.preventDefault();
    e.stopPropagation();
    fav.toggle(kind, id);
  };
  return (
    <button
      type="button"
      className={`rr-star rr-star-${size} ${on ? "on" : ""}`}
      onClick={handle}
      aria-pressed={on}
      aria-label={on ? `Remove from My Reef` : `Save to My Reef`}
      title={on ? "Saved to My Reef · click to remove" : "Save to My Reef"}
    >
      <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
        <path
          d="M12 2.5l2.95 6.32 6.55.85-4.85 4.55 1.3 6.78L12 17.6l-5.95 3.4 1.3-6.78L2.5 9.67l6.55-.85L12 2.5z"
          fill={on ? "currentColor" : "none"}
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinejoin="round"
        />
      </svg>
      {label && <span className="rr-star-label">{on ? "Saved" : label}</span>}
    </button>
  );
}

Object.assign(window, { useFavorites, StarButton, rrFavLoad, rrFavSave });


// === deploy/pages-home.jsx ===
// ReefReads; page components

// ---------- Hero video sources ----------
// Defaults live in data-hero.js (window.HERO_VIDEOS). Edit that file to
// change the clips, titles, and credits.
// We keep a fallback array here so the page still works if data-hero.js
// is missing for any reason.
const HERO_VIDEOS_FALLBACK = [
  { src: "", poster: "kelp",    title: "Acropora garden at peak",        credit: "Reef tour · @SaltyAquaria",     creditUrl: "#" },
  { src: "", poster: "anemone", title: "Bubble-tip with hosting ocellaris", credit: "Footage · BlueWorld Reef",   creditUrl: "#" },
  { src: "", poster: "school",  title: "Schooling chromis at dusk",     credit: "Tank · @KitanoReef",            creditUrl: "#" },
  { src: "", poster: "jellies", title: "Moon jellies pulse cycle",      credit: "Footage · Monterey Bay Aquarium", creditUrl: "#" },
];

// Source-type detection. Returns one of:
//   {type:"file",     src}                  — direct .mp4/.webm/etc
//   {type:"youtube",  id, embed}            — YouTube watch / shorts / youtu.be
//   {type:"vimeo",    id, embed}            — Vimeo
//   {type:"instagram",url}                  — link out (no inline embed)
//   {type:"poster",   key}                  — animated CSS placeholder
function classifyHeroSource(v) {
  const s = (v.src || "").trim();
  if (!s) return { type: "poster", key: v.poster || "kelp" };
  // YouTube
  let m = s.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/)|youtu\.be\/)([A-Za-z0-9_-]{6,})/);
  if (m) return { type: "youtube", id: m[1], embed: `https://www.youtube-nocookie.com/embed/${m[1]}?autoplay=1&mute=1&loop=1&playlist=${m[1]}&controls=0&modestbranding=1&playsinline=1&rel=0` };
  // Vimeo
  m = s.match(/vimeo\.com\/(?:video\/)?(\d{6,})/);
  if (m) return { type: "vimeo", id: m[1], embed: `https://player.vimeo.com/video/${m[1]}?autoplay=1&muted=1&loop=1&background=1&controls=0` };
  // Instagram (no inline autoplay possible — link out)
  if (/instagram\.com\/(reel|p|tv)\//.test(s)) return { type: "instagram", url: s };
  // Otherwise assume it's a direct video file URL
  return { type: "file", src: s };
}

// Placeholder video frame — pretty looping CSS scene per "poster" key.
// Replace the parent <video> when a real `src` is provided.
function VideoPoster({ kind }) {
  if (kind === "kelp") {
    return (
      <div className="vid-poster" style={{ background: 'radial-gradient(ellipse at 50% 30%, oklch(0.42 0.13 195), oklch(0.18 0.04 240))' }}>
        <div className="caustics" style={{ opacity: 0.7 }}/>
        {Array.from({ length: 7 }).map((_, i) => (
          <div key={i} className="vp-stalk" style={{
            left: `${10 + i * 13}%`,
            height: `${50 + (i % 3) * 18}%`,
            animationDelay: `-${i * 0.7}s`,
            background: `linear-gradient(180deg, transparent, oklch(${0.4 + (i % 2) * 0.08} 0.14 ${140 + i * 8}) 60%, oklch(0.25 0.12 ${140 + i * 8}))`
          }}/>
        ))}
        <div className="vp-glow"/>
      </div>
    );
  }
  if (kind === "anemone") {
    return (
      <div className="vid-poster" style={{ background: 'radial-gradient(ellipse at 50% 50%, oklch(0.32 0.1 280), oklch(0.14 0.04 250))' }}>
        <div className="caustics" style={{ opacity: 0.5 }}/>
        <div className="vp-anemone"/>
        <div className="vp-fish" style={{ animationDelay: '-2s' }}/>
        <div className="vp-fish small" style={{ animationDelay: '-6s', top: '58%' }}/>
        <div className="vp-glow" style={{ background: 'radial-gradient(circle at 50% 60%, oklch(0.7 0.18 30 / 0.4), transparent 60%)' }}/>
      </div>
    );
  }
  if (kind === "school") {
    return (
      <div className="vid-poster" style={{ background: 'linear-gradient(180deg, oklch(0.36 0.1 220), oklch(0.16 0.05 240))' }}>
        <div className="caustics" style={{ opacity: 0.4 }}/>
        {Array.from({ length: 18 }).map((_, i) => (
          <div key={i} className="vp-school-fish" style={{
            top: `${20 + (i % 5) * 12}%`,
            left: `${(i * 7) % 100}%`,
            animationDelay: `-${i * 0.4}s`,
            animationDuration: `${10 + (i % 4) * 2}s`,
          }}/>
        ))}
      </div>
    );
  }
  // jellies
  return (
    <div className="vid-poster" style={{ background: 'radial-gradient(ellipse at 50% 60%, oklch(0.32 0.13 310), oklch(0.12 0.04 250))' }}>
      <div className="caustics" style={{ opacity: 0.3 }}/>
      {Array.from({ length: 5 }).map((_, i) => (
        <div key={i} className="vp-jelly" style={{
          left: `${10 + i * 18 + (i % 2) * 5}%`,
          animationDelay: `-${i * 1.2}s`,
          animationDuration: `${12 + i}s`,
          transform: `scale(${0.7 + (i % 3) * 0.2})`
        }}/>
      ))}
    </div>
  );
}

function HeroReef() {
  const HERO_VIDEOS = useMemo(() =>
    (window.HERO_VIDEOS && window.HERO_VIDEOS.length ? window.HERO_VIDEOS : HERO_VIDEOS_FALLBACK).map(v => ({
      ...v,
      classified: classifyHeroSource(v)
    })), []);

  const bubbles = useMemo(() =>
    Array.from({ length: 20 }, (_, i) => ({
      left: Math.random() * 100,
      delay: Math.random() * 12,
      duration: 8 + Math.random() * 10,
      size: 3 + Math.random() * 6,
      opacity: 0.2 + Math.random() * 0.4
    })), []);

  const [activeVid, setActiveVid] = useState(0);
  const [paused, setPaused] = useState(false);
  const videoRefs = useRef([]);

  // Auto-advance every 7 seconds
  useEffect(() => {
    if (paused) return;
    const id = setInterval(() => {
      setActiveVid(v => (v + 1) % HERO_VIDEOS.length);
    }, 7000);
    return () => clearInterval(id);
  }, [paused]);

  // Sync <video> playback to active slide
  useEffect(() => {
    videoRefs.current.forEach((v, i) => {
      if (!v) return;
      if (i === activeVid) v.play?.().catch(() => {});
      else { try { v.pause?.(); } catch(_) {} }
    });
  }, [activeVid]);

  const current = HERO_VIDEOS[activeVid];

  return (
    <div className="hero">
      <div className="hero-reef">
        <div className="caustics"/>
        <div className="bubbles">
          {bubbles.map((b, i) => (
            <div key={i} className="bubble" style={{
              left: `${b.left}%`, bottom: '-10px',
              width: b.size, height: b.size,
              animationDelay: `-${b.delay}s`,
              animationDuration: `${b.duration}s`,
              opacity: b.opacity
            }}/>
          ))}
        </div>
      </div>

      <div className="hero-content">
        <span className="hero-eyebrow">A field guide for reef keepers</span>
        <h1 className="hero-title">
          The ocean,<br/>in <em>your</em> living room.
        </h1>
        <p className="hero-lede">
          Species profiles, coral care, water chemistry diagnostics and beginner-to-expert guides; everything needed to keep a reef alive and thriving. Built by hobbyists, for hobbyists.
        </p>
        <div className="hero-actions">
          <a href="/guides" className="btn primary">Start a new tank →</a>
          <a href="/library" className="btn ghost">Browse the library</a>
        </div>
        <div className="hero-stats">
          <div><div className="hero-stat-n">{(window.SPECIES || []).length}</div><div className="hero-stat-l">Species & corals</div></div>
          <div><div className="hero-stat-n">{(window.ARTICLES || []).length}</div><div className="hero-stat-l">Field articles</div></div>
          <div><div className="hero-stat-n">13</div><div className="hero-stat-l">Diagnostic tools</div></div>
          <div><div className="hero-stat-n">{Object.keys(window.GUIDES || {}).length}</div><div className="hero-stat-l">Learning tracks</div></div>
        </div>
      </div>

      <div className="hero-right hero-video-stage"
           onMouseEnter={() => setPaused(true)}
           onMouseLeave={() => setPaused(false)}>
        <div className="hero-video-frame">
          {HERO_VIDEOS.map((v, i) => {
            const c = v.classified;
            return (
              <div key={i} className={`hero-video-slide ${i === activeVid ? 'on' : ''}`}>
                {c.type === "file" && (
                  <video
                    ref={el => videoRefs.current[i] = el}
                    src={c.src}
                    muted loop playsInline autoPlay={i === 0}
                    preload="metadata"
                    style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                  />
                )}
                {(c.type === "youtube" || c.type === "vimeo") && (
                  // YouTube and Vimeo embeds. They autoplay muted via URL params.
                  // The 16:9 iframe is scaled to cover the frame.
                  <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
                    <iframe
                      title={v.title}
                      src={i === activeVid || i === 0 ? c.embed : ""}
                      style={{
                        position: "absolute",
                        top: "50%", left: "50%",
                        transform: "translate(-50%, -50%)",
                        // Oversize the iframe so YouTube's letterboxing is hidden
                        width: "max(100%, 177.78vh)",
                        height: "max(56.25vw, 100%)",
                        minWidth: "100%", minHeight: "100%",
                        border: 0,
                        pointerEvents: "none"
                      }}
                      allow="autoplay; encrypted-media; picture-in-picture"
                      allowFullScreen
                    />
                  </div>
                )}
                {c.type === "instagram" && (
                  <a
                    href={c.url}
                    target="_blank"
                    rel="noopener"
                    className="hero-ig-card"
                    onClick={e => e.stopPropagation()}
                  >
                    <VideoPoster kind={v.poster || "anemone"}/>
                    <div className="hero-ig-card-overlay">
                      <div className="hero-ig-card-badge">
                        <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2">
                          <rect x="3" y="3" width="18" height="18" rx="5"/>
                          <circle cx="12" cy="12" r="4"/>
                          <circle cx="17.5" cy="6.5" r="0.5" fill="currentColor"/>
                        </svg>
                        Watch on Instagram ↗
                      </div>
                    </div>
                  </a>
                )}
                {c.type === "poster" && <VideoPoster kind={c.key}/>}
                <div className="hero-video-vignette"/>
              </div>
            );
          })}

          {/* Overlay UI: title, credit, progress dots, controls */}
          <div className="hero-video-ui">
            <div className="hero-video-top">
              <span className="hero-video-tag">{current.tag || "LIVE FOOTAGE"}</span>
              <button
                className="hero-video-pause"
                onClick={() => setPaused(p => !p)}
                aria-label={paused ? "Play" : "Pause"}
              >
                {paused ? "▶" : "❚❚"}
              </button>
            </div>

            <div className="hero-video-bottom">
              <div className="hero-video-meta">
                <div className="hero-video-title">{current.title}</div>
                <a href={current.creditUrl} className="hero-video-credit" onClick={e => e.stopPropagation()}>
                  <span className="hero-video-credit-dot"/>
                  {current.credit}
                </a>
              </div>

              <div className="hero-video-dots">
                {HERO_VIDEOS.map((_, i) => (
                  <button
                    key={i}
                    className={`hero-video-dot ${i === activeVid ? 'on' : ''} ${paused ? 'paused' : ''}`}
                    onClick={() => setActiveVid(i)}
                    aria-label={`Go to clip ${i + 1}`}
                  >
                    <span className="hero-video-dot-fill"/>
                  </button>
                ))}
              </div>
            </div>
          </div>
        </div>

        {/* Footer: contribute-a-video CTA */}
        <a href="/donate" className="hero-video-contribute">
          <span>↑</span> Submit a tank tour for the homepage
        </a>
      </div>
    </div>
  );
}

// ---------- Home ----------
function HomePage({ navigate }) {
  return (
    <>
      <HeroReef/>

      <div className="section">
        <div className="section-head">
          <div>
            <div className="kicker" style={{ marginBottom: 8 }}>Featured tools</div>
            <h2 className="section-title">Diagnose, plan, build.</h2>
          </div>
          <p className="section-lede">Six interactive tools built around the problems every reefkeeper actually hits.</p>
        </div>
        <div className="grid grid-3">
          {[
            { k: "01 / Health", t: "Water Parameter Scanner", d: "Enter your test kit readings, get instant status, reef-safe ranges, and what's likely drifting.", h: "#/tools/params" },
            { k: "02 / Planning", t: "Compatibility Matrix", d: "Check any pair of fish or corals. Sweeper tentacles, aggression, predator-prey; all encoded.", h: "#/tools/compat" },
            { k: "03 / Maintenance", t: "Dosing Calculator", d: "Alk, calcium, magnesium. Plug in your swing and get a dosing plan.", h: "#/tools/dosing" },
            { k: "04 / Diagnosis", t: "Symptom → Diagnosis", d: "Click the symptoms. We narrow it down to ich, velvet, HLLE, brown jelly, and the likely culprits.", h: "#/tools/symptoms" },
            { k: "05 / Setup", t: "Tank Setup Planner", d: "12-week timeline from wet salt to first fish. Checklist format, save progress.", h: "#/tools/planner" },
            { k: "06 / Library", t: "Livestock Library", d: `${(window.SPECIES || []).length} species and corals. Filter by difficulty, reef-safety, tank size, temperament.`, h: "#/library" }
          ].map((c, i) => (
            <a key={i} href={c.h} className="card link">
              <div className="card-kicker"><span className="bar"/>{c.k}</div>
              <h3>{c.t}</h3>
              <p>{c.d}</p>
            </a>
          ))}
        </div>
      </div>

      <div className="section">
        <div className="section-head">
          <div>
            <div className="kicker" style={{ marginBottom: 8 }}>Progression</div>
            <h2 className="section-title">Learning tracks.</h2>
          </div>
          <p className="section-lede">Start where you are. Every guide links to the next.</p>
        </div>
        <div className="grid grid-3">
          {[
            { stage: "Beginner", title: "Your first 90 days", body: "Cycle, CUC, first fish, first coral, first crisis. We walk you through all of it.", weeks: 12 },
            { stage: "Intermediate", title: "Stability & growth", body: "Dosing, additive management, aquascaping, the ULNS question, fragging your first colony.", weeks: 20 },
            { stage: "Advanced", title: "SPS, automation, scale", body: "Acropora husbandry, probe-driven automation, redundant life support, selective breeding.", weeks: 52 }
          ].map((t, i) => (
            <a key={i} href="/guides" className="card link">
              <div className="card-kicker"><span className="bar"/>Track {i + 1} · {t.weeks} weeks</div>
              <h3>{t.title}</h3>
              <p>{t.body}</p>
              <div style={{ marginTop: 14, display: 'flex', gap: 4 }}>
                {Array.from({length: 5}).map((_,j) => (
                  <div key={j} style={{ height: 3, flex: 1, borderRadius: 2, background: j <= i + 1 ? 'var(--cyan)' : 'var(--bg-3)' }}/>
                ))}
              </div>
            </a>
          ))}
        </div>
      </div>

      <div className="section">
        <div className="section-head">
          <div>
            <div className="kicker" style={{ marginBottom: 8 }}>Latest in the library</div>
            <h2 className="section-title">Fresh profiles.</h2>
          </div>
          <a href="/library" className="btn ghost small">All {(window.SPECIES || []).length} →</a>
        </div>
        <div className="species-grid">
          {useMemo(() => {
            const all = [...(window.SPECIES || [])];
            for (let i = all.length - 1; i > 0; i--) {
              const j = Math.floor(Math.random() * (i + 1));
              [all[i], all[j]] = [all[j], all[i]];
            }
            return all.slice(0, 4);
          }, []).map(sp => <SpeciesCard key={sp.id} sp={sp} onClick={() => navigate(`/species/${sp.id}`)}/>)}
        </div>
      </div>
    </>
  );
}

// ---------- Species card ----------
function SpeciesCard({ sp, onClick }) {
  const creatureEl = useMemo(() => {
    // 1) Real media takes priority: video > image > SVG fallback
    if (sp.video) {
      return (
        <video
          className="sp-video"
          src={sp.video}
          poster={sp.image || undefined}
          autoPlay
          muted
          loop
          playsInline
          preload="metadata"
        />
      );
    }
    if (sp.image) {
      return <img className="sp-photo" src={sp.image} alt={sp.common} loading="lazy"/>;
    }
    // 2) SVG creature fallback
    if (window.resolveCreature) return window.resolveCreature(sp, 120);
    const map = {
      ocellaris: <Creature.clownfish size={110}/>,
      mandarin: <Creature.mandarin size={120}/>,
      yellowtang: <Creature.tang size={120}/>,
      banggai: <Creature.cardinal size={80}/>,
      royalgramma: <Creature.gramma size={120}/>,
      sixline: <Creature.gramma size={120} color1="var(--coral)" color2="var(--cyan)"/>,
      firefish: <Creature.firefish size={120}/>,
      purpledotty: <Creature.gramma size={110}/>,
      greenstar: <Creature.zoa size={110}/>,
      hammer: <Creature.anemone size={110} color="oklch(0.75 0.14 100)"/>,
      torch: <Creature.anemone size={120} color="var(--amber)"/>,
      zoa: <Creature.zoa size={130}/>,
      acropora: <Creature.acro size={130} color="oklch(0.70 0.17 275)"/>,
      mushroom: <Creature.mushroom size={120}/>,
      duncan: <Creature.anemone size={110} color="var(--lime)"/>,
      cleaner: <Creature.shrimp size={130}/>,
      turbosnail: <Creature.snail size={70}/>,
    };
    return map[sp.id] || <Creature.clownfish size={100}/>;
  }, [sp.id]);

  return (
    <div className="species-card" onClick={onClick}>
      <div className="sp-media">
        <span className="sp-media-label">{sp.kind} · {sp.category}</span>
        <div className="sp-creature">{creatureEl}</div>
        {window.StarButton && <div className="sp-star"><window.StarButton kind="species" id={sp.id}/></div>}
        {(sp.image || sp.video) && sp.credit && (
          sp.creditUrl
            ? <a className="sp-credit" href={sp.creditUrl} target="_blank" rel="noopener" onClick={e => e.stopPropagation()}>📷 {sp.credit}</a>
            : <span className="sp-credit">📷 {sp.credit}</span>
        )}
      </div>
      <div className="sp-body">
        <div className="sp-category">{sp.category}</div>
        <div className="sp-common">{sp.common}</div>
        <div className="sp-latin">{sp.latin}</div>
        <div className="sp-tags">
          <span className="diff">
            {[1,2,3,4,5].map(i => <span key={i} className={`diff-dot ${i <= sp.difficulty ? 'on' : ''}`}/>)}
          </span>
          {sp.reefSafe === 'safe' && <span className="sp-tag green">reef safe</span>}
          {sp.reefSafe === 'invasive' && <span className="sp-tag amber">invasive</span>}
          {sp.temperament === 'peaceful' && <span className="sp-tag">peaceful</span>}
          {sp.temperament === 'semi-aggressive' && <span className="sp-tag amber">semi-aggressive</span>}
          {sp.temperament === 'aggressive-sweepers' && <span className="sp-tag coral">sweepers</span>}
        </div>
      </div>
    </div>
  );
}

window.HomePage = HomePage;
window.SpeciesCard = SpeciesCard;
window.HeroReef = HeroReef;


// === deploy/pages-library.jsx ===
// ReefReads. Library page

function LibraryPage({ navigate }) {
  const [filterKind, setFilterKind] = useState("all");
  const [filterDiff, setFilterDiff] = useState("all");
  const [filterReefSafe, setFilterReefSafe] = useState("all");
  const [filterTemper, setFilterTemper] = useState("all");
  const [minTank, setMinTank] = useState(0);
  const [query, setQuery] = useState("");

  const filtered = window.SPECIES.filter(sp => {
    if (filterKind !== "all" && sp.kind !== filterKind) return false;
    if (filterDiff !== "all" && sp.difficulty !== +filterDiff) return false;
    if (filterReefSafe !== "all" && sp.reefSafe !== filterReefSafe) return false;
    if (filterTemper !== "all" && sp.temperament !== filterTemper) return false;
    if (sp.minTank < minTank) return false;
    if (query && !(sp.common.toLowerCase().includes(query.toLowerCase()) || sp.latin.toLowerCase().includes(query.toLowerCase()))) return false;
    return true;
  });

  const Chip = ({ on, onClick, children }) => (
    <button className={`chip ${on ? 'active' : ''}`} onClick={onClick}>{children}</button>
  );

  return (
    <>
      <div style={{ marginBottom: 28 }}>
        <div className="kicker" style={{ marginBottom: 10 }}>Library · {filtered.length} of {window.SPECIES.length}</div>
        <h1 className="section-title" style={{ fontSize: 48 }}>Livestock & coral.</h1>
        <p className="section-lede" style={{ marginTop: 8, maxWidth: 620 }}>
          Filterable by difficulty, reef-safety, temperament, and minimum tank size. Click any card for layered care info.
        </p>
      </div>

      <div className="lib-layout">
        <aside className="filters">
          <div className="field" style={{ marginBottom: 18 }}>
            <label>Search</label>
            <input type="text" placeholder="clownfish, acro…" value={query} onChange={e => setQuery(e.target.value)}/>
          </div>

          <div className="filter-group">
            <h4>Kind</h4>
            <div className="chips">
              {[["all","All"],["fish","Fish"],["coral","Coral"],["invert","Inverts"]].map(([v,l]) => (
                <Chip key={v} on={filterKind === v} onClick={() => setFilterKind(v)}>{l}</Chip>
              ))}
            </div>
          </div>

          <div className="filter-group">
            <h4>Difficulty</h4>
            <div className="chips">
              {[["all","All"],["1","1"],["2","2"],["3","3"],["4","4"],["5","5"]].map(([v,l]) => (
                <Chip key={v} on={filterDiff === v} onClick={() => setFilterDiff(v)}>{l}</Chip>
              ))}
            </div>
          </div>

          <div className="filter-group">
            <h4>Reef safe</h4>
            <div className="chips">
              {[["all","All"],["safe","Safe"],["invasive","Invasive"]].map(([v,l]) => (
                <Chip key={v} on={filterReefSafe === v} onClick={() => setFilterReefSafe(v)}>{l}</Chip>
              ))}
            </div>
          </div>

          <div className="filter-group">
            <h4>Temperament</h4>
            <div className="chips">
              {[["all","Any"],["peaceful","Peaceful"],["semi-aggressive","Semi"],["aggressive-sweepers","Sweepers"]].map(([v,l]) => (
                <Chip key={v} on={filterTemper === v} onClick={() => setFilterTemper(v)}>{l}</Chip>
              ))}
            </div>
          </div>

          <div className="filter-group" style={{ marginBottom: 0 }}>
            <h4>Min tank size</h4>
            <div className="range-row">
              <span>{minTank} gal</span>
              <span>+</span>
            </div>
            <input type="range" min="0" max="100" step="5" value={minTank} onChange={e => setMinTank(+e.target.value)}/>
          </div>
        </aside>

        <div>
          {filtered.length === 0 ? (
            <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-3)', border: '1px dashed var(--line)', borderRadius: 12 }}>
              No species match those filters.
            </div>
          ) : (
            <div className="species-grid">
              {filtered.map(sp => <SpeciesCard key={sp.id} sp={sp} onClick={() => navigate(`/species/${sp.id}`)}/>)}
            </div>
          )}
        </div>
      </div>
    </>
  );
}

// ---------- Species detail ----------
function SpeciesDetail({ id, navigate }) {
  const sp = window.SPECIES.find(s => s.id === id);
  if (!sp) return <div>Species not found. <a href="/library">Back to library</a></div>;

  // Real photo / video override (sp.image / sp.video). Falls back to SVG.
  let mediaEl;
  if (sp.video) {
    mediaEl = (
      <video
        className="detail-video"
        src={sp.video}
        poster={sp.image || undefined}
        autoPlay
        muted
        loop
        playsInline
        controls
      />
    );
  } else if (sp.image) {
    mediaEl = <img className="detail-photo" src={sp.image} alt={sp.common}/>;
  } else {
    mediaEl = (window.resolveCreature && window.resolveCreature(sp, 240)) || <Creature.clownfish size={200}/>;
  }
  const hasMedia = !!(sp.image || sp.video);

  const ParamBar = ({ name, range, unit, fullRange }) => {
    const [lo, hi] = range;
    const [min, max] = fullRange;
    const pLo = ((lo - min) / (max - min)) * 100;
    const pHi = ((hi - min) / (max - min)) * 100;
    return (
      <div className="param-bar">
        <span className="name">{name}</span>
        <div className="bar-track">
          <div className="bar-range" style={{ left: `${pLo}%`, width: `${pHi - pLo}%` }}/>
        </div>
        <span className="value">{lo}–{hi} {unit}</span>
      </div>
    );
  };

  return (
    <>
      <div style={{ marginBottom: 18 }}>
        {window.Breadcrumb && <window.Breadcrumb trail={[
          { label: "Home", href: "/" },
          { label: "Species library", href: "/library" },
          { label: sp.kind.charAt(0).toUpperCase() + sp.kind.slice(1), href: `/library?kind=${sp.kind}` },
          { label: sp.category },
          { label: sp.common }
        ]}/>}
      </div>

      {/* HEADER STRIP */}
      <div className="detail">
        <div className="detail-media">
          {!hasMedia && <div className="caustics" style={{ inset: '-30%', opacity: 0.5 }}/>}
          {!hasMedia && <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '15%',
            background: 'linear-gradient(180deg, oklch(0.30 0.03 60 / 0), oklch(0.25 0.03 60))' }}/>}
          {hasMedia ? (
            mediaEl
          ) : (
            <div style={{ position: 'relative', zIndex: 2, animation: 'sway 5s ease-in-out infinite', transformOrigin: 'bottom center' }}>
              {mediaEl}
            </div>
          )}
          {hasMedia && sp.credit && (
            sp.creditUrl
              ? <a className="sp-credit" href={sp.creditUrl} target="_blank" rel="noopener">📷 {sp.credit}</a>
              : <span className="sp-credit">📷 {sp.credit}</span>
          )}
          <div style={{ position: 'absolute', top: 14, right: 14, zIndex: 4, fontFamily: 'var(--f-mono)', fontSize: 10,
            letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--ink-3)',
            background: hasMedia ? 'oklch(0.10 0.02 240 / 0.55)' : 'transparent',
            padding: hasMedia ? '3px 7px' : 0, borderRadius: 4, backdropFilter: hasMedia ? 'blur(4px)' : 'none' }}>
            ID · {sp.id.toUpperCase()}
          </div>
        </div>

        <div>
          <div className="detail-hdr">
            <div className="kicker">{sp.kind} · {sp.category}</div>
            <div style={{ display: 'flex', alignItems: 'flex-start', gap: 16, justifyContent: 'space-between', flexWrap: 'wrap' }}>
              <h1 className="detail-title" style={{ flex: 1, minWidth: 280 }}>{sp.common}</h1>
              {window.StarButton && <window.StarButton kind="species" id={sp.id} size="lg" label="Save to My Reef"/>}
            </div>
            <div className="detail-latin">{sp.latin}</div>
            <p style={{ marginTop: 14, fontSize: 15, lineHeight: 1.7, color: 'var(--ink-2)', maxWidth: 640, fontStyle: 'italic', borderLeft: '2px solid var(--line)', paddingLeft: 14 }}>{sp.short}</p>
            <p style={{ marginTop: 16, fontSize: 15, lineHeight: 1.75, color: 'var(--ink-1)', maxWidth: 640 }}>{sp.overview}</p>

            <div className="detail-meta">
              <div className="meta-cell">
                <div className="lab">Difficulty</div>
                <div className="val">
                  {window.DIFFICULTY_LABELS[sp.difficulty]}
                  <span className="diff" style={{ marginLeft: 10 }}>
                    {[1,2,3,4,5].map(i => <span key={i} className={`diff-dot ${i <= sp.difficulty ? 'on' : ''}`}/>)}
                  </span>
                </div>
              </div>
              <div className="meta-cell">
                <div className="lab">Minimum tank</div>
                <div className="val">{sp.minTank} gal</div>
              </div>
              <div className="meta-cell">
                <div className="lab">Temperament</div>
                <div className="val" style={{ textTransform: 'capitalize' }}>{sp.temperament}</div>
              </div>
              <div className="meta-cell">
                <div className="lab">Diet</div>
                <div className="val" style={{ textTransform: 'capitalize' }}>{sp.diet}</div>
              </div>
            </div>
          </div>
        </div>
      </div>

      {/* QUICK STATS STRIP */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 12, margin: '20px 0 18px' }}>
        <div className="info-block" style={{ padding: 16 }}>
          <h5>Max size</h5>
          <p style={{ fontFamily: 'var(--f-serif)', fontSize: 26, lineHeight: 1 }}>
            {sp.maxSize}<span style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--f-mono)', marginLeft: 3 }}>in</span>
          </p>
        </div>
        <div className="info-block" style={{ padding: 16 }}>
          <h5>Reef safety</h5>
          <p style={{ fontSize: 14, textTransform: 'capitalize' }}>{sp.reefSafe.replace('-', ' ')}</p>
        </div>
        <div className="info-block" style={{ padding: 16 }}>
          <h5>Origin</h5>
          <p style={{ fontSize: 13 }}>{sp.origin}</p>
        </div>
        <div className="info-block" style={{ padding: 16 }}>
          <h5>Activity</h5>
          <p style={{ fontSize: 13 }}>{sp.tags?.includes('night-active') ? 'Nocturnal' : sp.tags?.includes('shy') || sp.tags?.includes('cryptic') ? 'Cave-dwelling' : 'Diurnal'}</p>
        </div>
        <div className="info-block" style={{ padding: 16 }}>
          <h5>Category</h5>
          <p style={{ fontSize: 13 }}>{sp.category}</p>
        </div>
      </div>

      {/* MAIN BODY: 2-col grid with key sections */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
        <div className="info-block" style={{ padding: 22 }}>
          <h5>Husbandry & care</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 6 }}>{sp.care}</p>
        </div>
        <div className="info-block" style={{ padding: 22 }}>
          <h5>Behavior in the tank</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 6 }}>{sp.behavior}</p>
        </div>
        <div className="info-block" style={{ padding: 22 }}>
          <h5>Tankmates & compatibility</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 6 }}>{sp.tankmates}</p>
          <a href="/tools/pair" style={{ color: 'var(--cyan)', fontSize: 12, marginTop: 10, display: 'inline-block' }}>Test a pairing →</a>
        </div>
        <div className="info-block" style={{ padding: 22 }}>
          <h5>Origin & habitat</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 6 }}>
            Native to <b style={{ color: 'var(--ink-1)' }}>{sp.origin}</b>. {' '}
            {sp.kind === 'fish' ? `Reaches ${sp.maxSize}" in adulthood, requiring a minimum of ${sp.minTank} gallons of established saltwater habitat with appropriate ${sp.diet === 'herbivore' ? 'algae growth and nori supplementation' : sp.diet === 'carnivore' ? 'meaty foods and microfauna' : 'mixed diet of plant and animal matter'}.` :
             sp.kind === 'coral' ? `Best placed in a tank of ${sp.minTank} gallons or larger with appropriate flow and ${sp.tags?.includes('low-light') ? 'low-to-moderate' : sp.tags?.includes('high-light') ? 'high' : 'moderate'} lighting.` :
             `Suited to systems of ${sp.minTank} gallons and up. Plays a working role in the cleanup or biological balance of a mature reef.`}
          </p>
        </div>
      </div>

      {/* WARNINGS + TAGS */}
      <div style={{ display: 'grid', gridTemplateColumns: sp.warnings ? '1.6fr 1fr' : '1fr', gap: 16, marginBottom: 16 }}>
        {sp.warnings && (
          <div className="info-block" style={{ padding: 22, borderLeft: '3px solid var(--amber)' }}>
            <h5 style={{ color: 'var(--amber)' }}>⚠ Warnings</h5>
            <p style={{ fontSize: 14, lineHeight: 1.7, marginTop: 6 }}>{sp.warnings}</p>
          </div>
        )}
        {sp.tags && sp.tags.length > 0 && (
          <div className="info-block" style={{ padding: 22 }}>
            <h5>Tags</h5>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }}>
              {sp.tags.map(t => (
                <span key={t} className="sp-tag">{t}</span>
              ))}
            </div>
          </div>
        )}
      </div>

      {/* PARAMETER RANGES */}
      {sp.params && (
        <div className="info-block" style={{ padding: 26 }}>
          <h5>Target parameter ranges</h5>
          <div style={{ marginTop: 14 }}>
            {sp.params.temp && <ParamBar name="Temp" range={sp.params.temp} unit="°C" fullRange={[20, 30]}/>}
            {sp.params.sg && <ParamBar name="Salinity" range={sp.params.sg} unit="sg" fullRange={[1.020, 1.028]}/>}
            {sp.params.ph && <ParamBar name="pH" range={sp.params.ph} unit="" fullRange={[7.8, 8.6]}/>}
            {sp.params.nitrate && <ParamBar name="Nitrate" range={sp.params.nitrate} unit="ppm" fullRange={[0, 50]}/>}
            {sp.params.alk && <ParamBar name="Alk" range={sp.params.alk} unit="dKH" fullRange={[6, 12]}/>}
            {sp.params.ca && <ParamBar name="Calcium" range={sp.params.ca} unit="ppm" fullRange={[350, 500]}/>}
            {sp.params.mg && <ParamBar name="Magnesium" range={sp.params.mg} unit="ppm" fullRange={[1200, 1500]}/>}
          </div>
        </div>
      )}

      {window.LastUpdated && <window.LastUpdated override={sp.updated}/>}
    </>
  );
}

window.LibraryPage = LibraryPage;
window.SpeciesDetail = SpeciesDetail;


// === deploy/pages-tools.jsx ===
// ReefReads. Tools pages

// ---------- Water parameter scanner ----------
function ParamsTool() {
  const [vals, setVals] = useState({
    temp: 25, sg: 1.025, ph: 8.2, alk: 8.5, ca: 420, mg: 1350,
    nitrate: 5, phosphate: 0.05
  });

  const ranges = {
    temp: { ideal: [24, 27], full: [18, 32], unit: "°C" },
    sg: { ideal: [1.024, 1.026], full: [1.015, 1.030], unit: "sg" },
    ph: { ideal: [8.1, 8.4], full: [7.5, 9.0], unit: "" },
    alk: { ideal: [8, 10], full: [5, 14], unit: "dKH" },
    ca: { ideal: [400, 450], full: [300, 550], unit: "ppm" },
    mg: { ideal: [1300, 1400], full: [1000, 1600], unit: "ppm" },
    nitrate: { ideal: [1, 10], full: [0, 100], unit: "ppm" },
    phosphate: { ideal: [0.02, 0.1], full: [0, 1], unit: "ppm" }
  };
  const names = {
    temp: "Temperature", sg: "Salinity", ph: "pH", alk: "Alkalinity",
    ca: "Calcium", mg: "Magnesium", nitrate: "Nitrate", phosphate: "Phosphate"
  };

  const status = (k, v) => {
    const [lo, hi] = ranges[k].ideal;
    const pad = (hi - lo) * 0.25;
    if (v >= lo && v <= hi) return "good";
    if (v >= lo - pad && v <= hi + pad) return "warn";
    return "bad";
  };

  const advice = () => {
    const bad = Object.keys(vals).filter(k => status(k, vals[k]) === "bad");
    const warn = Object.keys(vals).filter(k => status(k, vals[k]) === "warn");
    if (bad.length === 0 && warn.length === 0) return { level: "good", text: "All parameters within reef-safe ranges. Keep up your testing cadence." };
    if (bad.length > 0) return { level: "bad", text: `Critical drift: ${bad.map(k => names[k]).join(", ")}. Address before adding livestock or fragging.` };
    return { level: "warn", text: `Trending: ${warn.map(k => names[k]).join(", ")}. Retest in 24h; small correction recommended.` };
  };

  const a = advice();

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 01 · Health</div>
        <h2 className="tool-title">Water parameter scanner</h2>
        <p className="tool-desc">Enter current readings. Markers snap to reef-safe ranges; status updates live.</p>
      </div>

      <div className="param-tool-grid">
        {Object.keys(vals).map(k => {
          const r = ranges[k];
          const st = status(k, vals[k]);
          const pLo = ((r.ideal[0] - r.full[0]) / (r.full[1] - r.full[0])) * 100;
          const pHi = ((r.ideal[1] - r.full[0]) / (r.full[1] - r.full[0])) * 100;
          const pMark = Math.max(0, Math.min(100, ((vals[k] - r.full[0]) / (r.full[1] - r.full[0])) * 100));
          return (
            <div key={k} className={`gauge ${st}`}>
              <div className="label"><span>{names[k]}</span><span>{st === "good" ? "OK" : st === "warn" ? "WATCH" : "ALERT"}</span></div>
              <div className="val">
                <input type="number" step={k === "sg" ? "0.001" : k === "phosphate" ? "0.01" : "0.1"}
                  value={vals[k]} onChange={e => setVals({ ...vals, [k]: +e.target.value })}/>
                <span className="unit">{r.unit}</span>
              </div>
              <div className="track">
                <div className="ideal" style={{ left: `${pLo}%`, width: `${pHi - pLo}%` }}/>
                <div className="marker" style={{ left: `calc(${pMark}% - 1px)` }}/>
              </div>
              <div className="status">{r.ideal[0]}–{r.ideal[1]} {r.unit}</div>
            </div>
          );
        })}
      </div>

      <div style={{ marginTop: 24, padding: 18, borderRadius: 10,
        background: a.level === "good" ? 'oklch(0.88 0.13 150 / 0.08)'
                  : a.level === "warn" ? 'oklch(0.82 0.14 75 / 0.08)'
                  : 'oklch(0.72 0.17 30 / 0.08)',
        border: `1px solid ${a.level === "good" ? 'oklch(0.88 0.13 150 / 0.3)' : a.level === "warn" ? 'oklch(0.82 0.14 75 / 0.3)' : 'oklch(0.72 0.17 30 / 0.3)'}`}}>
        <div className="kicker" style={{ marginBottom: 6,
          color: a.level === "good" ? 'var(--lime)' : a.level === "warn" ? 'var(--amber)' : 'var(--coral)' }}>
          {a.level === "good" ? "System stable" : a.level === "warn" ? "Watchlist" : "Needs attention"}
        </div>
        <p style={{ fontSize: 14 }}>{a.text}</p>
      </div>
    </div>
  );
}

// ---------- Compatibility matrix ----------
function CompatTool() {
  const picks = [
    "ocellaris", "yellowtang", "banggai", "royalgramma", "sixline", "firefish", "purpledotty", "mandarin"
  ].map(id => window.SPECIES.find(s => s.id === id));

  // Deterministic matrix
  const cell = (a, b) => {
    if (a.id === b.id) return { c: "self", l: "—" };
    // sixline is terror to mandarin & royalgramma
    if ((a.id === "sixline" && (b.id === "mandarin" || b.id === "royalgramma"))
     || (b.id === "sixline" && (a.id === "mandarin" || a.id === "royalgramma"))) return { c: "bad", l: "✗" };
    // two dottybacks
    if (a.id === "purpledotty" && b.id === "purpledotty") return { c: "bad", l: "✗" };
    // Two clowns fine
    // Tang + tang
    if (a.id === "yellowtang" && b.id === "yellowtang") return { c: "warn", l: "?" };
    if (a.temperament === "semi-aggressive" && b.temperament === "semi-aggressive") return { c: "warn", l: "?" };
    if (a.temperament === "peaceful" && b.temperament === "peaceful") return { c: "ok", l: "✓" };
    return { c: "ok", l: "✓" };
  };

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 02 · Planning</div>
        <h2 className="tool-title">Compatibility matrix</h2>
        <p className="tool-desc">Eight common reef fish. Green coexists, amber depends on tank size, red avoid.</p>
      </div>

      <div className="compat-table" style={{ gridTemplateColumns: `minmax(160px, 1.2fr) repeat(${picks.length}, minmax(0, 1fr))` }}>
        <div className="compat-cell hdr"/>
        {picks.map(p => (
          <div key={p.id} className="compat-cell hdr" title={p.common}>
            {p.common.split(" ")[0].slice(0, 6)}
          </div>
        ))}
        {picks.map(a => (
          <React.Fragment key={a.id}>
            <div className="compat-cell rowlabel">{a.common}</div>
            {picks.map(b => {
              const x = cell(a, b);
              return <div key={b.id} className={`compat-cell ${x.c}`}>{x.l}</div>;
            })}
          </React.Fragment>
        ))}
      </div>
      <div className="legend">
        <div><span className="legend-dot" style={{ background: 'oklch(0.88 0.13 150 / 0.6)' }}/>Compatible</div>
        <div><span className="legend-dot" style={{ background: 'oklch(0.82 0.14 75 / 0.6)' }}/>Depends on tank size</div>
        <div><span className="legend-dot" style={{ background: 'oklch(0.72 0.17 30 / 0.6)' }}/>Avoid</div>
      </div>
    </div>
  );
}

// ---------- Dosing calculator ----------
function DosingTool() {
  const [volume, setVolume] = useState(50);
  const [currentAlk, setCurrentAlk] = useState(7.5);
  const [targetAlk, setTargetAlk] = useState(8.5);
  const [currentCa, setCurrentCa] = useState(400);
  const [targetCa, setTargetCa] = useState(430);
  const [currentMg, setCurrentMg] = useState(1280);
  const [targetMg, setTargetMg] = useState(1350);

  // Simplified dosing formulas (approximate hobbyist standard)
  // 1 dKH = ~17.86 ppm CaCO3. Bionic/2-part: ~1 mL per gal raises ~0.14 dKH (varies).
  const mlAlk = Math.max(0, ((targetAlk - currentAlk) / 0.14) * (volume / 1));
  const mlCa = Math.max(0, ((targetCa - currentCa) / 2.8) * (volume / 1));
  const mlMg = Math.max(0, ((targetMg - currentMg) / 6) * (volume / 1));

  const Row = ({ label, current, setCurrent, target, setTarget, unit, ml, dose }) => (
    <div style={{ display: 'grid', gridTemplateColumns: '120px 1fr 1fr auto', gap: 14, alignItems: 'end', marginBottom: 14 }}>
      <div className="kicker" style={{ color: 'var(--ink)' }}>{label}</div>
      <div className="field" style={{ marginBottom: 0 }}>
        <label>Current ({unit})</label>
        <input type="number" step="0.1" value={current} onChange={e => setCurrent(+e.target.value)}/>
      </div>
      <div className="field" style={{ marginBottom: 0 }}>
        <label>Target ({unit})</label>
        <input type="number" step="0.1" value={target} onChange={e => setTarget(+e.target.value)}/>
      </div>
      <div style={{ textAlign: 'right', minWidth: 120 }}>
        <div className="kicker">Dose</div>
        <div style={{ fontFamily: 'var(--f-serif)', fontSize: 22 }}>{ml.toFixed(1)} mL</div>
        <div className="kicker" style={{ marginTop: 2 }}>{dose}</div>
      </div>
    </div>
  );

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 03 · Maintenance</div>
        <h2 className="tool-title">Two-part dosing calculator</h2>
        <p className="tool-desc">Assumes a standard two-part additive (Bionic/B-ionic). Split doses of 50+ mL across 2-3 applications.</p>
      </div>

      <div className="field" style={{ maxWidth: 240, marginBottom: 22 }}>
        <label>System volume (gal)</label>
        <input type="number" value={volume} onChange={e => setVolume(+e.target.value)}/>
      </div>

      <Row label="Alkalinity" current={currentAlk} setCurrent={setCurrentAlk} target={targetAlk} setTarget={setTargetAlk} unit="dKH" ml={mlAlk} dose="2-part Alk"/>
      <Row label="Calcium" current={currentCa} setCurrent={setCurrentCa} target={targetCa} setTarget={setTargetCa} unit="ppm" ml={mlCa} dose="2-part Ca"/>
      <Row label="Magnesium" current={currentMg} setCurrent={setCurrentMg} target={targetMg} setTarget={setTargetMg} unit="ppm" ml={mlMg} dose="Mg supp."/>

      <div style={{ marginTop: 18, padding: 14, borderRadius: 10, background: 'var(--bg-2)', border: '1px dashed var(--line)' }}>
        <div className="kicker" style={{ color: 'var(--cyan)', marginBottom: 6 }}>Rule of thumb</div>
        <p style={{ fontSize: 13, color: 'var(--ink-2)' }}>
          Never raise alkalinity by more than <b>1.4 dKH in 24h</b>. Faster swings cause tissue recession in SPS. Re-test 2h after dosing.
        </p>
      </div>
    </div>
  );
}

// ---------- Symptom diagnosis ----------
function SymptomTool() {
  const [picked, setPicked] = useState([]);

  const matches = window.DIAGNOSES.map(d => {
    const hits = d.matches.filter(m => picked.includes(m)).length;
    return { ...d, score: hits, ratio: hits / d.matches.length };
  }).filter(d => d.score > 0).sort((a, b) => b.ratio - a.ratio || b.score - a.score);

  const top = matches[0];

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 04 · Diagnosis</div>
        <h2 className="tool-title">Symptom to diagnosis</h2>
        <p className="tool-desc">Tap everything you're seeing. We'll narrow it down as you go.</p>
      </div>

      <div className="symptom-flow">
        <div>
          {["behavior", "visible", "coral", "water"].map(cat => (
            <div key={cat} style={{ marginBottom: 18 }}>
              <div className="kicker" style={{ marginBottom: 8 }}>{cat}</div>
              <div className="symptom-list">
                {window.SYMPTOMS.filter(s => s.category === cat).map(s => {
                  const on = picked.includes(s.id);
                  return (
                    <button key={s.id} className={`symptom-pill ${on ? 'active' : ''}`}
                      onClick={() => setPicked(p => on ? p.filter(i => i !== s.id) : [...p, s.id])}>
                      <span>{s.text}</span>
                      <span className="tick">{on ? "✓ selected" : "+ add"}</span>
                    </button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>

        <div>
          <div style={{ position: 'sticky', top: 80 }}>
            {picked.length === 0 ? (
              <div className="diagnosis" style={{ textAlign: 'center', color: 'var(--ink-3)' }}>
                <div style={{ fontFamily: 'var(--f-serif)', fontSize: 22, color: 'var(--ink-2)', marginBottom: 6 }}>
                  Awaiting input
                </div>
                <p style={{ fontSize: 13 }}>Select one or more symptoms on the left to see likely diagnoses and treatment paths.</p>
              </div>
            ) : top ? (
              <div className="diagnosis">
                <div className="diag-conf"><span className="dot"/>{top.confidence}</div>
                <h4>{top.title}</h4>
                <div className="diag-section">
                  <h5>What it is</h5>
                  <p>{top.what}</p>
                </div>
                <div className="diag-section">
                  <h5>Treatment protocol</h5>
                  <ul>{top.action.map((a, i) => <li key={i}>{a}</li>)}</ul>
                </div>
                {top.warn && (
                  <div className="diag-section">
                    <h5 style={{ color: 'var(--coral)' }}>⚠ Critical</h5>
                    <p style={{ color: 'var(--ink-2)' }}>{top.warn}</p>
                  </div>
                )}
                {matches.length > 1 && (
                  <div className="diag-section">
                    <h5>Also consider</h5>
                    <p>{matches.slice(1, 3).map(m => m.title).join(" · ")}</p>
                  </div>
                )}
              </div>
            ) : (
              <div className="diagnosis">
                <h4>Inconclusive</h4>
                <p style={{ fontSize: 13, color: 'var(--ink-2)' }}>Those symptoms don't match a common condition. Post a photo on the ReefReads community forum for human eyes.</p>
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- Tank setup planner ----------
function PlannerTool() {
  const phases = [
    { week: "Week 0", title: "Hardware & plumbing", tasks: ["Pressure-test plumbing with freshwater", "Mount return pump and wave-maker", "Rinse sand (if using) and scape dry rock", "Install heater(s) and ATO"] },
    { week: "Weeks 1–2", title: "Salt & cycle", tasks: ["Mix saltwater to 1.025 sg", "Add ammonia source (Dr. Tim's, raw shrimp, or bottled bacteria)", "Test ammonia/nitrite daily", "Run return at full bore to knock down dust"] },
    { week: "Weeks 3–4", title: "First livestock. CUC", tasks: ["Ammonia & nitrite read zero 72+ hours", "Drip-acclimate clean-up crew", "Start feeding schedule (nothing; they eat algae)", "Begin weekly 10% water change"] },
    { week: "Weeks 5–8", title: "First fish", tasks: ["Quarantine first pair (Ocellaris is classic)", "Start 3-week QT prophylactic treatment", "Keep nitrate 1–10 ppm, phosphate 0.02–0.1", "Dial in lighting photoperiod"] },
    { week: "Weeks 9–12", title: "First corals", tasks: ["Start with beginner soft corals (zoas, mushrooms)", "Dip all new frags", "Target-feed LPS weekly", "Begin two-part dosing if alk/ca drops"] },
    { week: "Month 4+", title: "Stability & expansion", tasks: ["Stock remaining fish (biggest/most aggressive last)", "Introduce LPS", "Measure alk uptake over 72h, is ULN feasible?", "Schedule annual: probe calibration, return impeller, ATO sensors"] }
  ];

  const [done, setDone] = useState(() => {
    try { return JSON.parse(localStorage.getItem('rr-planner') || '{}'); }
    catch { return {}; }
  });
  useEffect(() => {
    localStorage.setItem('rr-planner', JSON.stringify(done));
  }, [done]);

  const toggle = (phaseIdx, taskIdx) => {
    const key = `${phaseIdx}-${taskIdx}`;
    setDone(d => ({ ...d, [key]: !d[key] }));
  };

  const totalTasks = phases.reduce((s, p) => s + p.tasks.length, 0);
  const doneCount = Object.values(done).filter(Boolean).length;
  const pct = Math.round((doneCount / totalTasks) * 100);

  return (
    <div className="tool">
      <div className="tool-head">
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
          <div>
            <div className="kicker" style={{ marginBottom: 8 }}>Tool 05 · Setup</div>
            <h2 className="tool-title">Tank setup planner</h2>
            <p className="tool-desc">A sequenced checklist from empty glass to a stocked reef. Progress auto-saves.</p>
          </div>
          <div style={{ textAlign: 'right', minWidth: 140 }}>
            <div className="kicker">Progress</div>
            <div style={{ fontFamily: 'var(--f-serif)', fontSize: 32, color: 'var(--cyan)' }}>{pct}%</div>
            <div className="kicker">{doneCount} of {totalTasks}</div>
          </div>
        </div>
      </div>

      <div className="timeline">
        {phases.map((ph, i) => {
          const allDone = ph.tasks.every((_, j) => done[`${i}-${j}`]);
          return (
            <div key={i} className={`tl-phase ${allDone ? 'done' : ''}`}>
              <div className="tl-week">{ph.week}</div>
              <div className="tl-title">{ph.title}</div>
              <ul className="checklist">
                {ph.tasks.map((t, j) => {
                  const key = `${i}-${j}`;
                  const on = done[key];
                  return (
                    <li key={j} className={on ? 'done' : ''} onClick={() => toggle(i, j)}>
                      <div className="checkbox">
                        <svg width="10" height="10" viewBox="0 0 10 10"><path d="M1 5 L4 8 L9 2" stroke="oklch(0.15 0.03 240)" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      </div>
                      <div>
                        <div className="task-title">{t}</div>
                      </div>
                    </li>
                  );
                })}
              </ul>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ---------- Guides page ----------
function GuidesPage() {
  const G = window.GUIDES || {};
  const trackKeys = ["beginner", "intermediate", "advanced", "specialty"];
  const tracks = trackKeys.map(k => ({ key: k, ...G[k] })).filter(t => t.name);
  const [openTrack, setOpenTrack] = useState(0);
  const [openMod, setOpenMod] = useState(null);
  // 'basic' = the standard summary articles; 'detailed' = the deep dives
  // (only some modules have a body_detailed; rest fall back to body)
  const [mode, setMode] = useState(() => {
    try { return localStorage.getItem("rr-guides-mode") || "basic"; }
    catch { return "basic"; }
  });
  useEffect(() => { try { localStorage.setItem("rr-guides-mode", mode); } catch {} }, [mode]);

  const track = tracks[openTrack];
  const mod = openMod !== null ? track.modules[openMod] : null;
  const totalModules = tracks.reduce((s, t) => s + (t.modules ? t.modules.length : 0), 0);

  // Which body to render: prefer body_detailed when in detailed mode and it exists.
  const renderBody = (m) =>
    mode === "detailed" && m.body_detailed ? m.body_detailed : m.body;

  // Has the module been expanded with a detailed deep-dive?
  const hasDetailed = (m) => !!m.body_detailed;

  return (
    <>
      <div style={{ marginBottom: 32, display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 24, flexWrap: 'wrap' }}>
        <div>
          <div className="kicker" style={{ marginBottom: 10 }}>Guides · {tracks.length} tracks · {totalModules} modules</div>
          <h1 className="section-title" style={{ fontSize: 48 }}>Learning tracks.</h1>
          <p className="section-lede" style={{ marginTop: 8, maxWidth: 620 }}>
            Three progressions, full articles. Start anywhere, but honor the sequencing.
          </p>
        </div>

        {/* Depth toggle */}
        <div className="guides-depth-toggle">
          <div className="kicker" style={{ marginBottom: 6 }}>Reading depth</div>
          <div className="guides-depth-seg">
            <button className={mode === "basic" ? "on" : ""} onClick={() => setMode("basic")}>
              <strong>Basic</strong>
              <span>Quick reference</span>
            </button>
            <button className={mode === "detailed" ? "on" : ""} onClick={() => setMode("detailed")}>
              <strong>Detailed</strong>
              <span>Deep dive</span>
            </button>
          </div>
        </div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', gap: 32, alignItems: 'start' }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, position: 'sticky', top: 20 }}>
          {tracks.map((t, i) => (
            <button key={i} onClick={() => { setOpenTrack(i); setOpenMod(null); }}
              style={{ textAlign: 'left', padding: 20, borderRadius: 12,
                border: `1px solid ${openTrack === i ? 'var(--cyan)' : 'var(--line)'}`,
                background: openTrack === i ? 'oklch(0.82 0.14 195 / 0.08)' : 'var(--panel)',
                color: 'var(--ink)', cursor: 'pointer',
                boxShadow: openTrack === i ? '0 0 0 3px oklch(0.82 0.14 195 / 0.1)' : 'none',
                transition: 'all 150ms' }}>
              <div className="kicker" style={{ color: openTrack === i ? 'var(--cyan)' : 'var(--ink-3)', marginBottom: 6 }}>
                {t.kicker} · {t.weeks} weeks
              </div>
              <div style={{ fontFamily: 'var(--f-serif)', fontSize: 22 }}>{t.name}</div>
              <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4 }}>{t.modules.length} modules</div>
            </button>
          ))}
        </div>

        {mod ? (
          <div className="tool">
            <button onClick={() => setOpenMod(null)} className="btn ghost small" style={{ marginBottom: 18 }}>← All modules</button>
            <div className="kicker" style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 10 }}>
              <span>{track.kicker} · Module {String(openMod + 1).padStart(2, '0')} · {mod.read}</span>
              <span className={`guides-depth-pill ${mode}`}>
                {mode === "detailed" && hasDetailed(mod) ? "Detailed" : mode === "detailed" ? "Basic (no detailed version yet)" : "Basic"}
              </span>
            </div>
            <h2 className="tool-title" style={{ fontSize: 32, marginBottom: 28 }}>{mod.title}</h2>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 26, maxWidth: 720 }}>
              {renderBody(mod).map((b, i) => (
                <section key={i}>
                  <h4 style={{ fontFamily: 'var(--f-serif)', fontSize: 20, marginBottom: 8, color: 'var(--ink)' }}>{b.h}</h4>
                  <p style={{ fontSize: 15, lineHeight: 1.75, color: 'var(--ink-2)' }}>{b.p}</p>
                </section>
              ))}
            </div>
            {mode === "detailed" && !hasDetailed(mod) && (
              <p style={{ marginTop: 24, padding: '12px 16px', fontSize: 13, color: 'var(--ink-3)', background: 'var(--bg-3)', borderRadius: 8, fontStyle: 'italic' }}>
                A deeper version of this module is on the editorial backlog. The Basic version above is what's available today.
              </p>
            )}
            <div style={{ display: 'flex', gap: 10, marginTop: 40, paddingTop: 24, borderTop: '1px solid var(--line)' }}>
              {openMod > 0 && (
                <button className="btn ghost small" onClick={() => setOpenMod(openMod - 1)}>← {track.modules[openMod - 1].title}</button>
              )}
              {openMod < track.modules.length - 1 && (
                <button className="btn ghost small" style={{ marginLeft: 'auto' }} onClick={() => setOpenMod(openMod + 1)}>{track.modules[openMod + 1].title} →</button>
              )}
            </div>
          </div>
        ) : (
          <div className="tool">
            <div className="kicker" style={{ marginBottom: 8 }}>{track.kicker}</div>
            <h2 className="tool-title">{track.name} track</h2>
            <p className="tool-desc" style={{ marginBottom: 24 }}>{track.desc}</p>
            <div style={{ display: 'grid', gap: 2, borderRadius: 10, overflow: 'hidden', border: '1px solid var(--line)' }}>
              {track.modules.map((m, i) => (
                <button key={i} onClick={() => setOpenMod(i)}
                   style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
                    background: 'var(--bg-2)', border: 'none', color: 'var(--ink)',
                    transition: 'background 150ms', cursor: 'pointer', textAlign: 'left' }}
                   onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-3)'}
                   onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-2)'}>
                  <div style={{ fontFamily: 'var(--f-mono)', fontSize: 11, color: 'var(--cyan)', width: 30 }}>
                    {String(i + 1).padStart(2, '0')}
                  </div>
                  <div style={{ flex: 1, fontSize: 14 }}>{m.title}</div>
                  {hasDetailed(m) && <span className="guides-depth-badge" title="Detailed deep-dive available">+</span>}
                  <div style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-3)' }}>{m.read}</div>
                  <div style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-3)', letterSpacing: '0.1em', textTransform: 'uppercase' }}>
                    Read →
                  </div>
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    </>
  );
}

window.ParamsTool = ParamsTool;
window.CompatTool = CompatTool;
window.DosingTool = DosingTool;
window.SymptomTool = SymptomTool;
window.PlannerTool = PlannerTool;
window.GuidesPage = GuidesPage;


// === deploy/pages-tools-extra.jsx ===
// ReefReads, additional tools

// ---------- Pair compatibility checker ----------
function PairCompat() {
  const all = window.SPECIES;
  const [aId, setAId] = uS("ocellaris");
  const [bId, setBId] = uS("yellowtang");
  const a = all.find(s => s.id === aId);
  const b = all.find(s => s.id === bId);

  const analyze = (a, b) => {
    if (!a || !b) return null;
    if (a.id === b.id) return { level: "self", title: "Same species", notes: ["Consider a pair or group of this species instead."] };
    const notes = [];
    let level = "ok";

    // Fish + fish
    if (a.kind === "fish" && b.kind === "fish") {
      if (a.category === "Wrasse" && b.category === "Wrasse" && a.id !== b.id) { level = "warn"; notes.push("Wrasses frequently fight, only combine in tanks 90gal+ with distinct body shapes."); }
      if (a.id === "sixline" && ["mandarin","firefish","watchman","royalgramma","basslet"].includes(b.id)) { level = "bad"; notes.push("Six Line Wrasse harasses slow/shy fish relentlessly."); }
      if (b.id === "sixline" && ["mandarin","firefish","watchman","royalgramma","basslet"].includes(a.id)) { level = "bad"; notes.push("Six Line Wrasse harasses slow/shy fish relentlessly."); }
      if (a.category === "Dottyback" && b.category === "Dottyback") { level = "bad"; notes.push("Two dottybacks will fight to the death."); }
      if (a.category === "Tang" && b.category === "Tang") { level = "warn"; notes.push("Two tangs require 6ft+ of swimming length. Add simultaneously; different genera preferred."); }
      if (a.category === "Dwarf Angel" && b.category === "Dwarf Angel") { level = "bad"; notes.push("One dwarf angel per tank except in very large systems."); }
      if (a.category === "Clownfish" && b.category === "Clownfish" && a.id !== b.id) { level = "bad"; notes.push("Different clownfish species fight; stick to one species."); }
      if (a.temperament === "semi-aggressive" && b.temperament === "peaceful" && b.maxSize <= 3) { level = level === "bad" ? "bad" : "warn"; notes.push(`${a.common} may bully smaller peaceful fish.`); }
      if (b.temperament === "semi-aggressive" && a.temperament === "peaceful" && a.maxSize <= 3) { level = level === "bad" ? "bad" : "warn"; notes.push(`${b.common} may bully smaller peaceful fish.`); }
      if (a.temperament === "peaceful" && b.temperament === "peaceful" && notes.length === 0) notes.push("Generally peaceful combination.");
    }

    // Coral + fish
    if ((a.kind === "coral" && b.kind === "fish") || (a.kind === "fish" && b.kind === "coral")) {
      const fish = a.kind === "fish" ? a : b;
      const coral = a.kind === "coral" ? a : b;
      if (fish.reefSafe === "with-caution") { level = "warn"; notes.push(`${fish.common} is not reliably reef-safe; may nip ${coral.category}.`); }
      else notes.push(`${fish.common} does not target ${coral.category}.`);
      if (coral.temperament === "aggressive-sweepers") notes.push("Leave 6–8 inches from other corals for sweeper tentacles.");
      if (coral.reefSafe === "invasive") notes.push("Isolate this coral; it spreads aggressively.");
    }

    // Coral + coral
    if (a.kind === "coral" && b.kind === "coral") {
      const euphy = id => ["hammer","torch","frogspawn"].includes(id);
      if (euphy(a.id) && euphy(b.id)) { level = "ok"; notes.push("Euphyllia trio coexist, can touch without stinging."); }
      else if (a.temperament === "aggressive-sweepers" || b.temperament === "aggressive-sweepers") { level = "warn"; notes.push("Sweeper tentacles will sting non-Euphyllia neighbors. Keep 6+ inches apart."); }
      if (a.reefSafe === "invasive" || b.reefSafe === "invasive") { level = level === "bad" ? "bad" : "warn"; notes.push("Invasive coral present; isolate or it will overtake."); }
      if (a.temperament === "chemical" || b.temperament === "chemical") { level = level === "bad" ? "bad" : "warn"; notes.push("Leather corals release allelopathic terpenes. Run activated carbon."); }
      if (notes.length === 0) notes.push("Peaceful pairing.");
    }

    // Invert + fish
    if ((a.kind === "invert" && b.kind === "fish") || (a.kind === "fish" && b.kind === "invert")) {
      const fish = a.kind === "fish" ? a : b;
      const inv = a.kind === "invert" ? a : b;
      if (inv.category === "Shrimp" && fish.category === "Dottyback") { level = "warn"; notes.push("Dottybacks may harass small shrimp."); }
      if (inv.category === "Shrimp" && fish.category === "Wrasse" && fish.maxSize >= 5) { level = "warn"; notes.push("Larger wrasses may eat small shrimp."); }
      if (notes.length === 0) notes.push("Compatible.");
    }

    // Parameter compatibility
    if (a.params?.temp && b.params?.temp) {
      const lo = Math.max(a.params.temp[0], b.params.temp[0]);
      const hi = Math.min(a.params.temp[1], b.params.temp[1]);
      if (lo > hi) { level = "bad"; notes.unshift("Parameter mismatch: incompatible temperature ranges."); }
    }

    return { level, title: level === "ok" ? "Compatible" : level === "warn" ? "Conditionally compatible" : "Incompatible", notes };
  };

  const result = analyze(a, b);

  const Select = ({ value, onChange }) => (
    <select value={value} onChange={e => onChange(e.target.value)}>
      <optgroup label="Fish">
        {all.filter(s => s.kind === "fish").map(s => <option key={s.id} value={s.id}>{s.common}</option>)}
      </optgroup>
      <optgroup label="Corals">
        {all.filter(s => s.kind === "coral").map(s => <option key={s.id} value={s.id}>{s.common}</option>)}
      </optgroup>
      <optgroup label="Inverts">
        {all.filter(s => s.kind === "invert").map(s => <option key={s.id} value={s.id}>{s.common}</option>)}
      </optgroup>
    </select>
  );

  const bg = result?.level === "ok" ? "oklch(0.88 0.13 150 / 0.08)"
           : result?.level === "warn" ? "oklch(0.82 0.14 75 / 0.08)"
           : result?.level === "bad" ? "oklch(0.72 0.17 30 / 0.08)"
           : "var(--bg-2)";
  const bd = result?.level === "ok" ? "oklch(0.88 0.13 150 / 0.3)"
           : result?.level === "warn" ? "oklch(0.82 0.14 75 / 0.3)"
           : result?.level === "bad" ? "oklch(0.72 0.17 30 / 0.3)"
           : "var(--line)";
  const col = result?.level === "ok" ? "var(--lime)"
           : result?.level === "warn" ? "var(--amber)"
           : result?.level === "bad" ? "var(--coral)"
           : "var(--ink-2)";

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 07 · Planning</div>
        <h2 className="tool-title">Pair compatibility checker</h2>
        <p className="tool-desc">Pick any two species, corals or inverts; we run aggression, diet, parameter and pest checks.</p>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', gap: 20, alignItems: 'center', marginBottom: 24 }}>
        <div className="field" style={{ marginBottom: 0 }}>
          <label>Species A</label>
          <Select value={aId} onChange={setAId}/>
        </div>
        <div style={{ fontFamily: 'var(--f-serif)', fontSize: 28, color: 'var(--ink-3)' }}>×</div>
        <div className="field" style={{ marginBottom: 0 }}>
          <label>Species B</label>
          <Select value={bId} onChange={setBId}/>
        </div>
      </div>

      {result && (
        <div style={{ padding: 22, borderRadius: 12, background: bg, border: `1px solid ${bd}` }}>
          <div className="kicker" style={{ color: col, marginBottom: 8 }}>Verdict</div>
          <h3 style={{ fontFamily: 'var(--f-serif)', fontSize: 28, marginBottom: 14 }}>{result.title}</h3>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            {result.notes.map((n, i) => (
              <li key={i} style={{ padding: '6px 0 6px 18px', position: 'relative', fontSize: 13.5, color: 'var(--ink-2)', lineHeight: 1.6 }}>
                <span style={{ position: 'absolute', left: 0, top: 12, width: 8, height: 1, background: col }}/>
                {n}
              </li>
            ))}
          </ul>
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginTop: 20 }}>
        {[a, b].map((s, i) => s && (
          <div key={i} style={{ padding: 16, borderRadius: 10, border: '1px solid var(--line)', background: 'var(--bg-2)' }}>
            <div className="kicker" style={{ marginBottom: 6 }}>{s.kind} · {s.category}</div>
            <div style={{ fontFamily: 'var(--f-serif)', fontSize: 20 }}>{s.common}</div>
            <div style={{ fontFamily: 'var(--f-serif)', fontStyle: 'italic', color: 'var(--ink-3)', fontSize: 13 }}>{s.latin}</div>
            <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 8, fontFamily: 'var(--f-mono)' }}>
              Min tank · {s.minTank}gal &nbsp; Temp · {s.temperament}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ---------- Parameter log (history) ----------
function ParamLog() {
  const [entries, setEntries] = uS(() => {
    try { return JSON.parse(localStorage.getItem('rr-paramlog') || '[]'); }
    catch { return []; }
  });
  const [form, setForm] = uS({ temp: 25, sg: 1.025, ph: 8.2, alk: 8.5, ca: 420, mg: 1350, nitrate: 5, phosphate: 0.05 });

  uE(() => { localStorage.setItem('rr-paramlog', JSON.stringify(entries)); }, [entries]);

  const addEntry = () => {
    const e = { ...form, date: Date.now() };
    setEntries([e, ...entries].slice(0, 30));
  };

  const removeEntry = (i) => setEntries(entries.filter((_, j) => j !== i));

  const units = { temp: "°C", sg: "sg", ph: "", alk: "dKH", ca: "ppm", mg: "ppm", nitrate: "ppm", phosphate: "ppm" };
  const keys = Object.keys(form);

  // Sparkline
  const Spark = ({ field }) => {
    if (entries.length < 2) return <span style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-3)' }}>—</span>;
    const vals = entries.slice().reverse().map(e => e[field]);
    const min = Math.min(...vals), max = Math.max(...vals);
    const rng = max - min || 1;
    const w = 80, h = 24;
    const pts = vals.map((v, i) => `${(i / (vals.length - 1)) * w},${h - ((v - min) / rng) * h}`).join(" ");
    return (
      <svg width={w} height={h} style={{ verticalAlign: 'middle' }}>
        <polyline points={pts} fill="none" stroke="var(--cyan)" strokeWidth="1.5"/>
        <circle cx={w} cy={h - ((vals[vals.length-1] - min) / rng) * h} r="2" fill="var(--cyan)"/>
      </svg>
    );
  };

  const fmt = ts => {
    const d = new Date(ts);
    return `${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} · ${d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`;
  };

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 06 · Health</div>
        <h2 className="tool-title">Parameter log</h2>
        <p className="tool-desc">Log test results. We chart trends and auto-save locally; 30 most recent entries kept.</p>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 14 }}>
        {keys.map(k => (
          <div key={k} className="field" style={{ marginBottom: 0 }}>
            <label>{k} ({units[k]})</label>
            <input type="number" step={k === "sg" ? "0.001" : k === "phosphate" ? "0.01" : "0.1"}
              value={form[k]} onChange={e => setForm({ ...form, [k]: +e.target.value })}/>
          </div>
        ))}
      </div>
      <button className="btn primary" onClick={addEntry}>Log entry</button>

      <div style={{ marginTop: 24 }}>
        <div className="kicker" style={{ marginBottom: 10 }}>Trends · last {entries.length} {entries.length === 1 ? "entry" : "entries"}</div>
        {entries.length === 0 ? (
          <div style={{ padding: 24, textAlign: 'center', border: '1px dashed var(--line)', borderRadius: 10, color: 'var(--ink-3)' }}>
            No entries yet. Log your first reading above.
          </div>
        ) : (
          <>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 20 }}>
              {keys.map(k => (
                <div key={k} style={{ padding: 12, border: '1px solid var(--line)', borderRadius: 10, background: 'var(--bg-2)' }}>
                  <div className="kicker" style={{ marginBottom: 4 }}>{k}</div>
                  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                    <div style={{ fontFamily: 'var(--f-serif)', fontSize: 18 }}>{entries[0][k]}<span style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-3)', marginLeft: 4 }}>{units[k]}</span></div>
                    <Spark field={k}/>
                  </div>
                </div>
              ))}
            </div>
            <div style={{ maxHeight: 260, overflowY: 'auto', border: '1px solid var(--line)', borderRadius: 10 }}>
              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12, fontFamily: 'var(--f-mono)' }}>
                <thead>
                  <tr style={{ background: 'var(--bg-3)', color: 'var(--ink-3)', letterSpacing: '0.1em', textTransform: 'uppercase', fontSize: 10 }}>
                    <th style={{ padding: '8px 10px', textAlign: 'left' }}>Date</th>
                    {keys.map(k => <th key={k} style={{ padding: '8px 10px', textAlign: 'right' }}>{k}</th>)}
                    <th></th>
                  </tr>
                </thead>
                <tbody>
                  {entries.map((e, i) => (
                    <tr key={e.date} style={{ borderTop: i ? '1px solid var(--line)' : 'none' }}>
                      <td style={{ padding: '8px 10px', color: 'var(--ink-2)' }}>{fmt(e.date)}</td>
                      {keys.map(k => <td key={k} style={{ padding: '8px 10px', textAlign: 'right', color: 'var(--ink-2)' }}>{e[k]}</td>)}
                      <td style={{ padding: '8px 10px', textAlign: 'right' }}>
                        <button onClick={() => removeEntry(i)} style={{ color: 'var(--ink-3)', fontSize: 10 }}>×</button>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ---------- Stocking calculator ----------
function StockingToolLegacy() {
  const [volume, setVolume] = uS(50);
  const [picks, setPicks] = uS({});
  const all = window.SPECIES.filter(s => s.kind === "fish");

  const add = id => setPicks(p => ({ ...p, [id]: (p[id] || 0) + 1 }));
  const sub = id => setPicks(p => { const n = { ...p, [id]: Math.max(0, (p[id] || 0) - 1) }; if (n[id] === 0) delete n[id]; return n; });

  const entries = Object.entries(picks).map(([id, n]) => ({ sp: all.find(s => s.id === id), n })).filter(e => e.sp);

  // Rule of thumb: 1 inch per 5 gal, plus min-tank enforcement
  const totalInches = entries.reduce((s, e) => s + e.sp.maxSize * e.n, 0);
  const capacity = volume / 5;
  const load = (totalInches / capacity) * 100;

  const warnings = [];
  entries.forEach(e => {
    if (e.sp.minTank > volume) warnings.push(`${e.sp.common} needs at least ${e.sp.minTank} gal.`);
  });
  if (load > 100) warnings.push("Overstocked, expect aggression and elevated nitrates.");
  else if (load > 80) warnings.push("Near capacity; watch parameters closely.");

  const loadColor = load > 100 ? "var(--coral)" : load > 80 ? "var(--amber)" : "var(--lime)";

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 08 · Planning</div>
        <h2 className="tool-title">Stocking calculator</h2>
        <p className="tool-desc">Build a fish list and see bioload, tank-size fit, and compatibility warnings.</p>
      </div>

      <div className="field" style={{ maxWidth: 240 }}>
        <label>Tank volume (gal)</label>
        <input type="number" value={volume} onChange={e => setVolume(+e.target.value)}/>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, marginTop: 12 }}>
        <div>
          <div className="kicker" style={{ marginBottom: 10 }}>Available fish</div>
          <div style={{ maxHeight: 400, overflowY: 'auto', border: '1px solid var(--line)', borderRadius: 10 }}>
            {all.map(s => (
              <div key={s.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 14px', borderBottom: '1px solid var(--line)' }}>
                <div>
                  <div style={{ fontSize: 13.5 }}>{s.common}</div>
                  <div style={{ fontSize: 10.5, color: 'var(--ink-3)', fontFamily: 'var(--f-mono)' }}>{s.minTank}gal min · {s.maxSize}"</div>
                </div>
                <button className="btn ghost small" onClick={() => add(s.id)}>+ Add</button>
              </div>
            ))}
          </div>
        </div>

        <div>
          <div className="kicker" style={{ marginBottom: 10 }}>Your stocking list</div>
          {entries.length === 0 ? (
            <div style={{ padding: 20, textAlign: 'center', border: '1px dashed var(--line)', borderRadius: 10, color: 'var(--ink-3)', fontSize: 13 }}>
              Add fish from the left to build your list.
            </div>
          ) : (
            <>
              <div style={{ border: '1px solid var(--line)', borderRadius: 10, marginBottom: 14 }}>
                {entries.map(e => (
                  <div key={e.sp.id} style={{ display: 'flex', alignItems: 'center', padding: '10px 14px', borderBottom: '1px solid var(--line)' }}>
                    <div style={{ flex: 1 }}>
                      <div style={{ fontSize: 13.5 }}>{e.sp.common} × {e.n}</div>
                      <div style={{ fontSize: 10.5, color: 'var(--ink-3)', fontFamily: 'var(--f-mono)' }}>{(e.sp.maxSize * e.n).toFixed(1)}" total</div>
                    </div>
                    <button className="btn ghost small" onClick={() => sub(e.sp.id)}>−</button>
                    <button className="btn ghost small" style={{ marginLeft: 4 }} onClick={() => add(e.sp.id)}>+</button>
                  </div>
                ))}
              </div>

              <div style={{ padding: 16, border: '1px solid var(--line)', borderRadius: 10, background: 'var(--bg-2)' }}>
                <div className="kicker" style={{ marginBottom: 8 }}>Bioload</div>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                  <div style={{ fontFamily: 'var(--f-serif)', fontSize: 28, color: loadColor }}>{Math.round(load)}%</div>
                  <div style={{ fontFamily: 'var(--f-mono)', fontSize: 11, color: 'var(--ink-3)' }}>{totalInches.toFixed(1)}" of {capacity.toFixed(1)}"</div>
                </div>
                <div style={{ height: 6, background: 'var(--bg-3)', borderRadius: 3, marginTop: 8, overflow: 'hidden' }}>
                  <div style={{ width: `${Math.min(100, load)}%`, height: '100%', background: loadColor, transition: 'width 250ms' }}/>
                </div>
                {warnings.length > 0 && (
                  <ul style={{ listStyle: 'none', padding: 0, marginTop: 12 }}>
                    {warnings.map((w, i) => (
                      <li key={i} style={{ fontSize: 12, color: 'var(--amber)', padding: '3px 0' }}>⚠ {w}</li>
                    ))}
                  </ul>
                )}
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

// ---------- Expanded planner ----------
function PlannerToolExpanded() {
  // ----- branching task data -----
  // Three knobs: size (nano|mid|large), focus (softie|mixed|sps), automation (manual|auto)
  const PLAN = {
    base: [
      { week: "Week 0", title: "Research & purchase", tasks: [
        "Define budget, scope, and long-term goals (softies / LPS / SPS)",
        "Measure floor space, wet tanks are ~10 lb/gal",
        "Electrical: GFCI outlet, drip loops planned",
        "Shortlist tank, stand, and filtration",
        "Order salt, RODI unit, test kits"
      ]},
      { week: "Week 0.5", title: "Hardware & plumbing", tasks: [
        "Pressure-test plumbing with freshwater (48h)",
        "Mount return pump and wave-maker",
        "Rinse sand (if using) and scape dry rock",
        "Install heater(s) and ATO",
        "Label every valve and cord"
      ]},
      { week: "Weeks 1–2", title: "Salt & cycle kickoff", tasks: [
        "Mix saltwater to 1.025 sg; verify with refractometer",
        "Add ammonia source (Dr. Tim's, raw shrimp, or bottled bacteria)",
        "Test ammonia/nitrite daily",
        "Run return at full bore to knock down dust",
        "Photoperiod off; no benefit to lighting yet"
      ]},
      { week: "Weeks 3–4", title: "Cycle completion & CUC", tasks: [
        "Ammonia & nitrite read zero 72+ hours",
        "Start photoperiod at 50%; ramp over 2 weeks",
        "Drip-acclimate clean-up crew (snails + hermits)",
        "Begin weekly 10% water change cadence",
        "Baseline full parameter test"
      ]},
      { week: "Weeks 5–8", title: "Quarantine & first fish", tasks: [
        "Set up QT tank (10-20 gal bare-bottom)",
        "Quarantine first pair. Ocellaris is classic",
        "3-week QT prophylactic: copper + prazi",
        "Keep display nitrate 1–10, phosphate 0.02–0.1",
        "Dial in lighting photoperiod to final spec"
      ]}
    ],
    // Focus-specific coral phases
    softie: { week: "Weeks 9–12", title: "Starter softies", tasks: [
      "Add zoas, mushrooms, GSP, Xenia",
      "Dip every frag in CoralRx for 10–15 min",
      "Mount low, most softies prefer 50–100 PAR",
      "Skip target feeding; most softies feed on dissolved organics",
      "Run carbon to suppress allelopathy from leathers/GSP"
    ]},
    mixed: { week: "Weeks 9–12", title: "First LPS colony", tasks: [
      "Add Euphyllia (torch / hammer / frogspawn), Duncan, candy cane",
      "Dip every frag in Bayer for 5 min, rinse, place",
      "Target-feed LPS weekly with mysis or reef roids",
      "Begin two-part dosing if alk drops >0.2 dKH/week",
      "Photograph corals for growth baseline"
    ]},
    sps: { week: "Weeks 9–12", title: "Easy SPS pilot", tasks: [
      "Confirm 4 consecutive weeks of stable alk (±0.2 dKH)",
      "Start with encrusting Montipora; never an Acropora as your first SPS",
      "Mount mid-rock; verify PAR with borrowed meter (~200 PAR)",
      "Begin amino acid dosing 2× per week",
      "Daily alkalinity log for the first 4 weeks of SPS"
    ]},

    longterm: [
      { week: "Months 4–6", title: "Stabilization", tasks: [
        "Stock remaining fish (biggest/most aggressive last)",
        "Measure alk uptake over 72h, is dosing needed?",
        "Calibrate all probes (monthly minimum)",
        "Build a spare-parts kit: impeller, heater, extra salt",
        "First ICP test to baseline trace elements"
      ]},
      { week: "Year 1+", title: "Long-term maintenance", tasks: [
        "Replace bulbs or verify PAR annually",
        "Full probe calibration quarterly",
        "Review and rotate dosing schedule",
        "Annual salinity and refractometer verification",
        "Photograph corals monthly for growth reference",
        "Travel plan: reef-sitter briefing, automation check"
      ]}
    ],

    // Add-ons per knob
    nano: { week: "Weeks 1–2 (nano-specific)", title: "Nano discipline", tasks: [
      "Schedule weekly 15–20% water change, your filtration",
      "Verify ATO every 3 days, small tanks swing salinity overnight",
      "One small fish max per 5 gallons; pick the species before you cycle",
      "Skip the skimmer (most nanos don't fit one)"
    ]},
    large: { week: "Weeks 0.5+ (large-specific)", title: "Large-system extras", tasks: [
      "Drill or confirm overflow path; pre-buy spare bulkheads",
      "Plumb the sump with full-bore unions for service",
      "Two heaters at 50% each for redundancy",
      "Plan a battery backup for at least one powerhead",
      "Wire emergency shutoff for the return pump to a float switch"
    ]},
    auto: { week: "Month 2+ (automation)", title: "Automation rollout", tasks: [
      "Wire heater(s) through Apex/Inkbird temperature controller",
      "Add ATO sensor + reservoir (5+ gal capacity)",
      "Install dosing pumps for two-part (Kamoer / Jebao DP-4)",
      "Push notifications to phone for temp / leak / pH alarms",
      "Schedule monthly Apex Fusion check + log export"
    ]},
    manual: { week: "Month 2+ (manual)", title: "Manual cadence", tasks: [
      "Set a weekly water-change calendar reminder",
      "Log alk reading every Sunday in the Reef Journal",
      "Top off by hand twice daily; set a phone alarm",
      "Replace test reagents every 6 months",
      "Maintenance checklist taped to the tank stand"
    ]}
  };

  const [size, setSize] = uS(() => localStorage.getItem('rr-plan-size') || 'mid');
  const [focus, setFocus] = uS(() => localStorage.getItem('rr-plan-focus') || 'mixed');
  const [auto, setAuto] = uS(() => localStorage.getItem('rr-plan-auto') || 'manual');
  uE(() => { localStorage.setItem('rr-plan-size', size); }, [size]);
  uE(() => { localStorage.setItem('rr-plan-focus', focus); }, [focus]);
  uE(() => { localStorage.setItem('rr-plan-auto', auto); }, [auto]);

  const phases = [
    ...PLAN.base,
    ...(size === 'nano' ? [PLAN.nano] : []),
    ...(size === 'large' ? [PLAN.large] : []),
    PLAN[focus],
    PLAN[auto],
    ...PLAN.longterm
  ];

  // Keyed by size+focus+auto so each plan has independent progress
  const planKey = `rr-planner-${size}-${focus}-${auto}`;
  const [done, setDone] = uS(() => {
    try { return JSON.parse(localStorage.getItem(planKey) || '{}'); }
    catch { return {}; }
  });
  uE(() => {
    try { setDone(JSON.parse(localStorage.getItem(planKey) || '{}')); }
    catch { setDone({}); }
  }, [planKey]);
  uE(() => { localStorage.setItem(planKey, JSON.stringify(done)); }, [done, planKey]);

  const toggle = (i, j) => {
    const key = `${i}-${j}`;
    setDone(d => ({ ...d, [key]: !d[key] }));
  };
  const resetPlan = () => { if (confirm("Reset progress for this plan?")) setDone({}); };

  const totalTasks = phases.reduce((s, p) => s + p.tasks.length, 0);
  const doneCount = phases.reduce((s, p, i) => s + p.tasks.filter((_, j) => done[`${i}-${j}`]).length, 0);
  const pct = Math.round((doneCount / totalTasks) * 100);

  // Approximate cost estimator
  const sizeCost = { nano: { eq: [600, 1200], live: [200, 500] }, mid: { eq: [1500, 3000], live: [500, 1500] }, large: { eq: [3500, 8000], live: [1500, 5000] } }[size];
  const focusMult = { softie: 1.0, mixed: 1.3, sps: 1.8 }[focus];
  const autoExtra = auto === 'auto' ? [400, 1500] : [0, 100];
  const costLow = Math.round(sizeCost.eq[0] + sizeCost.live[0] * focusMult + autoExtra[0]);
  const costHigh = Math.round(sizeCost.eq[1] + sizeCost.live[1] * focusMult + autoExtra[1]);

  // Approximate timeline weeks
  const baseWeeks = { nano: 8, mid: 12, large: 16 }[size];
  const focusWeeks = { softie: 0, mixed: 4, sps: 12 }[focus];
  const totalWeeks = baseWeeks + focusWeeks;

  const Pill = ({ active, onClick, children, sub }) => (
    <button onClick={onClick}
      style={{ padding: '10px 14px', borderRadius: 10, cursor: 'pointer', textAlign: 'left',
        border: `1px solid ${active ? 'var(--cyan)' : 'var(--line)'}`,
        background: active ? 'oklch(0.82 0.14 195 / 0.12)' : 'var(--bg-2)',
        color: 'var(--ink)', transition: 'all 150ms',
        boxShadow: active ? '0 0 0 3px oklch(0.82 0.14 195 / 0.1)' : 'none' }}>
      <div style={{ fontSize: 13, fontWeight: 500 }}>{children}</div>
      <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>{sub}</div>
    </button>
  );

  return (
    <div className="tool">
      <div className="tool-head">
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 16 }}>
          <div>
            <div className="kicker" style={{ marginBottom: 8 }}>Tool 05 · Setup</div>
            <h2 className="tool-title">Tank setup planner</h2>
            <p className="tool-desc">Pick a path. The plan adapts to your tank size, target ecosystem, and how hands-on you want to be.</p>
          </div>
          <div style={{ textAlign: 'right', minWidth: 140 }}>
            <div className="kicker">Progress</div>
            <div style={{ fontFamily: 'var(--f-serif)', fontSize: 40, color: 'var(--cyan)', lineHeight: 1 }}>{pct}%</div>
            <div className="kicker">{doneCount} of {totalTasks}</div>
          </div>
        </div>
        <div style={{ height: 4, background: 'var(--bg-3)', borderRadius: 2, marginTop: 16, overflow: 'hidden' }}>
          <div style={{ width: `${pct}%`, height: '100%', background: 'linear-gradient(90deg, var(--cyan), var(--lime))', transition: 'width 300ms' }}/>
        </div>
      </div>

      {/* ----- Path selector ----- */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20, marginTop: 28, marginBottom: 28 }}>
        <div>
          <div className="kicker" style={{ marginBottom: 10 }}>Tank size</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            <Pill active={size === 'nano'} onClick={() => setSize('nano')} sub="Under 30 gal · AIO or pico">Nano</Pill>
            <Pill active={size === 'mid'} onClick={() => setSize('mid')} sub="40–90 gal · most home builds">Mid-size</Pill>
            <Pill active={size === 'large'} onClick={() => setSize('large')} sub="100+ gal · sumped, drilled">Large</Pill>
          </div>
        </div>
        <div>
          <div className="kicker" style={{ marginBottom: 10 }}>Coral focus</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            <Pill active={focus === 'softie'} onClick={() => setFocus('softie')} sub="Zoas, mushrooms, leathers">Softie-dominant</Pill>
            <Pill active={focus === 'mixed'} onClick={() => setFocus('mixed')} sub="LPS + softies + easy SPS">Mixed reef</Pill>
            <Pill active={focus === 'sps'} onClick={() => setFocus('sps')} sub="Acropora and friends">SPS-focused</Pill>
          </div>
        </div>
        <div>
          <div className="kicker" style={{ marginBottom: 10 }}>Style</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            <Pill active={auto === 'manual'} onClick={() => setAuto('manual')} sub="Weekly checks, hand dosing">Hands-on</Pill>
            <Pill active={auto === 'auto'} onClick={() => setAuto('auto')} sub="Apex, dosers, ATO">Automated</Pill>
          </div>
        </div>
      </div>

      {/* ----- Plan summary ----- */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 22 }}>
        <div className="gauge good">
          <div className="label"><span>Plan length</span></div>
          <div className="val">{totalWeeks} weeks</div>
        </div>
        <div className="gauge good">
          <div className="label"><span>Total tasks</span></div>
          <div className="val">{totalTasks}</div>
        </div>
        <div className="gauge good">
          <div className="label"><span>Est. startup</span></div>
          <div className="val">${costLow.toLocaleString()}–${costHigh.toLocaleString()}</div>
        </div>
        <div className="gauge good">
          <div className="label"><span>Plan key</span></div>
          <div className="val" style={{ fontSize: 14, textTransform: 'uppercase', letterSpacing: '0.08em' }}>{size}/{focus}/{auto}</div>
        </div>
      </div>

      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }}>
        <p style={{ fontSize: 13, color: 'var(--ink-3)' }}>Progress is saved per plan; switching paths keeps your previous progress intact.</p>
        <button onClick={resetPlan} className="btn ghost small">Reset this plan</button>
      </div>

      <div className="timeline">
        {phases.map((ph, i) => {
          const allDone = ph.tasks.every((_, j) => done[`${i}-${j}`]);
          return (
            <div key={i} className={`tl-phase ${allDone ? 'done' : ''}`}>
              <div className="tl-week">{ph.week}</div>
              <div className="tl-title">{ph.title}</div>
              <ul className="checklist">
                {ph.tasks.map((t, j) => {
                  const key = `${i}-${j}`;
                  const on = done[key];
                  return (
                    <li key={j} className={on ? 'done' : ''} onClick={() => toggle(i, j)}>
                      <div className="checkbox">
                        <svg width="10" height="10" viewBox="0 0 10 10"><path d="M1 5 L4 8 L9 2" stroke="oklch(0.15 0.03 240)" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      </div>
                      <div><div className="task-title">{t}</div></div>
                    </li>
                  );
                })}
              </ul>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ---------- Water change calculator ----------
function WaterChangeTool() {
  const [volume, setVolume] = uS(50);
  const [pct, setPct] = uS(10);
  const [freq, setFreq] = uS("weekly");
  const [saltBrand, setSaltBrand] = uS("red-sea-blue");

  const brands = {
    "red-sea-blue": { name: "Red Sea Coral Pro", lbPerGal: 0.54, dkh: 12.0 },
    "instant-ocean": { name: "Instant Ocean", lbPerGal: 0.50, dkh: 8.8 },
    "io-reef": { name: "Instant Ocean Reef Crystals", lbPerGal: 0.52, dkh: 10.5 },
    "tropic-marin": { name: "Tropic Marin Pro Reef", lbPerGal: 0.55, dkh: 8.5 },
    "aquaforest": { name: "Aquaforest Reef Salt", lbPerGal: 0.55, dkh: 7.5 }
  };

  const gallons = (volume * pct) / 100;
  const brand = brands[saltBrand];
  const lb = gallons * brand.lbPerGal;
  const annual = freq === "weekly" ? gallons * 52 : freq === "biweekly" ? gallons * 26 : gallons * 12;
  const annualLb = annual * brand.lbPerGal;

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 09 · Maintenance</div>
        <h2 className="tool-title">Water change calculator</h2>
        <p className="tool-desc">Plan salt consumption and water-change volume by brand.</p>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14, marginBottom: 22 }}>
        <div className="field" style={{ marginBottom: 0 }}><label>Tank volume (gal)</label>
          <input type="number" value={volume} onChange={e => setVolume(+e.target.value)}/></div>
        <div className="field" style={{ marginBottom: 0 }}><label>Change %</label>
          <input type="number" value={pct} onChange={e => setPct(+e.target.value)}/></div>
        <div className="field" style={{ marginBottom: 0 }}><label>Frequency</label>
          <select value={freq} onChange={e => setFreq(e.target.value)}>
            <option value="weekly">Weekly</option>
            <option value="biweekly">Bi-weekly</option>
            <option value="monthly">Monthly</option>
          </select></div>
        <div className="field" style={{ marginBottom: 0 }}><label>Salt brand</label>
          <select value={saltBrand} onChange={e => setSaltBrand(e.target.value)}>
            {Object.entries(brands).map(([k, b]) => <option key={k} value={k}>{b.name}</option>)}
          </select></div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
        {[
          ["Per change", `${gallons.toFixed(1)} gal`],
          ["Salt per change", `${lb.toFixed(2)} lb`],
          ["Annual water", `${annual.toFixed(0)} gal`],
          ["Annual salt", `${annualLb.toFixed(1)} lb`]
        ].map(([l, v], i) => (
          <div key={i} className="gauge good">
            <div className="label"><span>{l}</span></div>
            <div className="val">{v}</div>
          </div>
        ))}
      </div>

      <div style={{ marginTop: 20, padding: 14, borderRadius: 10, background: 'var(--bg-2)', border: '1px dashed var(--line)' }}>
        <div className="kicker" style={{ color: 'var(--cyan)', marginBottom: 6 }}>Salt profile</div>
        <p style={{ fontSize: 13, color: 'var(--ink-2)' }}>
          {brand.name} mixes to approximately <b>{brand.dkh} dKH</b> alkalinity at 1.025 sg. For reef tanks, aim for a salt whose newly-mixed alkalinity matches your target to minimize parameter shock after water changes.
        </p>
      </div>
    </div>
  );
}

// ---------- Livestock Journal ----------
function JournalTool() {
  const [entries, setEntries] = uS(() => {
    try { return JSON.parse(localStorage.getItem('rr-journal') || '[]'); }
    catch { return []; }
  });
  const [draft, setDraft] = uS("");

  uE(() => { localStorage.setItem('rr-journal', JSON.stringify(entries)); }, [entries]);

  const add = () => {
    if (!draft.trim()) return;
    setEntries([{ text: draft.trim(), date: Date.now() }, ...entries]);
    setDraft("");
  };
  const remove = i => setEntries(entries.filter((_, j) => j !== i));

  const fmt = ts => {
    const d = new Date(ts);
    return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }) + " · " + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
  };

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 10 · Reference</div>
        <h2 className="tool-title">Reef journal</h2>
        <p className="tool-desc">Log observations, additions, losses, parameter events. Stored locally.</p>
      </div>

      <div className="field">
        <label>New entry</label>
        <textarea rows="3" placeholder="Added two Ocellaris. Both eating pellet same day. Alk dosed 12mL."
          value={draft} onChange={e => setDraft(e.target.value)}/>
      </div>
      <button className="btn primary" onClick={add}>Add entry</button>

      <div style={{ marginTop: 22, display: 'flex', flexDirection: 'column', gap: 10 }}>
        {entries.length === 0 ? (
          <div style={{ padding: 24, textAlign: 'center', border: '1px dashed var(--line)', borderRadius: 10, color: 'var(--ink-3)' }}>
            Journal is empty. Your notes appear here.
          </div>
        ) : entries.map((e, i) => (
          <div key={e.date} style={{ padding: 14, border: '1px solid var(--line)', borderRadius: 10, background: 'var(--bg-2)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
              <div className="kicker">{fmt(e.date)}</div>
              <button onClick={() => remove(i)} style={{ color: 'var(--ink-3)', fontSize: 11 }}>remove</button>
            </div>
            <p style={{ fontSize: 14, color: 'var(--ink)', marginTop: 6, whiteSpace: 'pre-wrap' }}>{e.text}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

// ---------- Glossary ----------
function GlossaryTool() {
  const terms = [
    // Water chemistry
    { t: "Alkalinity (dKH)", c: "Chemistry", d: "Measure of carbonate buffering in reef water. Target 8–10 dKH; stability matters more than exact value." },
    { t: "Specific Gravity (SG)", c: "Chemistry", d: "A density ratio versus pure water. Reef target 1.025 at 25 °C. Read with a calibrated refractometer, not a hydrometer." },
    { t: "Salinity (ppt)", c: "Chemistry", d: "Mass of dissolved salts in parts per thousand. 35 ppt is the reef target. SG and ppt are equivalent, most refractometers report SG." },
    { t: "pH", c: "Chemistry", d: "Acidity of tank water. Target 8.0–8.4 daytime. House CO₂ from human breathing depresses pH; well-ventilated rooms and refugiums offset this." },
    { t: "ORP", c: "Chemistry", d: "Oxidation-Reduction Potential, measured in mV. Indicates how 'clean' or oxidizing the water is. 300–400 mV is healthy. Drops indicate organic load." },
    { t: "TDS", c: "Chemistry", d: "Total Dissolved Solids, measured in ppm. RODI output should read 0. Climbing TDS at the tap is normal seasonally, climbing TDS post-DI means the resin is spent." },
    { t: "Calcium (Ca)", c: "Chemistry", d: "Reef target 400–450 ppm. Consumed by SPS, LPS, coralline algae, and clams as they build skeleton." },
    { t: "Magnesium (Mg)", c: "Chemistry", d: "Reef target 1280–1400 ppm. Buffers calcium and alkalinity so they don't precipitate out as each other. Low Mg makes both impossible to hold." },
    { t: "Potassium (K)", c: "Chemistry", d: "Trace element pulled by SPS for red/pink pigmentation. Target ~400 ppm. Difficult to test without ICP." },
    { t: "Strontium (Sr)", c: "Chemistry", d: "Trace element involved in coral skeleton formation. Target ~8 ppm. Replenished by most salt mixes; rarely needs supplementation." },
    { t: "Iodine (I)", c: "Chemistry", d: "Trace element consumed by soft corals, anemones, and shrimp during molting. Depletes invisibly; supplement weekly or via ICP-guided dosing." },
    { t: "Redfield ratio", c: "Chemistry", d: "Nitrogen-to-phosphorus ratio in healthy reef water, roughly 16:1 by atoms. Tanks with skewed ratios (e.g. high N + zero P) often see dinos and cyano." },
    { t: "ULNS", c: "Chemistry", d: "Ultra-Low Nutrient System. Now considered outdated, modern reefkeeping targets measurable nitrate (2–10 ppm) and phosphate (0.03–0.10 ppm)." },

    // Filtration & equipment
    { t: "ATO", c: "Equipment", d: "Auto Top-Off. Replaces evaporated freshwater to keep salinity stable. The single most useful piece of automation." },
    { t: "RODI", c: "Equipment", d: "Reverse Osmosis + Deionization. The water you should be using: 0 TDS pure water, free of phosphate, copper, and chloramine." },
    { t: "Skimmer", c: "Equipment", d: "A reaction chamber that pulls organic compounds out of solution by binding them to fine bubble surfaces. Empties as foam into a collection cup." },
    { t: "Sump", c: "Equipment", d: "A secondary tank below the display, housing equipment, refugium, and water-chemistry gear. Keeps the display clean and adds water volume." },
    { t: "HOB", c: "Equipment", d: "Hang-On-Back filter or skimmer. The starter alternative to a sump for tanks without an overflow." },
    { t: "Refugium", c: "Equipment", d: "A secondary chamber with macroalgae for nutrient export and pod cultivation. Lit on reverse photoperiod to stabilize pH." },
    { t: "DSB", c: "Equipment", d: "Deep Sand Bed. 4+ inches of fine sand cultured to host anaerobic bacteria that break down nitrate to nitrogen gas." },
    { t: "BPF", c: "Equipment", d: "Bare bottom or barebottom. A tank with no substrate, used for SPS-focused systems where detritus removal matters more than aesthetics." },
    { t: "Closed loop", c: "Equipment", d: "An external circulation pump that pulls water from one part of the tank and returns it to another via plumbing. Powerful, silent, expensive to install." },
    { t: "Powerhead / wavemaker", c: "Equipment", d: "Submerged pump generating flow inside the display. Modern controllers oscillate them to mimic surf." },
    { t: "Apex / GHL / Hydros", c: "Equipment", d: "Aquarium controllers. Probes for pH, temperature, ORP, salinity feed a controller that can switch outlets, alarm, and dose reagents based on rules." },
    { t: "Trident", c: "Equipment", d: "Neptune's automated alkalinity / calcium / magnesium tester. Runs 3 tests per day, drives dosing pumps based on real measurements." },
    { t: "ICP-OES", c: "Equipment", d: "Inductively Coupled Plasma test, mail-in. Measures 30+ elements from a 50ml sample. Useful for trends and outliers, not weekly tuning." },

    // Coral types
    { t: "SPS", c: "Corals", d: "Small Polyp Stony coral. Acropora, Montipora, Stylophora, Pocillopora. Demand high light, high flow, stable parameters." },
    { t: "LPS", c: "Corals", d: "Large Polyp Stony coral. Euphyllia (torch/hammer/frogspawn), brains, Duncans, chalices, acans. Moderate light, moderate flow." },
    { t: "Softie", c: "Corals", d: "Soft coral with no calcium skeleton, leathers, zoas, mushrooms, Xenia, GSP. Most beginner-friendly category." },
    { t: "NPS", c: "Corals", d: "Non-Photosynthetic. Corals that get all energy from feeding. Dendronephthya, Sun Corals (Tubastrea), gorgonians, chili corals." },
    { t: "Zoa / Palys", c: "Corals", d: "Zoanthid and Palythoa, colonial polyps with infinite named color morphs. Some Palythoa carry palytoxin; wear gloves." },
    { t: "Acro", c: "Corals", d: "Acropora, the genus that defines high-end SPS. Hundreds of named morphs (PC Rainbow, Pikachu, Tierra Del Fuego)." },
    { t: "Monti", c: "Corals", d: "Montipora, encrusting, plating, or branching SPS. The 'easy SPS' starting point. Includes Setosa, Capricornis, Digitata." },
    { t: "Euphyllia", c: "Corals", d: "Genus of LPS that contains Torch, Hammer, Frogspawn, Octospawn. All siblings, they can touch, but sting other corals." },

    // Lighting & flow
    { t: "PAR", c: "Lighting", d: "Photosynthetically Active Radiation. The usable-light metric for corals, in μmol/m²/s. Targets: 50–100 softies, 100–200 LPS, 200–400 SPS." },
    { t: "PUR", c: "Lighting", d: "Photosynthetically Usable Radiation. Subset of PAR that corals actually use for photosynthesis, the blue and red wavelengths." },
    { t: "Kelvin (K)", c: "Lighting", d: "Color temperature. Low K (5000K) looks yellow/warm. High K (20000K) looks deep blue. Reef tanks typically run 14000–22000K aesthetically." },
    { t: "Photoperiod", c: "Lighting", d: "Total daily hours of lighting, including ramps. 8–10 hours is standard with a 4–6 hour peak window." },
    { t: "Acclimation (light)", c: "Lighting", d: "Gradual increase in light intensity over 2–4 weeks after adding a new fixture or moving a coral up the rock. Skipping it bleaches everything." },
    { t: "Gyre flow", c: "Lighting", d: "Cross-tank circulation pattern produced by linear pumps. Distributes flow without dead zones better than point-source powerheads." },
    { t: "Turnover rate", c: "Lighting", d: "Gallons per hour through the display divided by tank volume. Reef target: 20–40× for mixed reefs, 40–60× for SPS-heavy systems." },

    // Care / pests / disease
    { t: "Cycle", c: "Care", d: "Establishing the nitrifying bacteria that convert ammonia → nitrite → nitrate. 2–6 weeks. Never skip." },
    { t: "Bioload", c: "Care", d: "Total biological waste production, fish, inverts, and uneaten food combined. Higher bioload = more aggressive filtration needed." },
    { t: "Stocking", c: "Care", d: "Adding fish gradually so the bacterial colony can adjust. Rule of thumb: one small fish per week after cycle." },
    { t: "QT", c: "Care", d: "Quarantine tank. 4 weeks of observation + prophylactic copper and prazi treatment before any fish enters the display." },
    { t: "Fallow", c: "Care", d: "A fishless waiting period (76 days for ich/velvet) that lets parasites die out without hosts. The only true eradication." },
    { t: "Dip", c: "Care", d: "Chemical bath for new frags or fish to kill pests. Common: Bayer, CoralRx, iodine for corals; methylene blue for fish." },
    { t: "Drip acclimation", c: "Care", d: "Slow drip of tank water into a bucket holding new livestock, over 45–90 minutes. Matches salinity and pH without shock." },
    { t: "Frag", c: "Care", d: "A cut piece of coral used for propagation, trade, or sale. The fundamental unit of reef commerce." },
    { t: "Frag plug", c: "Care", d: "A small ceramic disc used to mount fragged coral. Glued onto rockwork once tissue has encrusted past the plug edge." },
    { t: "Scape", c: "Care", d: "The rock and sand arrangement. Dry rock is preferred over live rock for pest avoidance. Negative space matters more than mass." },
    { t: "Ich", c: "Pests & disease", d: "Cryptocaryon irritans. White-spot parasite that breaks out under stress. Treatment: copper or transfer method in QT. 76-day fallow in display." },
    { t: "Velvet", c: "Pests & disease", d: "Amyloodinium ocellatum. Fast-killing protozoa, gold-dust appearance, gill damage. 48–72 hour fatal window. Copper in QT, formalin baths." },
    { t: "Brook", c: "Pests & disease", d: "Brooklynella hostilis. Slimy white mucus on clownfish, peeling skin, clamped fins. Formalin baths + transfer method." },
    { t: "Uronema", c: "Pests & disease", d: "Parasitic ciliate causing red hemorrhagic patches on chromis and anthias. Metronidazole or formalin baths." },
    { t: "Aiptasia", c: "Pests & disease", d: "Pest anemone. Stings corals, multiplies fast. Biological control: peppermint shrimp (small), Berghia nudibranchs (any size), Copperband Butterfly (large)." },
    { t: "Majano", c: "Pests & disease", d: "Pest anemone resembling a tiny bubble-tip. Tougher than aiptasia. Berghia don't eat them, manual removal or syringe injection of Aiptasia-X." },
    { t: "AEFW", c: "Pests & disease", d: "Acropora-Eating Flatworm. Camouflaged on SPS, sucks tissue. Treatment: FlatwormExit, multiple iodine dips of all SPS." },
    { t: "Red bug", c: "Pests & disease", d: "Tegastes acroporanus, an Acropora-specific copepod. Treatment: Interceptor (milbemycin) tank-wide for 4 doses." },
    { t: "Bryopsis", c: "Pests & disease", d: "Feather-like nuisance algae, glued onto rockwork. Magnesium dosing to 1800 ppm (Tech-M brand specifically) is the historical treatment." },
    { t: "Dinos", c: "Pests & disease", d: "Dinoflagellates. Brown slime with O₂ bubbles, day-active. Root cause: stripped nutrients. Fix nutrients first, UV second." },
    { t: "Cyano", c: "Pests & disease", d: "Cyanobacteria. Red/purple slime sheets on sand and rock. Driven by poor flow + accumulated detritus. Chemiclean for short-term." },
    { t: "GHA", c: "Pests & disease", d: "Green Hair Algae. Phosphate-driven. Manual removal, increased flow, herbivore introduction (Kole tang, sea hare), GFO as last resort." },
    { t: "RTN", c: "Pests & disease", d: "Rapid Tissue Necrosis. Hours-to-days bleaching of SPS, leaving clean white skeleton. Usually triggered by parameter event." },
    { t: "STN", c: "Pests & disease", d: "Slow Tissue Necrosis. Millimeters-per-day tissue recession in SPS, often from the base. Less acute than RTN but harder to halt." },
    { t: "Allelopathy", c: "Pests & disease", d: "Chemical warfare between corals. Leather corals, GSP, and some softies release terpenes that stunt SPS. Run carbon to remove." },
    { t: "HLLE", c: "Pests & disease", d: "Head and Lateral Line Erosion. Pitting along sensory line in tangs and angels. Causes: diet, activated carbon dust, stray voltage." },

    // Slang & community
    { t: "LFS", c: "Slang", d: "Local Fish Store. The good ones are family. The bad ones are why captive-bred matters." },
    { t: "WC", c: "Slang", d: "Wild-Caught. Imported from the ocean. Some species can't be captive-bred yet; many can." },
    { t: "CB", c: "Slang", d: "Captive-Bred. Hatchery-raised. Hardier, ethical, often more expensive; worth it for clowns, dottybacks, gobies." },
    { t: "MACNA", c: "Slang", d: "Marine Aquarium Conference of North America. The yearly trade show; vendors, talks, frag swaps." },
    { t: "Reef madness", c: "Slang", d: "The compulsion to buy 'just one more' frag, fish, or piece of equipment. Universal hobbyist affliction." },
    { t: "Tank of the Month", c: "Slang", d: "Recurring feature on Reef2Reef and others showcasing exceptional tanks. Aspirational but distorts expectations for beginners." },
    { t: "Frag swap", c: "Slang", d: "Community event where reefers trade coral frags. The best way to enter the hobby cheaply and build local connections." },
    { t: "Nano", c: "Slang", d: "Tank under 30 gallons. Pico is under 10 gallons. Both are harder than 'normal' size for the same reasons." },
    { t: "Bombproof", c: "Slang", d: "A species or piece of gear that survives anything. Ocellaris clowns, Trochus snails, basic heaters." },
    { t: "WTB / WTS / FS", c: "Slang", d: "Want To Buy / Want To Sell / For Sale. Standard tags on hobbyist forums and Facebook groups." },
    { t: "Dry start", c: "Slang", d: "Cycling and aquascaping the tank without livestock, sometimes for months, before introducing animals. Disciplined approach." }
  ];

  const cats = ["All", ...Array.from(new Set(terms.map(t => t.c)))];
  const [q, setQ] = uS("");
  const [cat, setCat] = uS("All");
  const ql = q.toLowerCase();
  const f = terms.filter(t => (cat === "All" || t.c === cat) && (t.t.toLowerCase().includes(ql) || t.d.toLowerCase().includes(ql)));

  return (
    <div className="tool">
      <div className="tool-head">
        <div className="kicker" style={{ marginBottom: 8 }}>Tool 11 · Reference</div>
        <h2 className="tool-title">Glossary</h2>
        <p className="tool-desc">{terms.length} terms. Every acronym and term, translated.</p>
      </div>
      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', alignItems: 'flex-end', marginBottom: 14 }}>
        <div className="field" style={{ minWidth: 280, marginBottom: 0 }}>
          <label>Search</label>
          <input type="text" placeholder="par, sump, alk…" value={q} onChange={e => setQ(e.target.value)}/>
        </div>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          {cats.map(c => (
            <button key={c} onClick={() => setCat(c)}
              style={{ padding: '8px 12px', borderRadius: 8, fontSize: 12, cursor: 'pointer',
                border: `1px solid ${cat === c ? 'var(--cyan)' : 'var(--line)'}`,
                background: cat === c ? 'oklch(0.82 0.14 195 / 0.12)' : 'var(--bg-2)',
                color: cat === c ? 'var(--cyan)' : 'var(--ink-2)' }}>{c}</button>
          ))}
        </div>
      </div>
      <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 10 }}>Showing {f.length} of {terms.length}</div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10 }}>
        {f.map((t, i) => (
          <div key={i} style={{ padding: 14, border: '1px solid var(--line)', borderRadius: 10, background: 'var(--bg-2)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
              <div style={{ fontFamily: 'var(--f-serif)', fontSize: 17 }}>{t.t}</div>
              <div style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{t.c}</div>
            </div>
            <p style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.5 }}>{t.d}</p>
          </div>
        ))}
        {f.length === 0 && <div style={{ padding: 24, color: 'var(--ink-3)' }}>No terms match.</div>}
      </div>
    </div>
  );
}

window.PairCompat = PairCompat;
window.ParamLog = ParamLog;
window.StockingToolLegacy = StockingToolLegacy;
window.PlannerToolExpanded = PlannerToolExpanded;
window.WaterChangeTool = WaterChangeTool;
window.JournalTool = JournalTool;
window.GlossaryTool = GlossaryTool;


// === deploy/pages-tools-extra2.jsx ===
// ReefReads, additional calculators (set 2)

// ---------- Salt Mix Calculator ----------
function SaltMixTool() {
  const [volume, setVolume] = useState(25);
  const [target, setTarget] = useState(1.025);
  const [salt, setSalt] = useState("instantocean");

  const salts = {
    instantocean: { name: "Instant Ocean", rate: 0.55 },
    redseapro: { name: "Red Sea Coral Pro", rate: 0.58 },
    redseablu: { name: "Red Sea Salt (Blue)", rate: 0.55 },
    tropic: { name: "Tropic Marin Pro Reef", rate: 0.6 },
    fritzrpm: { name: "Fritz RPM", rate: 0.56 },
    aquaforestreef: { name: "AquaForest Reef Salt", rate: 0.58 }
  };

  // Approximate cups per gallon to hit target SG
  // Real linear: ~0.5 cup/gal at 1.025
  const factor = (target - 1.000) / 0.025;
  const cups = volume * salts[salt].rate * factor;
  const lbs = cups * 0.5; // ~0.5 lb per cup

  return (
    <>
      <div style={{ marginBottom: 24 }}>
        <div className="kicker">Salt Mix Calculator</div>
        <h1 className="section-title" style={{ fontSize: 36, marginTop: 6 }}>How much salt for a water change?</h1>
        <p className="section-lede" style={{ marginTop: 8 }}>
          Enter your batch volume and target salinity. We'll calculate the salt needed.
        </p>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
        <div className="tool">
          <h3 className="tool-title">Inputs</h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 18, marginTop: 18 }}>
            <label>
              <div className="kicker" style={{ marginBottom: 6 }}>Batch volume (gallons)</div>
              <input type="number" value={volume} onChange={e => setVolume(+e.target.value || 0)} min="1" step="1"
                style={{ width: '100%', padding: 12, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 8 }}/>
            </label>
            <label>
              <div className="kicker" style={{ marginBottom: 6 }}>Target SG</div>
              <input type="number" value={target} onChange={e => setTarget(+e.target.value || 0)} min="1.020" max="1.028" step="0.001"
                style={{ width: '100%', padding: 12, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 8 }}/>
            </label>
            <label>
              <div className="kicker" style={{ marginBottom: 6 }}>Salt brand</div>
              <select value={salt} onChange={e => setSalt(e.target.value)}
                style={{ width: '100%', padding: 12, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 8 }}>
                {Object.entries(salts).map(([k, v]) => <option key={k} value={k}>{v.name}</option>)}
              </select>
            </label>
          </div>
        </div>

        <div className="tool" style={{ background: 'oklch(0.20 0.04 220)' }}>
          <div className="kicker" style={{ color: 'var(--cyan)' }}>You need</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 18 }}>
            <div>
              <div style={{ fontFamily: 'var(--f-serif)', fontSize: 56, color: 'var(--cyan)', lineHeight: 1 }}>{cups.toFixed(1)}</div>
              <div className="kicker" style={{ marginTop: 4 }}>cups of salt</div>
            </div>
            <div>
              <div style={{ fontFamily: 'var(--f-serif)', fontSize: 32 }}>{lbs.toFixed(1)} lbs</div>
              <div className="kicker" style={{ marginTop: 4 }}>approximate weight</div>
            </div>
            <div style={{ borderTop: '1px solid var(--line)', paddingTop: 14, fontSize: 13, color: 'var(--ink-3)' }}>
              Mix at 1.025 SG. Always verify with a refractometer, calibrate monthly with 35ppt calibration fluid.
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

// ---------- Stocking Calculator ----------
function StockingTool() {
  const [tankGal, setTankGal] = useState(40);
  const [stocked, setStocked] = useState([]);
  const [search, setSearch] = useState("");

  const fish = (window.SPECIES || []).filter(s => s.kind === "fish");
  const filtered = fish.filter(f =>
    f.common.toLowerCase().includes(search.toLowerCase()) ||
    f.latin?.toLowerCase().includes(search.toLowerCase())
  ).slice(0, 8);

  const add = (f) => {
    if (stocked.find(s => s.id === f.id)) return;
    setStocked([...stocked, f]);
    setSearch("");
  };
  const remove = (id) => setStocked(stocked.filter(s => s.id !== id));

  // Bioload formula: bioload = sum of (maxSize × temperament_factor)
  // Tank capacity = tankGal × 0.6 (rough)
  const bioloadUsed = stocked.reduce((sum, s) => {
    const tempFactor = s.temperament === "aggressive" ? 1.5 : s.temperament === "semi-aggressive" ? 1.2 : 1;
    return sum + s.maxSize * tempFactor;
  }, 0);
  const capacity = tankGal * 0.6;
  const pct = Math.min(100, (bioloadUsed / capacity) * 100);

  // Min tank check
  const tooSmall = stocked.filter(s => s.minTank > tankGal);

  // Aggression check
  const angels = stocked.filter(s => s.category === "Angel" || s.category === "Dwarf Angel");
  const tangs = stocked.filter(s => s.category === "Tang");
  const conflicts = [];
  if (angels.length > 1) conflicts.push("Multiple angels, usually fight unless tank is 200gal+");
  if (tangs.length > 1) conflicts.push("Multiple tangs, needs 180gal+ for varied genera, never same Zebrasoma");

  return (
    <>
      <div style={{ marginBottom: 24 }}>
        <div className="kicker">Stocking Calculator</div>
        <h1 className="section-title" style={{ fontSize: 36, marginTop: 6 }}>How much can your tank hold?</h1>
        <p className="section-lede" style={{ marginTop: 8 }}>
          Add fish to your virtual stocking list. We'll flag overstocking and aggression risks.
        </p>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
        <div className="tool">
          <label>
            <div className="kicker" style={{ marginBottom: 6 }}>Display tank size (gallons)</div>
            <input type="number" value={tankGal} onChange={e => setTankGal(+e.target.value || 0)}
              style={{ width: '100%', padding: 12, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 8 }}/>
          </label>

          <div style={{ marginTop: 20 }}>
            <div className="kicker" style={{ marginBottom: 6 }}>Add fish</div>
            <input type="text" placeholder="Search..." value={search} onChange={e => setSearch(e.target.value)}
              style={{ width: '100%', padding: 12, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 8 }}/>
          </div>

          {search && (
            <div style={{ marginTop: 8, maxHeight: 240, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 4 }}>
              {filtered.map(f => (
                <button key={f.id} onClick={() => add(f)}
                  style={{ textAlign: 'left', padding: 10, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 6, cursor: 'pointer', fontSize: 13 }}>
                  <strong>{f.common}</strong> · {f.maxSize}" · {f.minTank}gal min
                </button>
              ))}
            </div>
          )}
        </div>

        <div className="tool">
          <div className="kicker">Bioload</div>
          <div style={{ marginTop: 8, marginBottom: 6, fontFamily: 'var(--f-serif)', fontSize: 32 }}>
            {pct.toFixed(0)}% used
          </div>
          <div style={{ height: 12, background: 'var(--bg-2)', borderRadius: 6, overflow: 'hidden' }}>
            <div style={{
              width: `${pct}%`, height: '100%',
              background: pct > 90 ? 'var(--coral)' : pct > 70 ? 'var(--amber)' : 'var(--cyan)',
              transition: 'width 200ms'
            }}/>
          </div>
          <div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 6, fontSize: 13 }}>
            {tooSmall.map(f => (
              <div key={f.id} style={{ color: 'var(--coral)' }}>⚠ {f.common} needs {f.minTank}gal+ minimum</div>
            ))}
            {conflicts.map((c, i) => <div key={i} style={{ color: 'var(--amber)' }}>⚠ {c}</div>)}
            {pct > 90 && <div style={{ color: 'var(--coral)' }}>⚠ Likely overstocked</div>}
            {pct < 70 && tooSmall.length === 0 && conflicts.length === 0 && stocked.length > 0 && (
              <div style={{ color: 'var(--cyan)' }}>✓ Stocking looks reasonable</div>
            )}
          </div>
        </div>
      </div>

      {stocked.length > 0 && (
        <div className="tool" style={{ marginTop: 16 }}>
          <h3 className="tool-title">Your stocking list</h3>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10, marginTop: 14 }}>
            {stocked.map(f => (
              <div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 12, background: 'var(--bg-2)', borderRadius: 8 }}>
                <div style={{ flex: 1 }}>
                  <div style={{ fontSize: 14 }}>{f.common}</div>
                  <div className="kicker" style={{ marginTop: 2 }}>{f.maxSize}" · {f.minTank}gal min · {f.temperament}</div>
                </div>
                <button onClick={() => remove(f.id)} className="btn ghost small">Remove</button>
              </div>
            ))}
          </div>
        </div>
      )}
    </>
  );
}

// ---------- Lighting Calculator (PAR estimator) ----------
function LightingTool() {
  const [fixture, setFixture] = useState("aiprime");
  const [height, setHeight] = useState(8);
  const [intensity, setIntensity] = useState(70);

  const fixtures = {
    aiprime: { name: "AI Prime 16HD", baseAt8: 250 },
    radion: { name: "EcoTech Radion XR15 G6", baseAt8: 380 },
    radion30: { name: "EcoTech Radion XR30 G6", baseAt8: 500 },
    hydra32: { name: "AI Hydra 32HD", baseAt8: 350 },
    reefilumix: { name: "Reefi Lumix", baseAt8: 320 },
    noopsyche: { name: "Noopsyche K7 Pro III", baseAt8: 230 },
    kessil: { name: "Kessil A360X", baseAt8: 280 }
  };

  // PAR drops with inverse square; simplify: PAR(h) = baseAt8 * (8/h)^1.5
  const par = fixtures[fixture].baseAt8 * Math.pow(8 / height, 1.5) * (intensity / 100);

  const zones = [
    { range: [0, 75], label: "Shaded floor", corals: "Mushrooms, ricordea, low-light LPS" },
    { range: [75, 150], label: "Mid-reef", corals: "Most LPS, soft corals, hardy SPS" },
    { range: [150, 250], label: "High light", corals: "Most Acropora, montipora, clams" },
    { range: [250, 500], label: "Top reef", corals: "Demanding SPS, premium acros, clams" }
  ];
  const zone = zones.find(z => par >= z.range[0] && par < z.range[1]) || zones[3];

  return (
    <>
      <div style={{ marginBottom: 24 }}>
        <div className="kicker">Lighting Calculator</div>
        <h1 className="section-title" style={{ fontSize: 36, marginTop: 6 }}>Estimate your PAR.</h1>
        <p className="section-lede" style={{ marginTop: 8 }}>
          Rough PAR estimate by fixture, height, and intensity. Verify with a real meter for SPS.
        </p>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
        <div className="tool">
          <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
            <label>
              <div className="kicker" style={{ marginBottom: 6 }}>Fixture</div>
              <select value={fixture} onChange={e => setFixture(e.target.value)}
                style={{ width: '100%', padding: 12, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 8 }}>
                {Object.entries(fixtures).map(([k, v]) => <option key={k} value={k}>{v.name}</option>)}
              </select>
            </label>
            <label>
              <div className="kicker" style={{ marginBottom: 6 }}>Mounting height (in inches above water)</div>
              <input type="number" value={height} onChange={e => setHeight(+e.target.value || 1)} min="2" max="24"
                style={{ width: '100%', padding: 12, background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 8 }}/>
            </label>
            <label>
              <div className="kicker" style={{ marginBottom: 6 }}>Channel intensity (peak %)</div>
              <input type="range" value={intensity} onChange={e => setIntensity(+e.target.value)} min="20" max="100" step="5"
                style={{ width: '100%' }}/>
              <div style={{ textAlign: 'right', fontFamily: 'var(--f-mono)', fontSize: 12 }}>{intensity}%</div>
            </label>
          </div>
        </div>

        <div className="tool" style={{ background: 'oklch(0.20 0.04 220)' }}>
          <div className="kicker" style={{ color: 'var(--cyan)' }}>Estimated PAR at substrate</div>
          <div style={{ fontFamily: 'var(--f-serif)', fontSize: 64, color: 'var(--cyan)', lineHeight: 1, marginTop: 8 }}>
            {par.toFixed(0)}
          </div>
          <div className="kicker" style={{ marginTop: 4 }}>μmol/m²/s</div>
          <div style={{ marginTop: 22, padding: 14, background: 'var(--bg-2)', borderRadius: 8 }}>
            <div style={{ fontSize: 14, fontFamily: 'var(--f-serif)' }}>{zone.label}</div>
            <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4 }}>{zone.corals}</div>
          </div>
        </div>
      </div>
    </>
  );
}

// ---------- Articles Page ----------
// Generative SVG cover art keyed by article category — gives every card a unique
// visual without needing real photography.
function ArticleCover({ category, seed, height = 180, accent }) {
  const palettes = {
    "Setup":           ["var(--cyan)",   "var(--cyan-deep)", "var(--lime)"],
    "Water Chemistry": ["var(--cyan)",   "var(--violet)",    "var(--cyan-deep)"],
    "Equipment":       ["var(--amber)",  "var(--coral)",     "var(--cyan-deep)"],
    "Daily Care":      ["var(--lime)",   "var(--cyan)",      "var(--amber)"],
    "Livestock":       ["var(--coral)",  "var(--amber)",     "var(--violet)"],
    "Coral":           ["var(--violet)", "var(--coral)",     "var(--cyan)"],
    "Disease":         ["var(--coral)",  "var(--amber)",     "var(--violet)"],
    "Troubleshooting": ["var(--amber)",  "var(--coral)",     "var(--cyan-deep)"],
    "Aquascape":       ["var(--lime)",   "var(--cyan-deep)", "var(--amber)"],
  };
  const p = palettes[category] || palettes["Setup"];
  // deterministic pseudo-random from seed string
  let h = 0;
  for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0;
  const rand = (n) => {
    h = (h * 9301 + 49297) % 233280;
    return (h / 233280) * n;
  };
  // Build a few concentric rings + scattered dots — feels like a depth-chart / data plot
  const rings = Array.from({ length: 5 }, (_, i) => ({
    cx: 30 + rand(60),
    cy: 30 + rand(80),
    r: 40 + i * 28 + rand(20),
    op: 0.06 + i * 0.04,
  }));
  const dots = Array.from({ length: 18 }, () => ({
    cx: rand(400),
    cy: rand(height),
    r: 1 + rand(2.5),
    c: p[Math.floor(rand(3))],
    op: 0.4 + rand(0.5),
  }));
  const lines = Array.from({ length: 3 }, (_, i) => ({
    y: 30 + i * (height / 4) + rand(20),
    op: 0.15 + rand(0.15),
  }));

  return (
    <svg viewBox={`0 0 400 ${height}`} preserveAspectRatio="none"
         style={{ display: 'block', width: '100%', height: height, borderRadius: '12px 12px 0 0' }}>
      <defs>
        <linearGradient id={`bg-${seed}`} x1="0" y1="0" x2="1" y2="1">
          <stop offset="0%" stopColor={p[0]} stopOpacity="0.18" />
          <stop offset="100%" stopColor={p[1]} stopOpacity="0.08" />
        </linearGradient>
        <radialGradient id={`glow-${seed}`} cx="0.7" cy="0.3" r="0.6">
          <stop offset="0%" stopColor={p[0]} stopOpacity="0.35" />
          <stop offset="100%" stopColor={p[0]} stopOpacity="0" />
        </radialGradient>
      </defs>
      <rect width="400" height={height} fill="var(--bg-2)" />
      <rect width="400" height={height} fill={`url(#bg-${seed})`} />
      <rect width="400" height={height} fill={`url(#glow-${seed})`} />
      {/* horizontal data lines */}
      {lines.map((l, i) => (
        <line key={i} x1="0" y1={l.y} x2="400" y2={l.y} stroke={p[2]} strokeOpacity={l.op} strokeWidth="0.5" strokeDasharray="2 4" />
      ))}
      {/* concentric rings */}
      {rings.map((r, i) => (
        <circle key={i} cx={r.cx} cy={r.cy} r={r.r} fill="none"
                stroke={p[i % 3]} strokeOpacity={r.op} strokeWidth="1" />
      ))}
      {/* scattered points */}
      {dots.map((d, i) => (
        <circle key={i} cx={d.cx} cy={d.cy} r={d.r} fill={d.c} opacity={d.op} />
      ))}
      {/* category label etched into corner */}
      <text x="14" y={height - 14} fontFamily="var(--f-mono)" fontSize="9"
            letterSpacing="0.2em" fill="var(--ink-3)" textTransform="uppercase">
        {String(category).toUpperCase()}
      </text>
      {/* corner crosshair */}
      <g stroke={p[0]} strokeWidth="1" opacity="0.5">
        <line x1="380" y1="14" x2="386" y2="14" />
        <line x1="383" y1="11" x2="383" y2="17" />
      </g>
    </svg>
  );
}

function ArticlesPage({ navigate, articleId }) {
  const articles = window.ARTICLES || [];

  if (articleId) {
    const article = articles.find(a => a.id === articleId);
    if (!article) return <div className="tool">Article not found.</div>;
    // related articles — same category, excluding self
    const related = articles.filter(a => a.category === article.category && a.id !== article.id).slice(0, 3);
    // pick a paragraph to pull-quote (the second body section's first sentence)
    const pullQuote = article.body[1] && article.body[1].p
      ? article.body[1].p.split(". ")[0] + "."
      : null;
    return (
      <>
        <div style={{ marginBottom: 20 }}>
          {window.Breadcrumb && <window.Breadcrumb trail={[
            { label: "Home", href: "/" },
            { label: "Field articles", href: "/articles" },
            { label: article.category },
            { label: article.title }
          ]}/>}
        </div>

        {/* Hero: cover art + title block */}
        <div style={{ marginBottom: 32, border: '1px solid var(--line)', borderRadius: 14, overflow: 'hidden', background: 'var(--bg-2)' }}>
          <ArticleCover category={article.category} seed={article.id} height={220} />
          <div style={{ padding: '28px 36px 32px' }}>
            <div className="kicker" style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
              <span style={{ color: 'var(--cyan)' }}>{article.category}</span>
              <span style={{ opacity: 0.4 }}>·</span>
              <span>{article.read} read</span>
              <span style={{ opacity: 0.4 }}>·</span>
              <span>{article.body.length} sections</span>
            </div>
            <h1 className="section-title" style={{ fontSize: 44, marginTop: 10, lineHeight: 1.1 }}>{article.title}</h1>
            <p style={{ marginTop: 14, fontSize: 17, color: 'var(--ink-2)', fontStyle: 'italic', maxWidth: 640, lineHeight: 1.5 }}>
              {article.excerpt}
            </p>
          </div>
        </div>

        {/* Two-column body: TOC sidebar + article */}
        <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: 48, alignItems: 'start' }}>
          {/* TOC */}
          <aside style={{ position: 'sticky', top: 24 }}>
            <div className="kicker" style={{ marginBottom: 12 }}>Contents</div>
            <ol style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
              {article.body.map((b, i) => (
                <li key={i} style={{ display: 'flex', gap: 10, alignItems: 'baseline' }}>
                  <span style={{ fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--ink-3)', minWidth: 18 }}>
                    {String(i + 1).padStart(2, '0')}
                  </span>
                  <span style={{ fontSize: 12.5, color: 'var(--ink-2)', lineHeight: 1.4 }}>{b.h}</span>
                </li>
              ))}
            </ol>
          </aside>

          {/* Body */}
          <article style={{ maxWidth: 680 }}>
            {article.body.map((b, i) => {
              const insertQuote = pullQuote && i === 2;
              return (
                <React.Fragment key={i}>
                  <section style={{ marginBottom: 32 }}>
                    <div style={{ fontFamily: 'var(--f-mono)', fontSize: 11, color: 'var(--cyan)', letterSpacing: '0.14em', marginBottom: 6 }}>
                      §{String(i + 1).padStart(2, '0')}
                    </div>
                    <h4 style={{ fontFamily: 'var(--f-serif)', fontSize: 24, color: 'var(--ink)', margin: '0 0 12px 0', lineHeight: 1.25 }}>{b.h}</h4>
                    <p style={{ fontSize: 15, lineHeight: 1.75, color: 'var(--ink-2)', margin: 0 }}>{b.p}</p>
                  </section>
                  {insertQuote && (
                    <blockquote style={{
                      margin: '36px 0 36px -24px',
                      padding: '24px 28px',
                      borderLeft: '2px solid var(--cyan)',
                      background: 'linear-gradient(90deg, oklch(0.82 0.14 195 / 0.06), transparent)',
                      borderRadius: '0 12px 12px 0',
                    }}>
                      <div style={{ fontFamily: 'var(--f-mono)', fontSize: 10, letterSpacing: '0.2em', color: 'var(--cyan)', marginBottom: 8 }}>
                        ON THE RECORD
                      </div>
                      <p style={{ fontFamily: 'var(--f-serif)', fontSize: 22, fontStyle: 'italic', color: 'var(--ink)', margin: 0, lineHeight: 1.4 }}>
                        "{pullQuote}"
                      </p>
                    </blockquote>
                  )}
                </React.Fragment>
              );
            })}

            {/* End mark */}
            <div style={{ marginTop: 40, paddingTop: 24, borderTop: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 12, color: 'var(--ink-3)', fontFamily: 'var(--f-mono)', fontSize: 11, letterSpacing: '0.2em' }}>
              <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 8px var(--cyan)' }}></span>
              END OF ARTICLE
              <span style={{ flex: 1, height: 1, background: 'var(--line)' }}></span>
            </div>

            {/* Related */}
            {related.length > 0 && (
              <div style={{ marginTop: 32 }}>
                <div className="kicker" style={{ marginBottom: 14 }}>More on {article.category}</div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(' + Math.min(related.length, 3) + ', 1fr)', gap: 12 }}>
                  {related.map(r => (
                    <a key={r.id} href={`/articles/${r.id}`}
                       style={{ display: 'block', textDecoration: 'none', color: 'inherit', border: '1px solid var(--line)', borderRadius: 10, padding: 14, background: 'var(--bg-2)', transition: 'all 150ms' }}
                       onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--cyan)'; }}
                       onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line)'; }}>
                      <div style={{ fontFamily: 'var(--f-mono)', fontSize: 9.5, color: 'var(--ink-3)', letterSpacing: '0.14em', marginBottom: 6 }}>{r.read}</div>
                      <div style={{ fontFamily: 'var(--f-serif)', fontSize: 16, lineHeight: 1.25, color: 'var(--ink)' }}>{r.title}</div>
                    </a>
                  ))}
                </div>
              </div>
            )}
            {window.LastUpdated && <window.LastUpdated override={article.date || article.updated}/>}
          </article>
        </div>
      </>
    );
  }

  const cats = [...new Set(articles.map(a => a.category))];
  const [filter, setFilter] = useState("all");
  const filtered = filter === "all" ? articles : articles.filter(a => a.category === filter);

  // Featured = the first article (or first matching filter)
  const featured = filtered[0];
  const rest = filtered.slice(1);

  // group rest into rows: occasional 1-up "wide" cards for visual rhythm
  return (
    <>
      <div style={{ marginBottom: 28 }}>
        <div className="kicker">Field notes · {articles.length} pieces · updated weekly</div>
        <h1 className="section-title" style={{ fontSize: 52, marginTop: 6 }}>Field notes.</h1>
        <p className="section-lede" style={{ marginTop: 10, maxWidth: 620, fontSize: 16 }}>
          Standalone deep-dives. Topical, opinionated, written for the bench, not the algorithm.
        </p>
      </div>

      {/* Category filter rail */}
      <div style={{ display: 'flex', gap: 6, marginBottom: 28, flexWrap: 'wrap', paddingBottom: 16, borderBottom: '1px solid var(--line)' }}>
        <button onClick={() => setFilter("all")} className={`chip ${filter === "all" ? "active" : ""}`}>
          All <span style={{ opacity: 0.6, marginLeft: 4 }}>{articles.length}</span>
        </button>
        {cats.map(c => {
          const count = articles.filter(a => a.category === c).length;
          return (
            <button key={c} onClick={() => setFilter(c)} className={`chip ${filter === c ? "active" : ""}`}>
              {c} <span style={{ opacity: 0.6, marginLeft: 4 }}>{count}</span>
            </button>
          );
        })}
      </div>

      {/* Featured hero card */}
      {featured && (
        <a href={`/articles/${featured.id}`} style={{ display: 'block', textDecoration: 'none', color: 'inherit', marginBottom: 28 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', gap: 0, border: '1px solid var(--line)', borderRadius: 14, overflow: 'hidden', background: 'var(--bg-2)', transition: 'all 200ms', cursor: 'pointer' }}
               onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--cyan)'; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 20px 40px -20px oklch(0.82 0.14 195 / 0.3)'; }}
               onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line)'; e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'none'; }}>
            <div style={{ position: 'relative' }}>
              <ArticleCover category={featured.category} seed={featured.id} height={280} />
              <div style={{ position: 'absolute', top: 14, left: 14, fontFamily: 'var(--f-mono)', fontSize: 10, letterSpacing: '0.2em', color: 'var(--cyan)', background: 'var(--bg)', border: '1px solid var(--cyan)', padding: '4px 8px', borderRadius: 4 }}>
                FEATURED
              </div>
            </div>
            <div style={{ padding: '32px 36px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
              <div className="kicker" style={{ marginBottom: 12 }}>
                <span style={{ color: 'var(--cyan)' }}>{featured.category}</span>
                <span style={{ opacity: 0.4, margin: '0 8px' }}>·</span>
                {featured.read} read
              </div>
              <div style={{ fontFamily: 'var(--f-serif)', fontSize: 30, lineHeight: 1.2, color: 'var(--ink)', marginBottom: 16 }}>
                {featured.title}
              </div>
              <div style={{ fontSize: 14.5, color: 'var(--ink-2)', lineHeight: 1.65, marginBottom: 20 }}>
                {featured.excerpt}
              </div>
              <div style={{ display: 'flex', gap: 14, fontFamily: 'var(--f-mono)', fontSize: 10.5, letterSpacing: '0.14em', color: 'var(--ink-3)' }}>
                <span>{featured.body.length} SECTIONS</span>
                <span>·</span>
                <span style={{ color: 'var(--cyan)' }}>READ →</span>
              </div>
            </div>
          </div>
        </a>
      )}

      {/* Rest of grid; 3-up with image headers */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        {rest.map((a, idx) => (
          <a key={a.id} href={`/articles/${a.id}`}
             style={{ textDecoration: 'none', color: 'inherit', cursor: 'pointer', display: 'flex', flexDirection: 'column', border: '1px solid var(--line)', borderRadius: 14, background: 'var(--bg-2)', overflow: 'hidden', transition: 'all 200ms' }}
             onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--cyan)'; e.currentTarget.style.transform = 'translateY(-2px)'; }}
             onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line)'; e.currentTarget.style.transform = 'translateY(0)'; }}>
            <ArticleCover category={a.category} seed={a.id} height={140} />
            <div style={{ padding: 18, display: 'flex', flexDirection: 'column', flex: 1 }}>
              <div className="kicker" style={{ marginBottom: 10, display: 'flex', justifyContent: 'space-between' }}>
                <span style={{ color: 'var(--cyan)' }}>{a.category}</span>
                <span>{a.read}</span>
              </div>
              <div style={{ fontFamily: 'var(--f-serif)', fontSize: 20, lineHeight: 1.2, color: 'var(--ink)', marginBottom: 10 }}>{a.title}</div>
              <div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.5, flex: 1 }}>{a.excerpt}</div>
              <div style={{ marginTop: 14, paddingTop: 12, borderTop: '1px solid var(--line)', fontFamily: 'var(--f-mono)', fontSize: 10, letterSpacing: '0.14em', color: 'var(--ink-3)', display: 'flex', justifyContent: 'space-between' }}>
                <span>{a.body.length} SECTIONS</span>
                <span style={{ color: 'var(--cyan)' }}>READ →</span>
              </div>
            </div>
          </a>
        ))}
      </div>
    </>
  );
}

window.SaltMixTool = SaltMixTool;
window.StockingTool = StockingTool;
window.LightingTool = LightingTool;
window.ArticlesPage = ArticlesPage;


// === deploy/pages-tools-protocols.jsx ===
// ReefReads, clinical protocols: Quarantine Timeline + Dosing Scheduler
//
// These are two of the highest-value tools on the site. They take messy,
// often-confused care info and turn it into a day-by-day or per-dose plan.

// ============================================================
// QUARANTINE TIMELINE
// ============================================================
//
// Protocols are simplified, conservative summaries — not veterinary advice.
// We name the drug class generically (e.g. "copper-based ionic copper")
// instead of a specific brand to avoid appearing to endorse a product.

const QT_DISEASES = [
  {
    id: "ich",
    name: "Marine ich (Cryptocaryon irritans)",
    short: "White grains-of-salt spots, flashing, rapid gill rate.",
    protocol: "copper",
    duration: 30,
  },
  {
    id: "velvet",
    name: "Marine velvet (Amyloodinium ocellatum)",
    short: "Dusty 'gold-flecked' coat, heavy breathing, sudden deaths.",
    protocol: "copper-aggressive",
    duration: 30,
  },
  {
    id: "brook",
    name: "Brooklynella (Clownfish disease)",
    short: "Slimy white mucus sheets, lethargy, mostly clownfish.",
    protocol: "formalin",
    duration: 14,
  },
  {
    id: "flukes",
    name: "Monogenean flukes",
    short: "Glass-clear worms on skin/gills; flashing without spots.",
    protocol: "prazi",
    duration: 21,
  },
  {
    id: "uronema",
    name: "Uronema marinum",
    short: "Red sores / streaks, anthias and chromis especially.",
    protocol: "formalin-prazi",
    duration: 21,
  },
  {
    id: "observe",
    name: "Observation-only (no symptoms)",
    short: "Standard prophylactic quarantine for a new acquisition.",
    protocol: "observation",
    duration: 30,
  },
];

const QT_PROTOCOLS = {
  // Copper-based ionic copper at therapeutic levels (~2.0–2.5 ppm).
  // Monitor copper daily with a test kit, replenish on water change.
  copper: (d) => ({
    label: "Therapeutic copper",
    days: [
      { day: 1,  title: "Move fish to QT tank", note: "Bare-bottom, sponge filter, heater. Use display-tank water if possible to reduce stress. Set lights low." },
      { day: 1,  title: "Begin copper ramp-up", note: "Dose 0.25 ppm copper, then ramp by 0.25 ppm per day until 2.0 ppm therapeutic level. Test copper with a kit daily." },
      ...Array.from({ length: 9 }, (_, i) => ({
        day: i + 2,
        title: `Copper day ${i + 2} of 10`,
        note: i === 0 ? "Continue ramp toward 2.0 ppm. Maintain feeding 2x/day." :
              i === 7 ? "Check copper level. Top off evaporation only with RO/DI." :
              "Observe behavior, gill rate. Feed lightly."
      })),
      { day: 11, title: "Mid-treatment water change", note: "20% water change. Re-dose copper to match. Wipe down equipment." },
      ...Array.from({ length: 13 }, (_, i) => ({
        day: i + 12,
        title: `Copper day ${i + 12} of 24`,
        note: i === 5 ? "Halfway through. Symptoms should be gone; if not, extend by 7 days." : "Daily observation. Feed and test."
      })),
      { day: 25, title: "Begin copper wean", note: "Drop copper 0.25 ppm/day with small water changes." },
      { day: 28, title: "Copper-free water change", note: "Large (50%) water change to remove residual copper." },
      { day: 29, title: "Observation day", note: "Watch for relapse." },
      { day: 30, title: "Cleared", note: "If asymptomatic for 14+ days, fish can move to display." }
    ]
  }),
  "copper-aggressive": (d) => ({
    label: "Therapeutic copper (aggressive; velvet protocol)",
    days: [
      { day: 1, title: "EMERGENCY: hospital tank NOW", note: "Velvet kills in 24-72h. Bare-bottom QT, immediate copper at 0.5 ppm, ramp fast (0.5 ppm/day to 2.5 ppm)." },
      { day: 2, title: "Copper at 1.0 ppm", note: "Continue ramp. Monitor for ammonia (sponge filter may not be cycled if rushed)." },
      { day: 3, title: "Copper at 1.5 ppm", note: "Mortalities still possible; keep feeding stress low." },
      { day: 4, title: "Copper at 2.0 ppm therapeutic", note: "Maintain. Test daily." },
      { day: 5, title: "Copper at 2.5 ppm", note: "Velvet requires higher copper than ich." },
      ...Array.from({ length: 19 }, (_, i) => ({
        day: i + 6,
        title: `Treatment day ${i + 6}`,
        note: i === 9 ? "Mid-treatment: 20% water change, re-dose." : "Maintain 2.5 ppm copper. Feed lightly."
      })),
      { day: 25, title: "Begin copper wean", note: "Reduce 0.5 ppm/day over 4 days." },
      { day: 29, title: "Copper-free water change", note: "50% water change." },
      { day: 30, title: "Cleared", note: "Move only if asymptomatic for 14+ days post-copper." }
    ]
  }),
  formalin: (d) => ({
    label: "Formalin bath series",
    days: [
      { day: 1, title: "Move fish to bare QT", note: "Hospital tank, separate from display." },
      ...Array.from({ length: 5 }, (_, i) => ({
        day: i + 1,
        title: `Formalin bath ${i + 1} of 5`,
        note: "45-minute bath at 0.25 ml/gal in a separate container with airstone. Return to fresh clean QT water afterward."
      })),
      ...Array.from({ length: 8 }, (_, i) => ({
        day: i + 6,
        title: `Observation day ${i + 6}`,
        note: i === 4 ? "If symptoms recur, repeat baths days 11-13." : "Watch for slime sheets, lethargy."
      })),
      { day: 14, title: "Cleared", note: "If asymptomatic for 7+ days post-baths." }
    ]
  }),
  prazi: (d) => ({
    label: "Praziquantel (prazi)",
    days: [
      { day: 1, title: "Day 1: dose praziquantel", note: "2.5 mg/L (single dose). Turn off skimmer and UV/ozone; they bind the drug. Carbon out." },
      ...Array.from({ length: 5 }, (_, i) => ({
        day: i + 2,
        title: `Day ${i + 2} of incubation`,
        note: "Drug works on adult flukes. Eggs are unaffected; they hatch on days 4-7. Maintain."
      })),
      { day: 7, title: "30% water change", note: "Remove drug; eggs about to hatch." },
      { day: 8, title: "Day 8: second prazi dose", note: "Kill the newly-hatched flukes before they reach maturity." },
      ...Array.from({ length: 6 }, (_, i) => ({
        day: i + 9,
        title: `Day ${i + 9} of treatment`,
        note: "Watch for further flashing."
      })),
      { day: 15, title: "Carbon back online", note: "Add fresh carbon, restart skimmer/UV." },
      ...Array.from({ length: 5 }, (_, i) => ({
        day: i + 16,
        title: `Day ${i + 16} observation`,
        note: "Final clearance window."
      })),
      { day: 21, title: "Cleared", note: "If no flashing for 10+ days post-prazi." }
    ]
  }),
  "formalin-prazi": (d) => ({
    label: "Formalin baths + praziquantel (Uronema combo)",
    days: [
      { day: 1, title: "Move to bare-bottom QT", note: "Separate from display. Drop temperature to 24-25°C to slow Uronema replication." },
      ...Array.from({ length: 3 }, (_, i) => ({
        day: i + 1,
        title: `Formalin bath ${i + 1} of 3`,
        note: "45-min bath at 0.25 ml/gal in separate container. Aggressive aeration."
      })),
      { day: 4, title: "Praziquantel main tank", note: "Dose 2.5 mg/L in QT. Carbon off. Skimmer off." },
      ...Array.from({ length: 6 }, (_, i) => ({
        day: i + 5,
        title: `Day ${i + 5} of incubation`,
        note: "Maintain. Feed lightly. Watch for further sores."
      })),
      { day: 11, title: "30% water change", note: "Remove drug." },
      ...Array.from({ length: 9 }, (_, i) => ({
        day: i + 12,
        title: `Day ${i + 12} observation`,
        note: i === 0 ? "Optional: second prazi dose if flukes were involved." : "Watch sores and behavior."
      })),
      { day: 21, title: "Cleared", note: "If asymptomatic for 14+ days." }
    ]
  }),
  observation: (d) => ({
    label: "Observation-only quarantine (no symptoms)",
    days: [
      { day: 1, title: "Acclimate to QT", note: "Bare-bottom tank, sponge filter, heater. Drip-acclimate 1-2 hours." },
      ...Array.from({ length: 13 }, (_, i) => ({
        day: i + 2,
        title: `Day ${i + 2} observation`,
        note: i === 0 ? "Begin daily feeding; varied diet, twice/day." :
              i === 5 ? "Halfway through week one. Most diseases present by now." :
              "Daily check: gill rate, flashing, appetite, body condition."
      })),
      { day: 15, title: "Begin prophylactic prazi (optional)", note: "Many keepers do a single 2.5 mg/L prazi dose now to clear any latent flukes. Carbon off." },
      ...Array.from({ length: 6 }, (_, i) => ({
        day: i + 16,
        title: `Day ${i + 16} observation`,
        note: "Watch for delayed symptoms."
      })),
      { day: 22, title: "Carbon back online", note: "If prazi was used. Otherwise N/A." },
      ...Array.from({ length: 7 }, (_, i) => ({
        day: i + 23,
        title: `Day ${i + 23} observation`,
        note: i === 6 ? "Final day. Asymptomatic → ready for display." : "Eat-pee-poop check."
      })),
      { day: 30, title: "Cleared for display", note: "Standard 30-day prophylactic QT complete." }
    ]
  }),
};

function QuarantineTimeline() {
  const SPECIES = window.SPECIES || [];
  const fish = pUM(() => SPECIES.filter(s => s.kind === "fish"), [SPECIES]);
  const [fishId, setFishId] = pUS(fish[0]?.id || "");
  const [diseaseId, setDiseaseId] = pUS("observe");

  const fishObj = SPECIES.find(s => s.id === fishId);
  const disease = QT_DISEASES.find(d => d.id === diseaseId);
  const plan = disease && QT_PROTOCOLS[disease.protocol] ? QT_PROTOCOLS[disease.protocol](disease) : null;

  const startDateKey = `rr-qt-start-${fishId}-${diseaseId}`;
  const [startDate, setStartDate] = pUS(() => {
    try { return localStorage.getItem(startDateKey) || new Date().toISOString().slice(0, 10); }
    catch { return new Date().toISOString().slice(0, 10); }
  });
  pUE(() => { try { localStorage.setItem(startDateKey, startDate); } catch {} }, [startDate, startDateKey]);

  // Completed-day tracking, per (fish, disease, day)
  const completedKey = `rr-qt-done-${fishId}-${diseaseId}`;
  const [done, setDone] = pUS(() => {
    try { return new Set(JSON.parse(localStorage.getItem(completedKey) || "[]")); }
    catch { return new Set(); }
  });
  pUE(() => {
    try { localStorage.setItem(completedKey, JSON.stringify([...done])); } catch {}
  }, [done, completedKey]);

  function toggleDone(day) {
    setDone(s => {
      const n = new Set(s);
      if (n.has(day)) n.delete(day); else n.add(day);
      return n;
    });
  }

  function dayDate(dayNum) {
    const d = new Date(startDate);
    d.setDate(d.getDate() + dayNum - 1);
    return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
  }

  const todayISO = new Date().toISOString().slice(0, 10);
  const startMs = new Date(startDate).getTime();
  const todayMs = new Date(todayISO).getTime();
  const currentDay = Math.max(1, Math.floor((todayMs - startMs) / 86400000) + 1);

  return (
    <div data-screen-label="quarantine">
      <div style={{ marginBottom: 28 }}>
        <div className="kicker">Reef-keeping tool</div>
        <h1 className="section-title" style={{ marginTop: 6 }}>Quarantine timeline.</h1>
        <p className="section-lede" style={{ marginTop: 8, maxWidth: 640 }}>
          Pick a fish and a disease concern. We'll build the day-by-day protocol, copper ramps, water changes, observation windows, so you don't have to keep track of where you are in the treatment.
        </p>
      </div>

      <div className="qt-setup">
        <label className="qt-field">
          <span>Fish</span>
          <select value={fishId} onChange={e => setFishId(e.target.value)}>
            {fish.map(s => <option key={s.id} value={s.id}>{s.common} ({s.latin})</option>)}
          </select>
        </label>
        <label className="qt-field">
          <span>Concern</span>
          <select value={diseaseId} onChange={e => setDiseaseId(e.target.value)}>
            {QT_DISEASES.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
          </select>
        </label>
        <label className="qt-field">
          <span>Day 1 (today by default)</span>
          <input type="date" value={startDate} onChange={e => setStartDate(e.target.value)}/>
        </label>
      </div>

      {disease && (
        <div className="qt-summary">
          <div className="qt-summary-protocol">{plan?.label}</div>
          <div className="qt-summary-duration"><strong>{disease.duration}</strong> days</div>
          <div className="qt-summary-note">{disease.short}</div>
        </div>
      )}

      {plan && (
        <ol className="qt-timeline">
          {plan.days.map((step, i) => {
            const isToday = step.day === currentDay;
            const isPast = step.day < currentDay;
            const isComplete = done.has(step.day);
            return (
              <li key={i} className={`qt-step ${isToday ? "today" : ""} ${isPast ? "past" : ""} ${isComplete ? "done" : ""}`}>
                <button
                  className="qt-check"
                  aria-label={isComplete ? "Mark incomplete" : "Mark complete"}
                  onClick={() => toggleDone(step.day)}
                >
                  {isComplete ? "✓" : ""}
                </button>
                <div className="qt-step-day">
                  <div className="qt-step-n">Day {step.day}</div>
                  <div className="qt-step-date">{dayDate(step.day)}</div>
                </div>
                <div className="qt-step-body">
                  <div className="qt-step-title">{step.title}</div>
                  <div className="qt-step-note">{step.note}</div>
                </div>
              </li>
            );
          })}
        </ol>
      )}

      <div className="qt-disclaimer">
        ⚠ Conservative reference protocols. Not veterinary advice. Severe outbreaks warrant a hospital tank + a copper test kit + an aquatic vet consultation. Always quarantine new fish, every time.
      </div>
    </div>
  );
}

// ============================================================
// DOSING SCHEDULER (2-part)
// ============================================================
//
// Goal: take a measured ALK consumption (and Ca consumption if known) and
// output a per-day dose schedule for a standard 2-part. Different salt
// products require different per-ml potencies — we expose the most popular
// ones as presets.

const TWO_PART_PRESETS = [
  // dKH per ml per gallon, Ca ppm per ml per gallon.
  // Generic balanced 2-part (BRS / Aquaforest / Tropic Marin classic).
  { id: "generic", name: "Generic balanced 2-part (1 ml/gal raises ~0.7 dKH / ~10 ppm Ca)", alkPotency: 0.7, caPotency: 10 },
  { id: "brs", name: "Bulk Reef Supply Pharma 2-part", alkPotency: 0.7, caPotency: 10 },
  { id: "kalk", name: "Kalkwasser (saturated, 2 tsp / gal RO)", alkPotency: 1.4, caPotency: 20 },
  { id: "tropic-marin", name: "Tropic Marin All-For-Reef", alkPotency: 0.5, caPotency: 7 },
  { id: "aquaforest", name: "Aquaforest Component 1+2+3 (component A)", alkPotency: 0.6, caPotency: 8 },
];

function DosingScheduler() {
  const [tank, setTank] = pUS(75);
  const [alkConsumption, setAlkConsumption] = pUS(1.0);  // dKH per day
  const [caConsumption, setCaConsumption] = pUS(15);     // ppm per day (Ca usually scales ~14 ppm per 1 dKH)
  const [presetId, setPresetId] = pUS("generic");
  const [doses, setDoses] = pUS(2);  // doses per day
  const [autoCa, setAutoCa] = pUS(true);

  const preset = TWO_PART_PRESETS.find(p => p.id === presetId) || TWO_PART_PRESETS[0];

  // ml of part 1 (alk) per day. Per-gal potency × gal = per-tank potency for 1 ml.
  // So daily-ml = dailyDeficit / per-tank-potency.
  const alkPerMlPerTank = preset.alkPotency / tank; // dKH per ml in this tank
  const dailyAlkMl = alkPerMlPerTank > 0 ? alkConsumption / alkPerMlPerTank : 0;

  const effectiveCa = autoCa ? alkConsumption * 14 : caConsumption;
  const caPerMlPerTank = preset.caPotency / tank;
  const dailyCaMl = caPerMlPerTank > 0 ? effectiveCa / caPerMlPerTank : 0;

  const perDoseAlk = dailyAlkMl / doses;
  const perDoseCa = dailyCaMl / doses;

  // Suggest dose times across waking hours so doses are spaced.
  const doseTimes = pUM(() => {
    const hours = [9, 13, 17, 21, 1]; // 9am, 1pm, 5pm, 9pm, 1am
    return hours.slice(0, doses);
  }, [doses]);

  return (
    <div data-screen-label="dosing-schedule">
      <div style={{ marginBottom: 28 }}>
        <div className="kicker">Reef-keeping tool</div>
        <h1 className="section-title" style={{ marginTop: 6 }}>Dosing scheduler.</h1>
        <p className="section-lede" style={{ marginTop: 8, maxWidth: 640 }}>
          Enter your daily alkalinity drop (measured over 24 hours with no dosing) and we'll build a 2-part dose schedule that holds your alk and calcium steady. Splitting doses through the day produces smaller swings than a single nightly dump.
        </p>
      </div>

      <div className="ds-grid">
        <div className="ds-inputs">
          <div className="ds-section-label">Tank</div>
          <label className="ds-field">
            <span>Net water volume <em>gallons</em></span>
            <input type="number" min="1" step="1" value={tank} onChange={e => setTank(+e.target.value || 0)}/>
          </label>
          <p className="ds-hint">Subtract rock + sand displacement (rule of thumb: total volume × 0.85).</p>

          <div className="ds-section-label">Consumption</div>
          <label className="ds-field">
            <span>Alkalinity drop / day <em>dKH</em></span>
            <input type="number" min="0" step="0.1" value={alkConsumption} onChange={e => setAlkConsumption(+e.target.value || 0)}/>
          </label>
          <p className="ds-hint">Measure: stop dosing for 24h, test before and after.</p>

          <label className="ds-checkbox">
            <input type="checkbox" checked={autoCa} onChange={e => setAutoCa(e.target.checked)}/>
            <span>Auto-match calcium to alk (14 ppm Ca per 1 dKH alk)</span>
          </label>
          {!autoCa && (
            <label className="ds-field">
              <span>Calcium drop / day <em>ppm</em></span>
              <input type="number" min="0" step="1" value={caConsumption} onChange={e => setCaConsumption(+e.target.value || 0)}/>
            </label>
          )}

          <div className="ds-section-label">Product</div>
          <label className="ds-field">
            <span>2-part preset</span>
            <select value={presetId} onChange={e => setPresetId(e.target.value)}>
              {TWO_PART_PRESETS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </select>
          </label>

          <label className="ds-field">
            <span>Doses per day</span>
            <select value={doses} onChange={e => setDoses(+e.target.value)}>
              {[1, 2, 3, 4, 5].map(n => <option key={n} value={n}>{n} dose{n > 1 ? "s" : ""}</option>)}
            </select>
          </label>
          <p className="ds-hint">More doses = smoother. Dosers usually do 3-4. Manual hands usually do 1-2.</p>
        </div>

        <div className="ds-output">
          <div className="ds-output-hdr">Daily schedule</div>
          <div className="ds-totals">
            <div className="ds-total">
              <div className="ds-total-l">Part 1 (alk)</div>
              <div className="ds-total-v">{dailyAlkMl.toFixed(1)}<em> ml/day</em></div>
            </div>
            <div className="ds-total">
              <div className="ds-total-l">Part 2 (calcium)</div>
              <div className="ds-total-v">{dailyCaMl.toFixed(1)}<em> ml/day</em></div>
            </div>
          </div>

          <div className="ds-schedule">
            {doseTimes.map((h, i) => (
              <div key={i} className="ds-dose-row">
                <div className="ds-dose-time">{(h % 12 || 12)}{h < 12 || h === 24 ? " AM" : " PM"}</div>
                <div className="ds-dose-amt">
                  <div><strong>{perDoseAlk.toFixed(1)} ml</strong> alk</div>
                  <div className="ds-dose-amt-sub">{perDoseCa.toFixed(1)} ml calcium</div>
                </div>
              </div>
            ))}
          </div>

          <div className="ds-notes">
            <p><strong>Important:</strong> alk and calcium go into <em>different parts of the tank</em>. Pick locations on opposite sides of a strong powerhead so they don't precipitate.</p>
            <p>Retest after 7 days. Adjust if you've drifted up or down by 0.5 dKH or 25 ppm Ca.</p>
            <p>Monthly: also test magnesium. It should sit 1280-1380 ppm. Low Mg makes alk and Ca chase each other.</p>
          </div>
        </div>
      </div>
    </div>
  );
}

window.QuarantineTimeline = QuarantineTimeline;
window.DosingScheduler = DosingScheduler;


// === deploy/pages-hubs.jsx ===
// ReefReads. New hub pages, disease library, best-of, donate
// Reuses existing tool components

// ============================================================
// CALCULATORS HUB
// ============================================================
function CalculatorsHub() {
  const [active, setActive] = hUS("stocking");

  const calcs = [
    { id: "stocking", label: "Stocking", icon: "🐠", desc: "How much life can your tank hold?" },
    { id: "waterchange", label: "Water Change", icon: "💧", desc: "Saltwater dilution math." },
    { id: "saltmix", label: "Salt Mix", icon: "🧂", desc: "How much salt for what salinity." },
    { id: "dosing", label: "Dosing", icon: "💉", desc: "Alkalinity & calcium dosing." },
    { id: "lighting", label: "Lighting / PAR", icon: "☀️", desc: "Estimate PAR at depth." },
  ];

  let Tool = null;
  if (active === "stocking") Tool = window.StockingTool;
  else if (active === "waterchange") Tool = window.WaterChangeTool;
  else if (active === "saltmix") Tool = window.SaltMixTool;
  else if (active === "dosing") Tool = window.DosingTool;
  else if (active === "lighting") Tool = window.LightingTool;

  return (
    <div data-screen-label="calculators-hub">
      <div className="hub-hero">
        <div className="hub-hero-bg" style={{ background: 'linear-gradient(135deg, oklch(0.32 0.14 220), oklch(0.45 0.16 195))' }}/>
        <div className="hub-hero-inner">
          <div className="kicker">Reef calculators</div>
          <h1 className="hub-hero-title">The math your tank runs on.</h1>
          <p className="hub-hero-sub">Five working calculators that take the guesswork out of stocking, water changes, salt batches, two-part dosing, and PAR planning.</p>
        </div>
        <div className="hub-hero-glyph">∑</div>
      </div>

      <div className="hub-tabs">
        {calcs.map(c => (
          <button
            key={c.id}
            className={`hub-tab ${active === c.id ? 'on' : ''}`}
            onClick={() => setActive(c.id)}
          >
            <span className="hub-tab-icon">{c.icon}</span>
            <span>
              <div className="hub-tab-label">{c.label}</div>
              <div className="hub-tab-desc">{c.desc}</div>
            </span>
          </button>
        ))}
      </div>

      <div style={{ marginTop: 20 }}>
        {Tool ? <Tool/> : <div>Tool unavailable</div>}
      </div>
    </div>
  );
}

// ============================================================
// COMPATIBILITY HUB
// ============================================================
function CompatibilityHub() {
  const [tab, setTab] = hUS("pair");

  return (
    <div data-screen-label="compatibility-hub">
      <div className="hub-hero">
        <div className="hub-hero-bg" style={{ background: 'linear-gradient(135deg, oklch(0.32 0.14 30), oklch(0.45 0.16 60))' }}/>
        <div className="hub-hero-inner">
          <div className="kicker">Compatibility</div>
          <h1 className="hub-hero-title">Will they get along?</h1>
          <p className="hub-hero-sub">Check a single pairing or scan a stocking list against itself. Both views pull from the same compatibility database.</p>
        </div>
        <div className="hub-hero-glyph">⇄</div>
      </div>

      <div className="hub-tabs" style={{ gridTemplateColumns: '1fr 1fr' }}>
        <button className={`hub-tab ${tab === 'pair' ? 'on' : ''}`} onClick={() => setTab('pair')}>
          <span className="hub-tab-icon">⇆</span>
          <span>
            <div className="hub-tab-label">Pair Checker</div>
            <div className="hub-tab-desc">Two species, one verdict.</div>
          </span>
        </button>
        <button className={`hub-tab ${tab === 'matrix' ? 'on' : ''}`} onClick={() => setTab('matrix')}>
          <span className="hub-tab-icon">▦</span>
          <span>
            <div className="hub-tab-label">Compatibility Matrix</div>
            <div className="hub-tab-desc">Whole-tank scan grid.</div>
          </span>
        </button>
      </div>

      <div style={{ marginTop: 20 }}>
        {tab === 'pair' ? <window.PairCompat/> : <window.CompatTool/>}
      </div>
    </div>
  );
}

// ============================================================
// DISEASE & PEST LIBRARY
// ============================================================
function DiseaseLibrary({ navigate, diseaseId }) {
  const all = window.DISEASES || [];

  if (diseaseId) {
    const d = all.find(x => x.id === diseaseId);
    if (d) return <DiseaseDetail disease={d} navigate={navigate}/>;
  }

  const [filter, setFilter] = hUS("all");
  const [query, setQuery] = hUS("");

  const filtered = all.filter(d => {
    if (filter !== "all" && d.type !== filter) return false;
    if (query && !`${d.common} ${d.latin} ${d.symptoms.join(' ')} ${d.description}`.toLowerCase().includes(query.toLowerCase())) return false;
    return true;
  });

  const byCategory = {};
  filtered.forEach(d => {
    if (!byCategory[d.category]) byCategory[d.category] = [];
    byCategory[d.category].push(d);
  });

  const dangerCounts = {
    extreme: all.filter(d => d.danger === 'extreme').length,
    high: all.filter(d => d.danger === 'high').length,
    moderate: all.filter(d => d.danger === 'moderate').length,
    low: all.filter(d => d.danger === 'low' || d.danger === 'minimal').length,
  };

  return (
    <div data-screen-label="disease-library">
      {/* MEDICAL-DATABASE HERO */}
      <div className="med-hero">
        <div className="med-hero-grid">
          <div>
            <div className="med-eyebrow">
              <span className="med-pulse"/> Reference database · {all.length} entries
            </div>
            <h1 className="med-h1">Disease & Pest<br/>Identification Library</h1>
            <p className="med-sub">A clinical reference for diagnosing what's happening in your tank. Match symptoms, identify pathogens, and find evidence-based treatment protocols. Built for reef-keepers, structured for clarity.</p>
            <div className="med-search">
              <span style={{color:'var(--ink-3)'}}>⌕</span>
              <input
                placeholder="Search symptoms, species, conditions…"
                value={query}
                onChange={e => setQuery(e.target.value)}
              />
            </div>
          </div>
          <div className="med-stats">
            <div className="med-stat extreme">
              <div className="med-stat-num">{dangerCounts.extreme}</div>
              <div className="med-stat-lab">Extreme</div>
            </div>
            <div className="med-stat high">
              <div className="med-stat-num">{dangerCounts.high}</div>
              <div className="med-stat-lab">High danger</div>
            </div>
            <div className="med-stat moderate">
              <div className="med-stat-num">{dangerCounts.moderate}</div>
              <div className="med-stat-lab">Moderate</div>
            </div>
            <div className="med-stat low">
              <div className="med-stat-num">{dangerCounts.low}</div>
              <div className="med-stat-lab">Low / nuisance</div>
            </div>
          </div>
        </div>
      </div>

      {/* TYPE FILTERS */}
      <div className="med-filters">
        {[
          { id: "all", label: "All entries", count: all.length },
          { id: "fish-disease", label: "Fish diseases", count: all.filter(d => d.type === 'fish-disease').length },
          { id: "coral-disease", label: "Coral diseases", count: all.filter(d => d.type === 'coral-disease').length },
          { id: "coral-pest", label: "Coral pests", count: all.filter(d => d.type === 'coral-pest').length },
          { id: "pest", label: "Tank pests", count: all.filter(d => d.type === 'pest').length },
          { id: "algae", label: "Algae & nuisance", count: all.filter(d => d.type === 'algae').length },
        ].map(t => (
          <button
            key={t.id}
            className={`med-filter ${filter === t.id ? 'on' : ''}`}
            onClick={() => setFilter(t.id)}
          >
            <span>{t.label}</span>
            <span className="med-filter-count">{t.count}</span>
          </button>
        ))}
      </div>

      {/* QUICK DIAGNOSIS BANNER */}
      <div className="med-banner">
        <div className="med-banner-icon">⚕</div>
        <div>
          <div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}>Diagnosing a sick fish?</div>
          <div style={{ fontSize: 13, color: 'var(--ink-2)' }}>Use the symptom-based diagnosis tool to narrow down likely causes; it cross-references this database with behavioral indicators.</div>
        </div>
        <a href="/tools/symptoms" className="btn-primary" style={{ flexShrink: 0 }}>Open Diagnosis →</a>
      </div>

      {/* RESULTS BY CATEGORY */}
      {Object.entries(byCategory).map(([cat, items]) => (
        <div key={cat} className="med-category">
          <div className="med-category-head">
            <h3>{cat}</h3>
            <div className="kicker">{items.length} {items.length === 1 ? 'entry' : 'entries'}</div>
          </div>
          <div className="med-grid">
            {items.map(d => (
              <a key={d.id} href={`/diseases/${d.id}`} className={`med-card severity-${d.severity}`}>
                <div className="med-card-head">
                  <div className="med-code">{d.code}</div>
                  <div className={`med-danger danger-${d.danger}`}>{d.danger}</div>
                </div>
                <h4>{d.common}</h4>
                <div className="med-latin">{d.latin}</div>
                <div className="med-quickid">
                  <span className="kicker" style={{fontSize:9}}>Quick ID</span>
                  <p>{d.quickId}</p>
                </div>
                <div className="med-card-foot">
                  <div className="med-severity-bar">
                    {[1,2,3,4,5].map(i => (
                      <span key={i} className={i <= d.severity ? 'on' : ''}/>
                    ))}
                  </div>
                  {d.contagious && <span className="med-tag-contagious">Contagious</span>}
                </div>
              </a>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

function DiseaseDetail({ disease: d, navigate }) {
  return (
    <div data-screen-label={`disease-${d.id}`}>
      {window.Breadcrumb && <window.Breadcrumb trail={[
        { label: "Home", href: "/" },
        { label: "Diseases & pests", href: "/diseases" },
        { label: d.category || d.kind || "Disease" },
        { label: d.common }
      ]}/>}

      <div className="med-detail-hero">
        <div className="med-detail-bg"/>
        <div className="med-detail-inner">
          <div className="med-detail-codes">
            <span className="med-code-large">{d.code}</span>
            <span className={`med-danger danger-${d.danger}`}>Danger · {d.danger}</span>
            {d.contagious && <span className="med-tag-contagious">Contagious</span>}
            <span className="med-cat-tag">{d.category}</span>
          </div>
          <h1 className="med-detail-title">{d.common}</h1>
          <div className="med-detail-latin">{d.latin}</div>
          <p className="med-detail-quickid">{d.quickId}</p>
        </div>
        <div className="med-detail-severity">
          <div className="kicker">Severity</div>
          <div className="med-severity-display">
            <span className="med-severity-num">{d.severity}</span>
            <span className="med-severity-of">/5</span>
          </div>
          <div className="med-severity-bar large">
            {[1,2,3,4,5].map(i => (
              <span key={i} className={i <= d.severity ? 'on' : ''}/>
            ))}
          </div>
        </div>
      </div>

      <div className="med-detail-grid">
        <div className="info-block">
          <h5>Description</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 8 }}>{d.description}</p>
        </div>

        <div className="info-block med-symptoms">
          <h5>Clinical Signs & Symptoms</h5>
          <ul>
            {d.symptoms.map((s, i) => (
              <li key={i}><span className="med-bullet">▸</span>{s}</li>
            ))}
          </ul>
        </div>

        <div className="info-block">
          <h5>Etiology / Causes</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 8 }}>{d.causes}</p>
        </div>

        <div className="info-block med-treatment">
          <h5>Treatment Protocol</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 8 }}>{d.treatment}</p>
        </div>

        <div className="info-block">
          <h5>Prevention</h5>
          <p style={{ fontSize: 14, lineHeight: 1.75, marginTop: 8 }}>{d.prevention}</p>
        </div>

        <div className="info-block">
          <h5>Affects</h5>
          <ul style={{ marginTop: 8 }}>
            {d.affects.map((a, i) => (
              <li key={i}><span className="med-bullet">▸</span>{a}</li>
            ))}
          </ul>
        </div>
      </div>

      <div className="med-disclaimer">
        ⚠ Reef husbandry information for reference only. Severe outbreaks warrant consultation with an aquatic veterinarian or experienced specialist.
      </div>
      {window.LastUpdated && <window.LastUpdated override={d.updated}/>}
    </div>
  );
}

// ============================================================
// BEST-OF (SEO) COLLECTIONS
// ============================================================
function BestOfHub({ navigate, slug }) {
  const all = window.BEST_OF || [];

  if (slug) {
    const collection = all.find(c => c.id === slug);
    if (collection) return <BestOfDetail collection={collection} navigate={navigate}/>;
  }

  const [filter, setFilter] = hUS("All");
  const categories = ["All", ...new Set(all.map(c => c.category))];
  const filtered = filter === "All" ? all : all.filter(c => c.category === filter);

  // Show "featured" as the first collection
  const featured = filtered[0];
  const rest = filtered.slice(1);

  return (
    <div data-screen-label="best-of">
      <div className="hub-hero" style={{marginBottom:22}}>
        <div className="hub-hero-bg" style={{ background: 'linear-gradient(135deg, oklch(0.34 0.16 320), oklch(0.45 0.18 280))' }}/>
        <div className="hub-hero-inner">
          <div className="kicker">Curated lists · updated quarterly</div>
          <h1 className="hub-hero-title">The Best Of ReefReads.</h1>
          <p className="hub-hero-sub">Editorial picks for the questions every reefer Googles: what's the best clownfish, what eats algae, which corals survive a beginner. Honest answers, ranked, no affiliate links.</p>
          <div className="bestof-hero-stats">
            <div><span className="bestof-stat-num">{all.length}</span><span className="bestof-stat-label">Collections</span></div>
            <div><span className="bestof-stat-num">{all.reduce((s,c) => s + (c.items || (c.groups || []).flatMap(g => g.items || [])).length, 0)}</span><span className="bestof-stat-label">Total picks</span></div>
            <div><span className="bestof-stat-num">{new Set(all.flatMap(c => c.items || (c.groups || []).flatMap(g => g.items || []))).size}</span><span className="bestof-stat-label">Unique species</span></div>
          </div>
        </div>
        <div className="hub-hero-glyph">★</div>
      </div>

      {/* Category filter rail */}
      <div className="bestof-filter-rail">
        {categories.map(c => (
          <button
            key={c}
            className={`chip ${filter === c ? 'active' : ''}`}
            onClick={() => setFilter(c)}
          >
            {c}
            <span style={{ opacity: 0.6, marginLeft: 6 }}>
              {c === "All" ? all.length : all.filter(x => x.category === c).length}
            </span>
          </button>
        ))}
      </div>

      {/* Featured large card */}
      {featured && (
        <a href={`/best-of/${featured.id}`} className="bestof-featured">
          <div className="bestof-featured-bg" style={{ background: featured.bg }}/>
          <div className="bestof-featured-glyph">{featured.icon}</div>
          <div className="bestof-featured-content">
            <div className="bestof-featured-tag">EDITOR'S PICK</div>
            <div className="kicker" style={{ color: 'rgba(255,255,255,0.75)', marginTop: 14 }}>{featured.category}</div>
            <h2 className="bestof-featured-h2">{featured.h1}</h2>
            <p className="bestof-featured-blurb">{featured.blurb}</p>
            <div className="bestof-featured-meta">
              <span><span className="bestof-meta-num">{(featured.items || (featured.groups || []).flatMap(g => g.items || [])).length}</span> picks</span>
              <span>·</span>
              <span>{featured.factors.length} criteria</span>
              <span>·</span>
              <span style={{ color: 'white' }}>Read the list →</span>
            </div>
          </div>
        </a>
      )}

      {/* Rest of grid */}
      <div className="bestof-grid">
        {rest.map(c => (
          <a key={c.id} href={`/best-of/${c.id}`} className="bestof-card">
            <div className="bestof-card-bg" style={{ background: c.bg }}/>
            <div className="bestof-card-icon">{c.icon}</div>
            <div className="bestof-card-body">
              <div className="kicker" style={{fontSize:10, color: 'rgba(255,255,255,0.7)'}}>{c.category}</div>
              <h3>{c.title}</h3>
              <p>{c.blurb}</p>
              <div className="bestof-card-meta">
                <span>{(c.items || (c.groups || []).flatMap(g => g.items || [])).length} picks</span>
                <span>→</span>
              </div>
            </div>
          </a>
        ))}
      </div>

      {/* SEO content block; popular questions */}
      <div className="bestof-faq">
        <div className="kicker" style={{ marginBottom: 10 }}>Frequently asked</div>
        <h3 className="bestof-faq-h3">Popular questions, straight answers.</h3>
        <div className="bestof-faq-grid">
          {[
            { q: "What's the easiest saltwater fish to keep?", a: "Captive-bred ocellaris clownfish. Hardy, peaceful, eats anything, and tolerates the parameter swings of a new tank." },
            { q: "What corals can I keep without dosing?", a: "Soft corals and most LPS, zoanthids, mushrooms, GSP, leathers, and Duncans all thrive with regular water changes alone." },
            { q: "How long until I can add fish to a new tank?", a: "4–6 weeks minimum, after a complete nitrogen cycle. Add cleanup crew first, wait another week, then one fish at a time." },
            { q: "What's the smallest tank for a reef?", a: "10 gallons is the practical floor. 20–30 gallons is dramatically more forgiving and lets you keep small clowns, gobies, and softies." },
            { q: "Do clownfish need an anemone?", a: "No. They host readily but are perfectly happy without one. Many host a torch coral or a powerhead in captivity." },
            { q: "What kills the most reef tanks?", a: "Tap water. Followed by tang stress disease, alkalinity swings, and the impatience that comes with watching a tank cycle." },
          ].map((f, i) => (
            <div key={i} className="bestof-faq-item">
              <h4>{f.q}</h4>
              <p>{f.a}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function BestOfDetail({ collection: c, navigate }) {
  const SP = window.SPECIES || [];
  const byId = Object.fromEntries(SP.map(s => [s.id, s]));

  // Build either a flat list (c.items) or a grouped list (c.groups).
  // For each entry, attach a `notes` field if the collection provided
  // a custom "why this pick" override.
  const collectGroup = (idList) =>
    (idList || [])
      .map(id => byId[id])
      .filter(Boolean)
      .map(sp => ({ ...sp, note: c.notes?.[sp.id] }));

  const flatItems = c.items ? collectGroup(c.items) : [];
  const groups = c.groups
    ? c.groups.map(g => ({ name: g.name, blurb: g.blurb, items: collectGroup(g.items) }))
    : [];
  // All picks across the collection, for the "at a glance" comparison.
  const allItems = c.items ? flatItems : groups.flatMap(g => g.items);

  // Generate per-pick reasoning by combining the species data + collection focus.
  // Falls through to `sp.note` (custom override) when provided.
  const reasoningFor = (sp) => {
    if (sp.note) return sp.note;
    const reasons = [];
    if (sp.difficulty === 1) reasons.push("rock-solid beginner difficulty");
    else if (sp.difficulty === 2) reasons.push("forgiving once established");
    else if (sp.difficulty <= 3) reasons.push("rewards careful husbandry");
    if (sp.tags?.includes("beginner")) reasons.push("widely available captive-bred");
    if (sp.tags?.includes("algae-eater")) reasons.push("active grazer");
    if (sp.tags?.includes("peaceful") || sp.temperament === "peaceful") reasons.push("won't pick fights");
    if (sp.minTank <= 30) reasons.push("nano-tank friendly");
    if (sp.reefSafe === "safe") reasons.push("100% reef safe");
    return reasons.slice(0, 3).join(" · ");
  };

  // Re-usable ranked card (extracted so we can render multiple groups).
  const RankedItem = ({ sp, rank, podium }) => {
    const isPodium = podium && rank <= 3;
    const trophyColors = ['oklch(0.82 0.14 75)', 'oklch(0.78 0.04 220)', 'oklch(0.68 0.13 50)'];
    return (
      <div className={`bestof-item ${isPodium ? 'podium' : ''}`}>
        <div className="bestof-rank" style={isPodium ? { color: trophyColors[rank - 1] } : {}}>
          {isPodium && <span className="bestof-medal">{['🥇','🥈','🥉'][rank - 1]}</span>}
          <span className="bestof-rank-num">#{rank}</span>
        </div>
        <div className="bestof-item-media" style={{
          background: `linear-gradient(135deg, oklch(0.3 0.1 ${(rank*40)%360}), oklch(0.45 0.15 ${(rank*40+60)%360}))`
        }}>
          <div className="bestof-item-glyph">
            {sp.kind === 'fish' ? '◐' : sp.kind === 'coral' ? '※' : '◇'}
          </div>
          <div className="bestof-item-media-label">{sp.kind}</div>
        </div>
        <div className="bestof-item-body">
          <div className="kicker">{sp.category}</div>
          <h3>{sp.common}</h3>
          <div className="bestof-item-latin">{sp.latin}</div>
          <p>{sp.short}</p>
          <div className="bestof-why">
            <span className="bestof-why-label">Why it made the list:</span>
            <span>{reasoningFor(sp)}</span>
          </div>
          <div className="bestof-item-meta">
            <span className="bestof-stat">
              <span className="kicker" style={{fontSize:8.5}}>DIFF</span>
              <span className="diff" style={{marginLeft:4}}>
                {[1,2,3,4,5].map(n => <span key={n} className={`diff-dot ${n <= sp.difficulty ? 'on' : ''}`}/>)}
              </span>
            </span>
            {sp.minTank && (
              <span className="bestof-stat">
                <span className="kicker" style={{fontSize:8.5}}>MIN TANK</span>
                <span>{sp.minTank}gal</span>
              </span>
            )}
            {sp.maxSize && (
              <span className="bestof-stat">
                <span className="kicker" style={{fontSize:8.5}}>MAX</span>
                <span>{sp.maxSize}"</span>
              </span>
            )}
            {sp.temperament && (
              <span className="bestof-stat">
                <span className="kicker" style={{fontSize:8.5}}>TEMP</span>
                <span style={{textTransform:'capitalize'}}>{sp.temperament.split('-')[0]}</span>
              </span>
            )}
          </div>
        </div>
        <a href={`/species/${sp.id}`} className="bestof-item-cta">
          <span>View profile</span>
          <span>→</span>
        </a>
      </div>
    );
  };

  return (
    <div data-screen-label={`best-of-${c.id}`}>
      {window.Breadcrumb && <window.Breadcrumb trail={[
        { label: "Home", href: "/" },
        { label: "Best of", href: "/best-of" },
        { label: c.title }
      ]}/>}

      <div className="bestof-detail-hero">
        <div className="bestof-detail-bg" style={{ background: c.bg }}/>
        <div className="bestof-detail-glyph">{c.icon}</div>
        <div className="bestof-detail-inner">
          <div className="kicker" style={{color:'rgba(255,255,255,0.85)'}}>{c.category} · {allItems.length} picks · {c.factors.length} criteria</div>
          <h1 className="bestof-detail-h1">{c.h1}</h1>
          <p className="bestof-detail-sub">{c.blurb}</p>
          <div className="bestof-keywords">
            {c.keywords.split(',').slice(0, 4).map((k, i) => (
              <span key={i} className="bestof-keyword">{k.trim()}</span>
            ))}
          </div>
        </div>
      </div>

      <div className="bestof-detail-body">
        <div className="bestof-intro">
          <div className="kicker" style={{ marginBottom: 8 }}>Editor's note</div>
          <p>{c.intro}</p>
        </div>

        <div className="bestof-twocol">
          <div className="bestof-factors">
            <div className="kicker">What we considered</div>
            <ul>
              {c.factors.map((f, i) => <li key={i}>{f}</li>)}
            </ul>
          </div>

          <div className="bestof-compare">
            <div className="kicker">At a glance</div>
            <table className="bestof-table">
              <thead>
                <tr>
                  <th>Pick</th>
                  <th>Difficulty</th>
                  <th>Min tank</th>
                  <th>Max size</th>
                </tr>
              </thead>
              <tbody>
                {allItems.map((sp, i) => (
                  <tr key={sp.id}>
                    <td>
                      <span style={{ color: 'var(--cyan)', marginRight: 8, fontFamily: 'var(--f-mono)', fontSize: 11 }}>#{i + 1}</span>
                      {sp.common}
                    </td>
                    <td>
                      <span className="diff">
                        {[1,2,3,4,5].map(n => <span key={n} className={`diff-dot ${n <= sp.difficulty ? 'on' : ''}`}/>)}
                      </span>
                    </td>
                    <td>{sp.minTank ? sp.minTank + "gal" : "—"}</td>
                    <td>{sp.maxSize ? sp.maxSize + "\"" : "—"}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        {/* RANKED PICKS, either flat or grouped */}
        {flatItems.length > 0 && (
          <>
            <h2 className="bestof-list-h2">The Ranked Picks</h2>
            <div className="bestof-list">
              {flatItems.map((sp, i) => <RankedItem key={sp.id} sp={sp} rank={i + 1} podium/>)}
            </div>
          </>
        )}

        {groups.map((g, gi) => (
          <div key={gi} className="bestof-group">
            <h2 className="bestof-list-h2" style={{ marginTop: gi === 0 ? 0 : 36 }}>{g.name}</h2>
            {g.blurb && <p className="section-lede" style={{ marginTop: 6, marginBottom: 18, maxWidth: 720 }}>{g.blurb}</p>}
            <div className="bestof-list">
              {g.items.map((sp, i) => <RankedItem key={sp.id} sp={sp} rank={i + 1} podium={false}/>)}
            </div>
          </div>
        ))}

        {/* Verdict, only when we can identify a single top pick */}
        {flatItems.length > 0 && (
          <div className="bestof-verdict">
            <div className="kicker" style={{ color: 'var(--cyan)', marginBottom: 10 }}>The verdict</div>
            <h3>Our top pick: {flatItems[0].common}</h3>
            <p>If you can only buy one, this is it. <em>{flatItems[0].short}</em></p>
          </div>
        )}

        {/* Related collections */}
        <div className="bestof-related">
          <div className="kicker">Related collections</div>
          <div style={{display:'flex', gap:10, flexWrap:'wrap', marginTop:10}}>
            {(window.BEST_OF || []).filter(x => x.id !== c.id).slice(0, 5).map(x => (
              <a key={x.id} href={`/best-of/${x.id}`} className="bestof-pill">
                <span style={{fontSize:14}}>{x.icon}</span>
                <span>{x.title}</span>
              </a>
            ))}
          </div>
        </div>

        {window.LastUpdated && <window.LastUpdated override={c.updated}/>}
      </div>
    </div>
  );
}
// ============================================================
// DONATE PAGE
// ============================================================
function DonatePage() {
  const [tier, setTier] = hUS(20);
  const [custom, setCustom] = hUS("");
  const [monthlyTier, setMonthlyTier] = hUS(8);
  const tiers = [5, 10, 20, 50, 100];
  const monthlyTiers = [3, 8, 15, 25];
  // Ko-fi handles Stripe + PayPal + Apple/Google Pay automatically.
  const KOFI = "https://ko-fi.com/reefreads";
  const oneTimeAmount = custom || tier;
  // ko-fi.com/reefreads supports ?amount= prefill on the tip URL.
  const oneTimeUrl = `${KOFI}/?amount=${oneTimeAmount}`;
  const monthlyUrl = `${KOFI}/tiers`;

  return (
    <div data-screen-label="donate">
      <div className="donate-hero">
        <div className="donate-hero-art">
          <div className="donate-coral c1"/>
          <div className="donate-coral c2"/>
          <div className="donate-coral c3"/>
          <div className="donate-bubble b1"/>
          <div className="donate-bubble b2"/>
          <div className="donate-bubble b3"/>
          <div className="donate-bubble b4"/>
          <div className="donate-fish"/>
        </div>
        <div className="donate-hero-inner">
          <div className="kicker">Keep ReefReads independent</div>
          <h1 className="donate-h1">Help us keep the lights on.<br/>(Literally; reefs need PAR.)</h1>
          <p className="donate-sub">ReefReads is built and maintained by reef-keepers, not by an aquarium retailer. No affiliate links, no sponsored species pages, no pop-ups. If the database has helped your tank, consider chipping in.</p>
        </div>
      </div>

      <div className="donate-grid">
        <div className="donate-card">
          <h3>One-time tip</h3>
          <p style={{fontSize:13, color:'var(--ink-2)', marginTop:6, marginBottom:18}}>Pay for a coffee, an Aiptasia-X, or a month of hosting.</p>
          <div className="donate-tiers">
            {tiers.map(t => (
              <button
                key={t}
                className={`donate-tier ${tier === t && !custom ? 'on' : ''}`}
                onClick={() => { setTier(t); setCustom(""); }}
              >
                ${t}
              </button>
            ))}
          </div>
          <div className="donate-custom">
            <span>$</span>
            <input
              type="number"
              placeholder="Custom amount"
              value={custom}
              onChange={e => { setCustom(e.target.value); setTier(0); }}
            />
          </div>
          <a
            className="btn-primary donate-cta"
            href={oneTimeUrl}
            target="_blank"
            rel="noopener"
            style={{display:'inline-block', textAlign:'center', textDecoration:'none'}}
          >
            Tip ${oneTimeAmount} on Ko-fi →
          </a>
          <p style={{fontSize:11, color:'var(--ink-3)', marginTop:10, textAlign:'center', fontFamily:'var(--f-mono)', letterSpacing:'0.04em'}}>
            Pay with card · PayPal · Apple Pay
          </p>
        </div>

        <div className="donate-card monthly">
          <div className="donate-badge">Recommended</div>
          <h3>Monthly supporter</h3>
          <p style={{fontSize:13, color:'var(--ink-2)', marginTop:6, marginBottom:18}}>Steady support keeps the database growing. Cancel anytime.</p>
          <div className="donate-tiers">
            {monthlyTiers.map(t => (
              <button
                key={t}
                className={`donate-tier ${monthlyTier === t ? 'on' : ''}`}
                onClick={() => setMonthlyTier(t)}
              >
                ${t}/mo
              </button>
            ))}
          </div>
          <ul className="donate-perks">
            <li>✓ Supporter badge in journal</li>
            <li>✓ Early access to new pages</li>
            <li>✓ Ad-free forever (still, even more so)</li>
            <li>✓ Vote on next species batch</li>
          </ul>
          <a
            className="btn-primary donate-cta"
            href={monthlyUrl}
            target="_blank"
            rel="noopener"
            style={{display:'inline-block', textAlign:'center', textDecoration:'none'}}
          >
            Support ${monthlyTier}/mo on Ko-fi →
          </a>
        </div>

        <div className="donate-card alt">
          <h3>Other ways to help</h3>
          <div className="donate-other">
            <div className="donate-other-item">
              <div className="donate-other-icon">✎</div>
              <div>
                <h4>Submit a correction</h4>
                <p>Spotted a wrong Latin name or out-of-date treatment protocol? Tell us.</p>
                <a href="mailto:support@reefreads.com?subject=Correction%20%E2%80%94%20ReefReads" style={{color:'var(--cyan)', fontSize:13}}>Email support@reefreads.com →</a>
              </div>
            </div>
            <div className="donate-other-item">
              <div className="donate-other-icon">📷</div>
              <div>
                <h4>Contribute a photo</h4>
                <p>Got a great shot of one of our species? We credit every contributor.</p>
                <a href="mailto:support@reefreads.com?subject=Photo%20submission%20%E2%80%94%20ReefReads" style={{color:'var(--cyan)', fontSize:13}}>Email support@reefreads.com →</a>
              </div>
            </div>
            <div className="donate-other-item">
              <div className="donate-other-icon">★</div>
              <div>
                <h4>Share with a reefer</h4>
                <p>Word of mouth is how we grew. Send a friend a useful page.</p>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

window.CalculatorsHub = CalculatorsHub;
window.CompatibilityHub = CompatibilityHub;
window.DiseaseLibrary = DiseaseLibrary;
window.BestOfHub = BestOfHub;
window.DonatePage = DonatePage;


// === deploy/pages-journal.jsx ===
// ReefReads. Tank Journal (parameter log)
// Stores entries in localStorage under the key `rr-tank-journal-v1`.
// Future: swap STORAGE.list/save/clear for a Cloudflare Workers fetch when
// sync is needed. See deploy/JOURNAL_SYNC.md.

const TJ_KEY = "rr-tank-journal-v1";

// --- Parameter definitions ---
// `target` is the typical reef-tank range; the chart shades this band.
// `precision` controls input step and decimal display.
const TJ_PARAMS = [
  { id: "ph",      label: "pH",          unit: "",       target: [8.1, 8.4],   range: [7.8, 8.6],   step: 0.01, color: "var(--cyan)"  },
  { id: "alk",     label: "Alkalinity",  unit: "dKH",    target: [8.0, 9.5],   range: [6, 12],      step: 0.1,  color: "var(--lime)"  },
  { id: "ca",      label: "Calcium",     unit: "ppm",    target: [400, 450],   range: [350, 500],   step: 1,    color: "var(--amber)" },
  { id: "mg",      label: "Magnesium",   unit: "ppm",    target: [1280, 1380], range: [1100, 1500], step: 5,    color: "var(--violet)"},
  { id: "no3",     label: "Nitrate",     unit: "ppm",    target: [1, 10],      range: [0, 40],      step: 0.5,  color: "var(--coral)" },
  { id: "po4",     label: "Phosphate",   unit: "ppm",    target: [0.03, 0.10], range: [0, 0.5],     step: 0.01, color: "oklch(0.78 0.14 280)" },
  { id: "temp",    label: "Temperature", unit: "°C",target: [25, 26],     range: [23, 28],     step: 0.1,  color: "oklch(0.85 0.14 50)" },
  { id: "sg",      label: "Salinity",    unit: "SG",     target: [1.025, 1.026],range: [1.020, 1.030],step: 0.001, color: "oklch(0.80 0.12 195)" },
];

const TJ_PARAM_BY_ID = Object.fromEntries(TJ_PARAMS.map(p => [p.id, p]));

function tjLoad() {
  try { return JSON.parse(localStorage.getItem(TJ_KEY) || "[]"); }
  catch { return []; }
}
function tjSave(entries) {
  localStorage.setItem(TJ_KEY, JSON.stringify(entries));
}
function tjFmt(p, v) {
  if (v == null || v === "") return "—";
  const decimals = p.step >= 1 ? 0 : p.step >= 0.1 ? 1 : p.step >= 0.01 ? 2 : 3;
  return Number(v).toFixed(decimals);
}
function tjISO(d = new Date()) {
  const z = d.getTimezoneOffset();
  const local = new Date(d.getTime() - z * 60000);
  return local.toISOString().slice(0, 10);
}

// --- The page ---
function JournalPage() {
  const [entries, setEntries] = tjUS(() => tjLoad());
  const [activeParam, setActiveParam] = tjUS("alk");
  const [range, setRange] = tjUS("30d"); // 7d | 30d | 90d | all
  const [tab, setTab] = tjUS("chart"); // chart | log

  tjUE(() => { tjSave(entries); }, [entries]);

  const sorted = tjUM(() => [...entries].sort((a, b) => a.date.localeCompare(b.date)), [entries]);

  const filtered = tjUM(() => {
    if (range === "all" || sorted.length === 0) return sorted;
    const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - days);
    const cutoffISO = tjISO(cutoff);
    return sorted.filter(e => e.date >= cutoffISO);
  }, [sorted, range]);

  function upsertEntry(entry) {
    setEntries(es => {
      const others = es.filter(e => e.date !== entry.date);
      return [...others, entry];
    });
  }
  function removeEntry(date) {
    setEntries(es => es.filter(e => e.date !== date));
  }
  function exportJSON() {
    const blob = new Blob([JSON.stringify(entries, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `reefreads-journal-${tjISO()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }
  function importJSON(file) {
    const r = new FileReader();
    r.onload = () => {
      try {
        const data = JSON.parse(r.result);
        if (!Array.isArray(data)) throw new Error("not an array");
        // merge by date
        const merged = [...entries];
        for (const inc of data) {
          if (!inc.date) continue;
          const i = merged.findIndex(e => e.date === inc.date);
          if (i >= 0) merged[i] = { ...merged[i], ...inc };
          else merged.push(inc);
        }
        setEntries(merged);
      } catch (e) {
        alert("That file doesn't look like a ReefReads journal export.");
      }
    };
    r.readAsText(file);
  }
  function clearAll() {
    if (!confirm("Delete all journal entries? This can't be undone.")) return;
    setEntries([]);
  }

  // Latest reading per param
  const latest = tjUM(() => {
    const out = {};
    for (const p of TJ_PARAMS) {
      for (let i = sorted.length - 1; i >= 0; i--) {
        const v = sorted[i][p.id];
        if (v != null && v !== "") { out[p.id] = { value: v, date: sorted[i].date }; break; }
      }
    }
    return out;
  }, [sorted]);

  return (
    <div data-screen-label="journal">
      <div style={{ marginBottom: 28 }}>
        <div className="kicker">Saved in your browser · zero accounts</div>
        <h1 className="section-title" style={{ marginTop: 6 }}>Tank Journal.</h1>
        <p className="section-lede" style={{ marginTop: 8, maxWidth: 640 }}>
          Log water tests as you do them. Watch the trends, not the single readings. Drift in alkalinity is what kills SPS, not what one Salifert says on a Tuesday.
        </p>
      </div>

      {/* Latest readings strip */}
      <div className="tj-latest">
        {TJ_PARAMS.map(p => {
          const l = latest[p.id];
          const within = l && Number(l.value) >= p.target[0] && Number(l.value) <= p.target[1];
          return (
            <button
              key={p.id}
              className={`tj-latest-cell ${activeParam === p.id ? 'on' : ''}`}
              onClick={() => setActiveParam(p.id)}
              style={{ '--p': p.color }}
            >
              <div className="tj-latest-label">{p.label}</div>
              <div className="tj-latest-value">
                {l ? tjFmt(p, l.value) : '—'}
                <span className="tj-latest-unit">{p.unit}</span>
              </div>
              <div className={`tj-latest-status ${l ? (within ? 'ok' : 'off') : 'none'}`}>
                {l ? (within ? 'in range' : 'out of range') : 'no data'}
                {l && <span style={{opacity:0.55, marginLeft:6}}>{l.date.slice(5)}</span>}
              </div>
            </button>
          );
        })}
      </div>

      <div className="tj-grid">
        {/* Left: chart + tabs */}
        <div className="tj-main">
          <div className="tj-toolbar">
            <div className="tj-tabs">
              <button className={tab === 'chart' ? 'on' : ''} onClick={() => setTab('chart')}>Chart</button>
              <button className={tab === 'log' ? 'on' : ''} onClick={() => setTab('log')}>Log ({entries.length})</button>
            </div>
            {tab === 'chart' && (
              <div className="tj-range">
                {[['7d','7d'],['30d','30d'],['90d','90d'],['all','All']].map(([v,l]) => (
                  <button key={v} className={range===v?'on':''} onClick={() => setRange(v)}>{l}</button>
                ))}
              </div>
            )}
          </div>

          {tab === 'chart' ? (
            <Chart entries={filtered} param={TJ_PARAM_BY_ID[activeParam]}/>
          ) : (
            <LogTable entries={sorted} onDelete={removeEntry}/>
          )}
        </div>

        {/* Right: add-entry form + actions */}
        <div className="tj-side">
          <AddEntry onSubmit={upsertEntry} existing={entries}/>
          <div className="tj-actions">
            <h4 style={{fontFamily:'var(--f-serif)', fontSize:20, fontWeight:400, marginBottom:14}}>Your data</h4>
            <button className="tj-action-btn" onClick={exportJSON}>
              <span>↓</span> Export JSON
            </button>
            <label className="tj-action-btn">
              <span>↑</span> Import JSON
              <input type="file" accept="application/json" hidden onChange={e => e.target.files[0] && importJSON(e.target.files[0])}/>
            </label>
            <button className="tj-action-btn danger" onClick={clearAll} disabled={!entries.length}>
              <span>×</span> Clear all
            </button>
            <p className="tj-action-note">
              Entries live in this browser only. No account, nothing sent anywhere. Export to keep a backup.
            </p>
            <p className="tj-action-note">
              Want sync across devices? See <code>JOURNAL_SYNC.md</code> in the project for the Cloudflare Workers + D1 path.
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}

// --- Add entry form ---
function AddEntry({ onSubmit, existing }) {
  const [date, setDate] = tjUS(tjISO());
  const [values, setValues] = tjUS({});

  // Pre-fill the form if editing an existing date
  tjUE(() => {
    const match = existing.find(e => e.date === date);
    if (match) {
      const { date: _d, note: _n, ...rest } = match;
      setValues({ ...rest, note: match.note || "" });
    } else {
      setValues({});
    }
  }, [date, existing]);

  function setField(id, v) { setValues(s => ({ ...s, [id]: v })); }
  function submit(e) {
    e.preventDefault();
    const entry = { date };
    let count = 0;
    for (const p of TJ_PARAMS) {
      const v = values[p.id];
      if (v != null && v !== "") { entry[p.id] = Number(v); count++; }
    }
    if (values.note) entry.note = values.note;
    if (!count && !values.note) return;
    onSubmit(entry);
    // Reset for next session, keep today's date
    setValues({});
  }
  return (
    <form className="tj-add" onSubmit={submit}>
      <h4 style={{fontFamily:'var(--f-serif)', fontSize:20, fontWeight:400, marginBottom:14}}>Log a reading</h4>
      <label className="tj-field tj-field-date">
        <span>Date</span>
        <input type="date" value={date} onChange={e => setDate(e.target.value)} max={tjISO()}/>
      </label>
      <div className="tj-fields">
        {TJ_PARAMS.map(p => (
          <label key={p.id} className="tj-field">
            <span>{p.label}<em>{p.unit}</em></span>
            <input
              type="number"
              step={p.step}
              placeholder={`${p.target[0]}–${p.target[1]}`}
              value={values[p.id] ?? ""}
              onChange={e => setField(p.id, e.target.value)}
            />
          </label>
        ))}
      </div>
      <label className="tj-field tj-field-note">
        <span>Note</span>
        <input type="text" placeholder="Water change, dosed Mg, new fish…" value={values.note || ""} onChange={e => setField('note', e.target.value)}/>
      </label>
      <button type="submit" className="btn-primary tj-submit">
        {existing.some(e => e.date === date) ? 'Update entry' : 'Save entry'}
      </button>
    </form>
  );
}

// --- Chart ---
function Chart({ entries, param }) {
  const W = 900, H = 360;
  const PAD = { l: 56, r: 24, t: 24, b: 36 };
  const data = tjUM(() => entries
    .filter(e => e[param.id] != null && e[param.id] !== "")
    .map(e => ({ date: e.date, v: Number(e[param.id]) })),
    [entries, param.id]);

  if (data.length === 0) {
    return (
      <div className="tj-chart-empty">
        <div style={{fontFamily:'var(--f-serif)', fontSize:32}}>—</div>
        <p>No <strong>{param.label.toLowerCase()}</strong> readings yet. Log one on the right.</p>
      </div>
    );
  }

  // Domain
  const first = new Date(data[0].date).getTime();
  const last = new Date(data[data.length - 1].date).getTime();
  const xSpan = Math.max(1, last - first);
  const allV = data.map(d => d.v);
  const vMinRaw = Math.min(...allV, param.target[0]);
  const vMaxRaw = Math.max(...allV, param.target[1]);
  const pad = (vMaxRaw - vMinRaw) * 0.15 || param.step * 5;
  const vMin = vMinRaw - pad;
  const vMax = vMaxRaw + pad;

  const x = i => {
    if (data.length === 1) return PAD.l + (W - PAD.l - PAD.r) / 2;
    const t = (new Date(data[i].date).getTime() - first) / xSpan;
    return PAD.l + t * (W - PAD.l - PAD.r);
  };
  const y = v => PAD.t + (1 - (v - vMin) / (vMax - vMin)) * (H - PAD.t - PAD.b);

  const path = data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${x(i).toFixed(1)} ${y(d.v).toFixed(1)}`).join(' ');

  // Target band rect
  const yTopTarget = y(param.target[1]);
  const yBotTarget = y(param.target[0]);

  // Y ticks (5)
  const yTicks = [];
  for (let i = 0; i <= 4; i++) {
    const v = vMin + ((vMax - vMin) * i) / 4;
    yTicks.push(v);
  }

  // X ticks — first, last, and ~3 in between if room
  const xTicks = [];
  if (data.length <= 6) {
    data.forEach((d, i) => xTicks.push({ i, label: d.date.slice(5) }));
  } else {
    const step = Math.ceil(data.length / 5);
    for (let i = 0; i < data.length; i += step) xTicks.push({ i, label: data[i].date.slice(5) });
    if (xTicks[xTicks.length - 1].i !== data.length - 1) xTicks.push({ i: data.length - 1, label: data[data.length - 1].date.slice(5) });
  }

  // Decimal precision for tick labels
  const dec = param.step >= 1 ? 0 : param.step >= 0.1 ? 1 : param.step >= 0.01 ? 2 : 3;

  return (
    <div className="tj-chart-wrap">
      <div className="tj-chart-hdr">
        <div>
          <div className="tj-chart-title">{param.label}<span className="tj-chart-unit"> ({param.unit || 'unitless'})</span></div>
          <div className="tj-chart-sub">Target: {param.target[0]}–{param.target[1]} {param.unit}</div>
        </div>
        <div className="tj-chart-counts">
          {data.length} reading{data.length !== 1 ? 's' : ''}
        </div>
      </div>
      <svg className="tj-chart" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
        {/* Target band */}
        <rect
          x={PAD.l} y={yTopTarget}
          width={W - PAD.l - PAD.r}
          height={Math.max(0, yBotTarget - yTopTarget)}
          fill={param.color}
          opacity="0.10"
        />
        <line x1={PAD.l} x2={W - PAD.r} y1={yTopTarget} y2={yTopTarget} stroke={param.color} strokeWidth="1" strokeDasharray="3 3" opacity="0.45"/>
        <line x1={PAD.l} x2={W - PAD.r} y1={yBotTarget} y2={yBotTarget} stroke={param.color} strokeWidth="1" strokeDasharray="3 3" opacity="0.45"/>

        {/* Y grid + labels */}
        {yTicks.map((v, i) => (
          <g key={i}>
            <line x1={PAD.l} x2={W - PAD.r} y1={y(v)} y2={y(v)} stroke="var(--line)" strokeWidth="0.5" opacity="0.5"/>
            <text x={PAD.l - 8} y={y(v)} textAnchor="end" dominantBaseline="middle" className="tj-axis">{v.toFixed(dec)}</text>
          </g>
        ))}

        {/* X labels */}
        {xTicks.map((t, i) => (
          <text key={i} x={x(t.i)} y={H - PAD.b + 18} textAnchor="middle" className="tj-axis">{t.label}</text>
        ))}

        {/* Line */}
        <path d={path} stroke={param.color} strokeWidth="2" fill="none" strokeLinejoin="round" strokeLinecap="round"/>

        {/* Points */}
        {data.map((d, i) => {
          const inRange = d.v >= param.target[0] && d.v <= param.target[1];
          return (
            <g key={i}>
              <circle cx={x(i)} cy={y(d.v)} r="5" fill={inRange ? param.color : "var(--coral)"} stroke="var(--bg)" strokeWidth="2"/>
              <title>{d.date} · {tjFmt(param, d.v)} {param.unit}</title>
            </g>
          );
        })}
      </svg>
    </div>
  );
}

// --- Log table ---
function LogTable({ entries, onDelete }) {
  if (entries.length === 0) {
    return (
      <div className="tj-chart-empty">
        <div style={{fontFamily:'var(--f-serif)', fontSize:32}}>—</div>
        <p>No entries yet. Log your first reading on the right.</p>
      </div>
    );
  }
  const rows = [...entries].reverse();
  return (
    <div className="tj-table-wrap">
      <table className="tj-table">
        <thead>
          <tr>
            <th>Date</th>
            {TJ_PARAMS.map(p => <th key={p.id}>{p.label}<em>{p.unit}</em></th>)}
            <th>Note</th>
            <th aria-label="actions"/>
          </tr>
        </thead>
        <tbody>
          {rows.map(e => (
            <tr key={e.date}>
              <td className="tj-table-date">{e.date}</td>
              {TJ_PARAMS.map(p => {
                const v = e[p.id];
                const has = v != null && v !== "";
                const within = has && v >= p.target[0] && v <= p.target[1];
                return (
                  <td key={p.id} className={`tj-table-v ${has ? (within ? 'ok' : 'off') : 'empty'}`}>
                    {has ? tjFmt(p, v) : '—'}
                  </td>
                );
              })}
              <td className="tj-table-note">{e.note || ''}</td>
              <td className="tj-table-actions">
                <button onClick={() => onDelete(e.date)} aria-label={`Delete ${e.date}`}>×</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

window.JournalPage = JournalPage;


// === deploy/pages-myreef.jsx ===
// ReefReads. My Reef page
// Lists everything the user has starred (favorites.jsx). localStorage only.

function MyReefPage() {
  const fav = window.useFavorites ? window.useFavorites() : { state: { species: [], articles: [] } };
  const [tab, setTab] = mrUS("species");

  const species = mrUM(() => {
    const byId = new Map((window.SPECIES || []).map(s => [s.id, s]));
    return (fav.state.species || []).map(id => byId.get(id)).filter(Boolean);
  }, [fav.state.species]);

  const articles = mrUM(() => {
    const byId = new Map((window.ARTICLES || []).map(a => [a.id, a]));
    return (fav.state.articles || []).map(id => byId.get(id)).filter(Boolean);
  }, [fav.state.articles]);

  const total = species.length + articles.length;

  return (
    <div data-screen-label="my-reef">
      <div style={{ marginBottom: 30 }}>
        <div className="kicker">Saved in your browser · zero accounts</div>
        <h1 className="section-title" style={{ marginTop: 6 }}>My Reef.</h1>
        <p className="section-lede" style={{ marginTop: 8, maxWidth: 640 }}>
          {total === 0
            ? "Star species and articles to keep them here. Useful for shortlisting livestock before a frag swap or building a reading queue."
            : `${total} saved · ${species.length} species, ${articles.length} article${articles.length !== 1 ? "s" : ""}.`}
        </p>
      </div>

      {total === 0 ? (
        <MyReefEmpty/>
      ) : (
        <>
          <div className="myreef-tabs">
            <button className={tab === "species" ? "on" : ""} onClick={() => setTab("species")}>
              Species <span>{species.length}</span>
            </button>
            <button className={tab === "articles" ? "on" : ""} onClick={() => setTab("articles")}>
              Articles <span>{articles.length}</span>
            </button>
          </div>

          {tab === "species" && (
            species.length === 0
              ? <MyReefSubEmpty kind="species"/>
              : (
                <div className="species-grid">
                  {species.map(sp => (
                    window.SpeciesCard
                      ? <window.SpeciesCard key={sp.id} sp={sp} onClick={() => { window.history.pushState({}, "", `/species/${sp.id}`); window.dispatchEvent(new Event("reefreads:navigate")); }}/>
                      : <div key={sp.id}><a href={`/species/${sp.id}`}>{sp.common}</a></div>
                  ))}
                </div>
              )
          )}

          {tab === "articles" && (
            articles.length === 0
              ? <MyReefSubEmpty kind="articles"/>
              : (
                <div className="myreef-articles">
                  {articles.map(a => (
                    <a key={a.id} href={`/articles/${a.id}`} className="myreef-article-card">
                      <div className="myreef-article-kicker">{a.category} · {a.read}</div>
                      <h3 className="myreef-article-title">{a.title}</h3>
                      <p className="myreef-article-dek">{a.excerpt || a.dek || ""}</p>
                    </a>
                  ))}
                </div>
              )
          )}
        </>
      )}
    </div>
  );
}

function MyReefEmpty() {
  return (
    <div className="myreef-empty">
      <svg viewBox="0 0 80 80" width="80" height="80" aria-hidden="true">
        <path
          d="M40 8l9.7 20.8L72 32l-16 15 4.3 23L40 58.7 19.7 70 24 47 8 32l22.3-3.2L40 8z"
          fill="none" stroke="var(--cyan)" strokeWidth="1.5" strokeLinejoin="round" opacity="0.6"
        />
      </svg>
      <h3>Nothing saved yet.</h3>
      <p>Tap the star on any species card or article to add it here.</p>
      <div className="myreef-empty-cta">
        <a href="/library" className="btn primary">Browse the library</a>
        <a href="/articles" className="btn ghost">Read articles</a>
      </div>
    </div>
  );
}

function MyReefSubEmpty({ kind }) {
  return (
    <div className="myreef-empty" style={{ padding: 40 }}>
      <p style={{ color: 'var(--ink-3)' }}>
        No {kind} saved yet. Star one to add it here.
      </p>
    </div>
  );
}

window.MyReefPage = MyReefPage;


// === deploy/router.jsx ===

// ---------- Shared components (exposed on window for use across all JSX files) ----------

// Breadcrumb: shown above detail pages. Also kept in sync with the BreadcrumbList
// JSON-LD that the prerender writes into the static stub.
function Breadcrumb({ trail }) {
  return (
    <nav className="rr-breadcrumb" aria-label="Breadcrumb">
      <ol>
        {trail.map((t, i) => {
          const last = i === trail.length - 1;
          return (
            <li key={i}>
              {last || !t.href
                ? <span aria-current={last ? "page" : undefined}>{t.label}</span>
                : <a href={t.href}>{t.label}</a>}
              {!last && <span className="rr-bc-sep" aria-hidden="true">/</span>}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

// Last-updated stamp. Reads from <meta name="rr-build-date">, with an optional
// per-entry override.
function LastUpdated({ override, prefix = "Last reviewed" }) {
  const date = override
    || document.querySelector('meta[name="rr-build-date"]')?.content
    || "";
  if (!date) return null;
  return <div className="rr-updated">{prefix} · <time>{date}</time></div>;
}

// Fuzzy 404 with suggestions drawn from the URL fragments.
function NotFound({ path }) {
  const suggestions = useMemo(() => {
    const all = [
      ...(window.SPECIES || []).map(s => ({ kind: "species", id: s.id, common: s.common, latin: s.latin, kindLabel: s.kind, category: s.category })),
      ...(window.DISEASES || []).map(d => ({ kind: "disease", id: d.id, common: d.common || d.name || d.id, latin: d.latin || "" })),
      ...(window.ARTICLES || []).map(a => ({ kind: "article", id: a.id, common: a.title, latin: a.kicker || "" })),
    ];
    const tokens = (path || "")
      .toLowerCase()
      .split(/[/_\-\s.]+/)
      .filter(t => t && t.length >= 2 && !["species","diseases","articles","library","tools","best","of"].includes(t));
    if (tokens.length === 0) return [];

    const norm = s => (s || "").toLowerCase().replace(/[^a-z0-9]/g, "");

    const lev = (a, b) => {
      if (Math.abs(a.length - b.length) > 3) return 99;
      const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
      for (let j = 1; j <= b.length; j++) dp[0][j] = j;
      for (let i = 1; i <= a.length; i++)
        for (let j = 1; j <= b.length; j++)
          dp[i][j] = Math.min(
            dp[i-1][j] + 1,
            dp[i][j-1] + 1,
            dp[i-1][j-1] + (a[i-1] === b[j-1] ? 0 : 1)
          );
      return dp[a.length][b.length];
    };

    const scored = all.map(it => {
      const id = norm(it.id);
      const common = norm(it.common);
      const latin = norm(it.latin);
      let score = 0;
      for (const t of tokens) {
        if (id.includes(t)) score += 8;
        if (common.includes(t)) score += 6;
        if (latin.includes(t)) score += 5;
        if (id.startsWith(t)) score += 3;
        // Approximate matches
        if (id.length && lev(id, t) <= 2) score += 4;
        if (common.length && lev(common, t) <= 2) score += 3;
      }
      return { ...it, score };
    }).filter(x => x.score > 0)
      .sort((a, b) => b.score - a.score)
      .slice(0, 6);

    return scored;
  }, [path]);

  const url = (s) => s.kind === "species" ? `/species/${s.id}`
    : s.kind === "disease" ? `/diseases/${s.id}`
    : `/articles/${s.id}`;

  return (
    <div className="rr-404">
      <div className="rr-404-art">
        <svg viewBox="0 0 200 120" width="220" height="132" aria-hidden="true">
          <defs>
            <radialGradient id="rr404g" cx="50%" cy="60%" r="60%">
              <stop offset="0%" stopColor="var(--cyan)" stopOpacity="0.35"/>
              <stop offset="100%" stopColor="var(--bg)" stopOpacity="0"/>
            </radialGradient>
          </defs>
          <rect x="0" y="0" width="200" height="120" fill="url(#rr404g)"/>
          <text x="100" y="78" textAnchor="middle" fontFamily="var(--f-serif)" fontSize="64" fill="var(--cyan)" opacity="0.85">404</text>
          {[...Array(8)].map((_, i) => (
            <circle key={i} cx={30 + i * 22} cy={100 - (i % 3) * 6} r={1 + (i % 2)} fill="var(--cyan)" opacity={0.3 + (i % 3) * 0.15}/>
          ))}
        </svg>
      </div>

      <div className="kicker">404 · {path || "unknown route"}</div>
      <h1 className="rr-404-title">That page slipped through the sump.</h1>
      <p className="rr-404-lede">
        Nothing at <code>{path || "/"}</code>. The URL might be mistyped, or the page has been renamed since it was last linked.
      </p>

      {suggestions.length > 0 && (
        <>
          <div className="kicker" style={{ marginTop: 36, marginBottom: 10 }}>Did you mean…</div>
          <ul className="rr-404-suggestions">
            {suggestions.map(s => (
              <li key={`${s.kind}-${s.id}`}>
                <a href={url(s)}>
                  <span className="rr-404-sug-kind">{s.kind === "species" ? s.kindLabel || "species" : s.kind}</span>
                  <span className="rr-404-sug-name">{s.common}</span>
                  {s.latin && <em className="rr-404-sug-latin">{s.latin}</em>}
                </a>
              </li>
            ))}
          </ul>
        </>
      )}

      <div className="rr-404-actions">
        <a href="/" className="btn primary">← Home</a>
        <a href="/library" className="btn ghost">Browse all species</a>
        <a href="/diseases" className="btn ghost">Disease library</a>
      </div>
    </div>
  );
}

// Expose for use in other JSX files (they run before this script does, but the
// components only execute at render time — by then the globals exist).
Object.assign(window, { Breadcrumb, LastUpdated, NotFound });

function getInitialPath() {
  // On Cloudflare Pages, window.location.pathname is the real route ("/species/ocellaris").
  // In local previews / nested folder hosting, the iframe URL ends in the
  // file path on disk. Fall back to the canonical URL's pathname which the
  // prerender stamped into every stub.
  const p = window.location.pathname || "/";
  const known = /^\/(library|diseases|articles|best-of|guides|calculators|compatibility|journal|my-reef|donate|species|tools)(\/|$)/;
  if (p === "/" || known.test(p)) return p;
  const canon = document.querySelector('link[rel="canonical"]');
  if (canon) {
    try { return new URL(canon.href).pathname; } catch (_) {}
  }
  return p;
}

function usePath() {
  const [path, setPath] = useState(getInitialPath());
  useEffect(() => {
    const onChange = () => {
      setPath(window.location.pathname || "/");
      // Remove the prerender skeleton on first navigation (and on hydration it's already gone)
      const skel = document.querySelector("[data-prerender]");
      if (skel) skel.remove();
    };
    window.addEventListener("popstate", onChange);
    window.addEventListener("reefreads:navigate", onChange);
    // Global delegation: hijack same-origin link clicks for SPA navigation.
    const onClick = (e) => {
      if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
      const a = e.target.closest("a");
      if (!a) return;
      const href = a.getAttribute("href");
      if (!href) return;
      if (a.target === "_blank" || a.hasAttribute("download")) return;
      // Only intercept root-relative paths
      if (!href.startsWith("/")) return;
      e.preventDefault();
      if (href === window.location.pathname + window.location.search) return;
      window.history.pushState({}, "", href);
      window.dispatchEvent(new Event("reefreads:navigate"));
    };
    document.addEventListener("click", onClick);
    return () => {
      window.removeEventListener("popstate", onChange);
      window.removeEventListener("reefreads:navigate", onChange);
      document.removeEventListener("click", onClick);
    };
  }, []);
  return path;
}

// Per-route meta updates on client navigation. The prerendered stubs already
// have correct <title>, <meta description>, canonical and OG tags for SEO —
// search engines read those before JS runs. We only update <title> and
// description for the live browser tab. Canonical/OG URL stay frozen so the
// preview environment doesn't pollute the public URL.
function updateMeta(meta) {
  if (!meta) return;
  if (meta.title) document.title = meta.title;
  if (meta.description) {
    let el = document.querySelector('meta[name="description"]');
    if (!el) {
      el = document.createElement("meta");
      el.setAttribute("name", "description");
      document.head.appendChild(el);
    }
    el.setAttribute("content", meta.description);
  }
}

function Sidebar({ path, theme, toggleTheme }) {
  const Item = ({ to, label }) => {
    const active = path === to || to !== "/" && path.startsWith(to);
    return (
      <a href={to} className={active ? "active" : ""}>
          <span className="nav-dot" />
          {label}
        </a>);

  };

  const NavItem = ({ to, label, icon }) => {
    const active = path === to || to !== "/" && path.startsWith(to);
    return (
      <a href={to} className={`nav-main ${active ? 'active' : ''}`}>
          <span className="nav-main-icon">{icon}</span>
          <span>{label}</span>
        </a>);

  };

  return (
    <aside className="sidebar" data-screen-label="sidebar">
        <div className="brand">
          <div className="brand-mark" />
          <div>
            <div className="brand-name">ReefReads</div>
            <div className="brand-sub">Field Guide · v2.6</div>
          </div>
        </div>

        <div className="nav">
          <div className="nav-group-label">Read</div>
          <NavItem to="/" label="Home" icon="◐" />
          <NavItem to="/guides" label="Guides" icon="❋" />
          <NavItem to="/articles" label="Articles" icon="✦" />
          <NavItem to="/best-of" label="Best Of" icon="★" />
          <NavItem to="/my-reef" label="My Reef" icon="☆" />

          <div className="nav-group-label">Reference</div>
          <NavItem to="/library" label="Species Library" icon="◇" />
          <NavItem to="/diseases" label="Disease & Pests" icon="⚕" />
          <NavItem to="/tools/glossary" label="Glossary" icon="¶" />

          <div className="nav-group-label">Tools</div>
          <NavItem to="/journal" label="Tank Journal" icon="◧" />
          <NavItem to="/tools/quarantine" label="Quarantine" icon="⦾" />
          <NavItem to="/tools/dosing-schedule" label="Dosing Schedule" icon="⚖" />
          <NavItem to="/calculators" label="Calculators" icon="∑" />
          <NavItem to="/compatibility" label="Compatibility" icon="⇄" />
          <NavItem to="/tools/symptoms" label="Diagnosis" icon="⊕" />
          <NavItem to="/tools/planner" label="Setup Planner" icon="◰" />

          <div className="nav-group-label">Support</div>
          <NavItem to="/donate" label="Donate" icon="♡" />
        </div>

        <div className="sidebar-footer">
          <div className="stat-mini"><span>Species</span><span>{(window.SPECIES || []).length}</span></div>
          <div className="stat-mini"><span>Diseases</span><span>{(window.DISEASES || []).length}</span></div>
          <div className="stat-mini"><span>Last update</span><span>May '26</span></div>
          <button className="theme-toggle" onClick={toggleTheme}>
            <span className="theme-dot" />
            {theme === "dark" ? "Dark mode" : "Light mode"}
          </button>
        </div>
      </aside>);

}

function Topbar({ path }) {
  const parts = [];
  if (path === "/") parts.push("Home");else
  {
    const segs = path.split("/").filter(Boolean);
    const [section, detail] = segs;
    parts.push(section || "Home");
    if (detail) {
      if (section === "species") {
        const sp = window.SPECIES.find((s) => s.id === detail);
        parts.push(sp ? sp.common : detail);
      } else parts.push(detail);
    }
  }

  return (
    <div className="topbar" data-screen-label="topbar">
        <div className="breadcrumb">
          <span>ReefReads</span>
          {parts.map((p, i) =>
        <React.Fragment key={i}>
              <span className="sep">/</span>
              <b style={{ textTransform: 'capitalize' }}>{p}</b>
            </React.Fragment>
        )}
        </div>
        <div className="search">
          <span style={{ fontSize: 13, color: 'var(--ink-3)' }}>⌕</span>
          <input placeholder="Search species, corals, symptoms…" />
          <span className="kbd">CTRL V
</span>
        </div>
      </div>);
}

function Tweaks({ theme, setTheme }) {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const onMessage = (e) => {
      if (e.data?.type === "__activate_edit_mode") setOpen(true);
      if (e.data?.type === "__deactivate_edit_mode") setOpen(false);
    };
    window.addEventListener("message", onMessage);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMessage);
  }, []);

  const setThemeAndPersist = (t) => {
    setTheme(t);
    window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { theme: t } }, "*");
  };

  return (
    <div className={`tweaks ${open ? 'open' : ''}`}>
        <h4>Tweaks · Appearance</h4>
        <div className="seg">
          <button className={theme === "dark" ? "on" : ""} onClick={() => setThemeAndPersist("dark")}>Dark</button>
          <button className={theme === "light" ? "on" : ""} onClick={() => setThemeAndPersist("light")}>Light</button>
        </div>
      </div>);

}

function App() {
  const path = usePath();
  const [theme, setTheme] = useState(window.__TWEAKS.theme || "dark");

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
  }, [theme]);

  useEffect(() => {
    window.scrollTo({ top: 0, behavior: 'instant' });
  }, [path]);

  const navigate = (to) => {
    // Accept legacy "#/foo" or new "/foo" forms.
    const p = to.startsWith("#") ? to.slice(1) : to;
    window.history.pushState({}, "", p || "/");
    window.dispatchEvent(new Event("reefreads:navigate"));
  };

  let page;
  const segs = path.split("/").filter(Boolean);
  const section = segs[0] || "";
  const detail = segs[1] || "";
  const sectionKey = section;

  // Per-route meta (title + description). Keeps the tab title and sharable
  // preview in sync with client-side navigation. Prerendered stubs set the
  // initial values; this catches in-app navigation.
  let routeMeta = null;

  if (!sectionKey) { page = <HomePage navigate={navigate} />; routeMeta = { title: "ReefReads — A field guide for reef keepers", description: "Species profiles, coral care, water chemistry diagnostics and beginner-to-expert guides for saltwater reef tanks." }; }
  else if (sectionKey === "guides") { page = <GuidesPage />; routeMeta = { title: "Reef-keeping guides | ReefReads", description: "Beginner to advanced learning tracks for saltwater reef-keeping: setup, livestock, water chemistry, and automation." }; }
  else if (sectionKey === "articles") { page = <ArticlesPage navigate={navigate} articleId={detail} />; routeMeta = { title: detail ? "Article | ReefReads" : "Field articles | ReefReads", description: "In-depth field articles on reef-keeping techniques, parameter chasing, and tank stories." }; }
  else if (sectionKey === "library") { page = <LibraryPage navigate={navigate} />; routeMeta = { title: "Species library | ReefReads", description: `Browse ${(window.SPECIES || []).length} saltwater fish, corals and inverts with care requirements, parameters and compatibility.` }; }
  else if (sectionKey === "diseases") { page = <DiseaseLibrary navigate={navigate} diseaseId={detail} />; routeMeta = { title: detail ? "Disease | ReefReads" : "Diseases & pests | ReefReads", description: "Diagnose and treat marine fish disease and reef tank pests with evidence-based protocols." }; }
  else if (sectionKey === "best-of") { page = <BestOfHub navigate={navigate} slug={detail} />; routeMeta = { title: detail ? "Best of | ReefReads" : "Best of. ReefReads", description: "Curated collections: easiest beginner fish, hardiest corals, and best clean-up crew picks." }; }
  else if (sectionKey === "calculators") { page = <CalculatorsHub />; routeMeta = { title: "Reef calculators | ReefReads", description: "Dosing, water change, salt mix and lighting calculators for reef tanks." }; }
  else if (sectionKey === "compatibility") { page = <CompatibilityHub />; routeMeta = { title: "Compatibility | ReefReads", description: "Check whether two saltwater species can live together and what tank size you'll need." }; }
  else if (sectionKey === "donate") { page = <DonatePage />; routeMeta = { title: "Support ReefReads", description: "Help keep ReefReads independent. One-time tip or monthly support via Ko-fi (Stripe + PayPal)." }; }
  else if (sectionKey === "journal") { page = <JournalPage />; routeMeta = { title: "Tank journal | ReefReads", description: "Log pH, alkalinity, calcium, magnesium, nitrate and phosphate over time. See trends. Stored locally in your browser." }; }
  else if (sectionKey === "my-reef") { page = <MyReefPage />; routeMeta = { title: "My Reef | ReefReads", description: "Your saved species and articles. Stored locally in your browser." }; }
  else if (sectionKey === "species" && detail) {
    page = <SpeciesDetail id={detail} navigate={navigate} />;
    const sp = (window.SPECIES || []).find((s) => s.id === detail);
    if (sp) routeMeta = { title: `${sp.common} (${sp.latin}) care guide | ReefReads`, description: sp.short };
  }
  else if (sectionKey === "tools") {
    if (detail === "params") page = <ParamsTool />;
    else if (detail === "compat") page = <CompatTool />;
    else if (detail === "dosing") page = <DosingTool />;
    else if (detail === "symptoms") page = <SymptomTool />;
    else if (detail === "planner") page = <PlannerToolExpanded />;
    else if (detail === "paramlog") page = <ParamLog />;
    else if (detail === "pair") page = <PairCompat />;
    else if (detail === "stocking") page = <StockingTool />;
    else if (detail === "waterchange") page = <WaterChangeTool />;
    else if (detail === "journal") page = <JournalTool />;
    else if (detail === "quarantine") page = <QuarantineTimeline />;
    else if (detail === "dosing-schedule") page = <DosingScheduler />;
    else if (detail === "glossary") page = <GlossaryTool />;
    else if (detail === "saltmix") page = <SaltMixTool />;
    else if (detail === "lighting") page = <LightingTool />;
    else page = <div><h1 className="section-title">Tools</h1><p className="section-lede" style={{ marginTop: 8 }}>Pick a tool from the sidebar.</p></div>;
    const TOOL_TITLES = {
      params: "Parameter scanner", compat: "Compatibility tool",
      dosing: "Dosing calculator", "dosing-schedule": "Dosing scheduler",
      quarantine: "Quarantine timeline", symptoms: "Symptom diagnosis",
      planner: "Tank setup planner", pair: "Pair compatibility",
      stocking: "Stocking calculator", waterchange: "Water change tool",
      journal: "Livestock journal", glossary: "Reef-keeping glossary",
      saltmix: "Salt mix comparator", lighting: "Lighting calculator",
      paramlog: "Parameter log"
    };
    routeMeta = { title: `${TOOL_TITLES[detail] || 'Tools'} | ReefReads`, description: "Reef tank tools: parameter checks, compatibility, dosing, quarantine timelines and more." };
  } else {
    page = <NotFound path={path}/>;
    routeMeta = { title: "Not found | ReefReads", description: "Page not found. Try searching the species library or returning to the homepage." };
  }

  useEffect(() => { updateMeta(routeMeta); }, [path]);

  return (
    <div className="app" data-screen-label={`01 ${sectionKey || 'home'}${detail ? ' / ' + detail : ''}`}>
        <Sidebar path={path} theme={theme} toggleTheme={() => setTheme((t) => t === "dark" ? "light" : "dark")} />
        <div className="main">
          <Topbar path={path} />
          <div className="page">{page}</div>
        </div>
        <Tweaks theme={theme} setTheme={setTheme} />
      </div>);

}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);