/* ============================================================================
   MOWDEW — Kit primitives
   Icon sprite (Lucide-style, 2px outline) + sprout mascot + logo + the shared
   components every screen leans on: Tag, Chip, Amount, RailChip, Avatar,
   IconBadge, PhotoTile, money formatting. Exposed on window.

   Load AFTER React + Babel, BEFORE any screen file.
   ========================================================================= */

const TN_ICONS = {
  // nav + chrome
  home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/><path d="M9 22V12h6v10"/>',
  route: '<circle cx="6" cy="19" r="3"/><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"/><circle cx="18" cy="5" r="3"/>',
  feed: '<path d="M4 4h16M4 4v16M4 9h16M4 14h16M4 19h16"/>',
  file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6M9 13h6M9 17h6"/>',
  ledger: '<rect x="3" y="3" width="18" height="18" rx="2.5"/><path d="M3 9h18M9 21V9"/>',
  user: '<circle cx="12" cy="8" r="4"/><path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1"/>',
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>',
  settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/>',
  bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
  // location / verification
  pin: '<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/>',
  navigation: '<path d="M3 11l19-9-9 19-2-8-8-2Z"/>',
  target: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none"/>',
  shield: '<path d="M12 2 4 5v6c0 5 3.5 8.5 8 11 4.5-2.5 8-6 8-11V5Z"/><path d="m9 12 2 2 4-4"/>',
  verified: '<path d="m12 2 2.4 1.8 3-.3 1.2 2.8 2.7 1.3-.6 3 1.6 2.5-1.6 2.5.6 3-2.7 1.3-1.2 2.8-3-.3L12 22l-2.4-1.8-3 .3-1.2-2.8L2.7 16.4l.6-3L1.7 11l1.6-2.5-.6-3 2.7-1.3 1.2-2.8 3 .3Z"/><path d="m9 12 2 2 4-4"/>',
  clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
  // capture / media
  camera: '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3Z"/><circle cx="12" cy="13" r="3.2"/>',
  image: '<rect x="3" y="3" width="18" height="18" rx="2.5"/><circle cx="8.5" cy="8.5" r="1.8"/><path d="m21 15-5-5L5 21"/>',
  notes: '<path d="M4 4h16v16H4Z"/><path d="M8 9h8M8 13h8M8 17h4"/>',
  // money / rails
  invoice: '<path d="M5 3v18l2-1.4 2 1.4 2-1.4 2 1.4 2-1.4 2 1.4V3l-2 1.4L13 3l-2 1.4L9 3 7 4.4Z"/><path d="M8 8h8M8 12h8M8 16h5"/>',
  send: '<path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/>',
  wallet: '<path d="M3 7a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v2H3Z"/><path d="M3 9v8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-3"/><path d="M21 11v4h-4a2 2 0 0 1 0-4Z"/>',
  link: '<path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/>',
  // actions
  plus: '<path d="M12 5v14M5 12h14"/>',
  check: '<path d="m5 12 5 5L20 7"/>',
  'check-circle': '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
  x: '<path d="M18 6 6 18M6 6l12 12"/>',
  search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
  'arrow-right': '<path d="M5 12h14M12 5l7 7-7 7"/>',
  'arrow-left': '<path d="M19 12H5M12 19l-7-7 7-7"/>',
  'chevron-right': '<path d="m9 18 6-6-6-6"/>',
  'chevron-down': '<path d="m6 9 6 6 6-6"/>',
  calendar: '<rect x="3" y="4" width="18" height="18" rx="2.5"/><path d="M3 10h18M8 2v4M16 2v4"/>',
  copy: '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
  phone: '<path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3-8.6A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .4 1.9.7 2.8a2 2 0 0 1-.5 2.1L8.1 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.8.7a2 2 0 0 1 1.7 2Z"/>',
  share: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/>',
  star: '<path d="m12 2 3 6.5 7 .9-5 4.9 1.3 7L12 18l-6.3 3.3L7 14.3 2 9.4l7-.9Z"/>',
  'thumbs-up': '<path d="M7 10v11H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1Z"/><path d="M7 10l4-7a2.5 2.5 0 0 1 2.5 3.5L12.5 9H19a2 2 0 0 1 2 2.3l-1.2 7A2 2 0 0 1 17.8 20H7"/>',
  'thumbs-down': '<path d="M17 14V3h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1Z"/><path d="M17 14l-4 7a2.5 2.5 0 0 1-2.5-3.5L11.5 15H5a2 2 0 0 1-2-2.3l1.2-7A2 2 0 0 1 6.2 4H17"/>',
  // domain
  leaf: '<path d="M11 20A7 7 0 0 1 4 13c0-6 6-9 16-9 0 8-4 13-9 13Z"/><path d="M4 20c2-4 5-7 9-9"/>',
  droplet: '<path d="M12 2.7s6 5.7 6 10.3a6 6 0 0 1-12 0c0-4.6 6-10.3 6-10.3Z"/>',
  can: '<path d="M5 11h9v6a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3Z"/><path d="M14 13h3l3-4"/><path d="M7 11V9a2 2 0 0 1 4 0v2"/><path d="M19.5 9.5 22 6"/>',
  zap: '<path d="M13 2 3 14h7l-1 8 10-12h-7l1-8Z"/>',
  more: '<circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/>',
  list: '<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>',
  history: '<path d="M3 3v5h5"/><path d="M3.5 9A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/>',
  // pool + chemistry
  pool: '<path d="M2 19c1.2 0 1.2 1 2.5 1S5.8 19 7 19s1.3 1 2.5 1 1.3-1 2.5-1 1.3 1 2.5 1 1.3-1 2.5-1 1.2 1 2.5 1"/><path d="M2 14.5c1.2 0 1.2 1 2.5 1s1.3-1 2.5-1 1.3 1 2.5 1 1.3-1 2.5-1 1.3 1 2.5 1 1.3-1 2.5-1 1.2 1 2.5 1"/><path d="M7 14V5a2 2 0 0 1 4 0M16 14V5a2 2 0 0 1 4 0"/>',
  flask: '<path d="M9 3h6M10 3v6.5L4.5 18A2 2 0 0 0 6.3 21h11.4a2 2 0 0 0 1.8-2.5L14 9.5V3"/><path d="M7 15h10"/>',
  testtube: '<path d="M15 3 6.5 11.5a3.5 3.5 0 0 0 5 5L20 8"/><path d="m13 5 4 4M8.5 13.5 12 17"/>',
  thermometer: '<path d="M14 14.8V5a2 2 0 0 0-4 0v9.8a4 4 0 1 0 4 0Z"/><path d="M12 9v6"/>',
  waves: '<path d="M2 7c1.2 0 1.2 1 2.5 1S5.8 7 7 7s1.3 1 2.5 1S10.8 7 12 7s1.3 1 2.5 1S15.8 7 17 7s1.3 1 2.5 1S20.8 7 22 7"/><path d="M2 12c1.2 0 1.2 1 2.5 1s1.3-1 2.5-1 1.3 1 2.5 1 1.3-1 2.5-1 1.3 1 2.5 1 1.3-1 2.5-1 1.2 1 2.5 1"/><path d="M2 17c1.2 0 1.2 1 2.5 1s1.3-1 2.5-1 1.3 1 2.5 1 1.3-1 2.5-1 1.3 1 2.5 1 1.3-1 2.5-1 1.2 1 2.5 1"/>',
  gauge: '<path d="M12 14 18 8"/><circle cx="12" cy="14" r="1.4" fill="currentColor" stroke="none"/><path d="M4.5 18a9 9 0 1 1 15 0"/>',
  sparkle: '<path d="M12 3v4M12 17v4M5 12H1M23 12h-4M6.3 6.3 9 9M15 15l2.7 2.7M17.7 6.3 15 9M9 15l-2.7 2.7"/>',
  message: '<path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-3.9-.9L3 20l1.3-4.1A8.4 8.4 0 0 1 12 3a8.4 8.4 0 0 1 9 8.5Z"/>',
  'calendar-check': '<rect x="3" y="4" width="18" height="18" rx="2.5"/><path d="M3 10h18M8 2v4M16 2v4"/><path d="m9 15 2 2 4-4"/>',
  grid: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/>',
  truck: '<path d="M14 17V5a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12h2"/><path d="M14 8h4l4 4v5h-2"/><circle cx="6.5" cy="17.5" r="2"/><circle cx="17.5" cy="17.5" r="2"/>',
  dollar: '<path d="M12 1v22M17 5.5C17 3.6 14.8 3 12 3S7 3.8 7 6s2.5 3 5 3.5 5 1.2 5 3.5-2.2 3.5-5 3.5-5-.8-5-2.5"/>',
  nudge: '<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9Z"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="m18 4 3-2M20 8h3"/>',
  filter: '<path d="M22 3H2l8 9.5V19l4 2v-8.5L22 3Z"/>',
  'trend-up': '<path d="M22 7 13.5 15.5 8.5 10.5 2 17"/><path d="M16 7h6v6"/>',
  'trend-down': '<path d="M22 17 13.5 8.5 8.5 13.5 2 7"/><path d="M16 17h6v-6"/>',
  lock: '<rect x="3" y="11" width="18" height="11" rx="2.5"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
  'eye-off': '<path d="M9.9 4.2A9.1 9.1 0 0 1 12 4c7 0 10 8 10 8a18 18 0 0 1-2.2 3.2M6.6 6.6A18 18 0 0 0 2 12s3 8 10 8a9 9 0 0 0 5.4-1.6"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20"/>',
  map: '<path d="m9 4-6 2v16l6-2 6 2 6-2V4l-6 2-6-2Z"/><path d="M9 4v16M15 6v16"/>',
  inbox: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.5 5h13L22 12v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6Z"/>',
  beaker: '<path d="M6 3h12M8 3v7l-4 8a2 2 0 0 0 1.8 3h12.4A2 2 0 0 0 20 18l-4-8V3"/><path d="M5.5 14h13"/>',
  hand: '<path d="M18 11V6a2 2 0 0 0-4 0v5M14 10V4a2 2 0 0 0-4 0v7M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M18 8a2 2 0 0 1 4 0v6a8 8 0 0 1-8 8h-2a8 8 0 0 1-7-4l-2.8-4a2 2 0 0 1 3.4-2L8 14"/>',
  briefcase: '<rect x="2" y="7" width="20" height="14" rx="2.5"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M2 13h20"/>',
  sprout: '<path d="M12 22V11"/><path d="M12 11C9 11 5 9 5 4c5 0 7 3 7 7Z"/><path d="M12 9c1-4 4-6 8-6 0 4-3 7-8 7"/>',
};

function Icon({ name, size = 22, strokeWidth = 2, style = {}, className = '' }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={strokeWidth}
      strokeLinecap="round" strokeLinejoin="round"
      className={className} style={style} aria-hidden="true"
      dangerouslySetInnerHTML={{ __html: TN_ICONS[name] || '' }} />
  );
}

/* ─── Money ─────────────────────────────────────────────────────────── */
function fmtMoney(cents) {
  return '$' + Math.abs(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function Amount({ cents, size = 16, tone = 'plain', weight = 800 }) {
  const color = tone === 'paid' ? 'var(--green-700)' : tone === 'due' ? 'var(--alert-700)' : tone === 'muted' ? 'var(--ink-500)' : 'var(--ink-900)';
  return (
    <span style={{ fontFamily: 'var(--font-display)', fontWeight: weight, fontVariantNumeric: 'tabular-nums lining-nums', letterSpacing: '-0.025em', whiteSpace: 'nowrap', fontSize: size, color }}>
      {fmtMoney(cents)}
    </span>
  );
}

/* ─── Sprout mascot — "Sprig" ───────────────────────────────────────────
   A friendly two-leaf sprout with a face. mood: happy | wave | wink | zzz.
   Rendered as flat shapes in brand green + sky pot + sunny cheeks.        */
function Sprout({ size = 96, mood = 'happy', style = {} }) {
  const blush = '#FF9E7D';
  return (
    <svg width={size} height={size} viewBox="0 0 120 120" style={style} aria-hidden="true">
      {/* soil mound */}
      <ellipse cx="60" cy="106" rx="34" ry="9" fill="#E3D9C6" />
      {/* pot */}
      <path d="M38 78h44l-4 22a8 8 0 0 1-8 7H50a8 8 0 0 1-8-7Z" fill="#46B6F0" />
      <path d="M36 72h48a3 3 0 0 1 3 3v3a3 3 0 0 1-3 3H36a3 3 0 0 1-3-3v-3a3 3 0 0 1 3-3Z" fill="#2FA3E0" />
      {/* stem */}
      <path d="M60 78V50" stroke="#15803C" strokeWidth="7" strokeLinecap="round" fill="none" />
      {/* left leaf */}
      <path d="M58 58C44 60 30 52 28 38c14-2 28 4 30 18Z" fill="#21A84F" />
      <path d="M40 44c6 3 12 8 16 13" stroke="#15803C" strokeWidth="2.4" fill="none" strokeLinecap="round" />
      {/* right leaf (the face leaf) */}
      <path d="M62 56C76 54 92 58 96 44 82 40 66 42 62 56Z" fill="#62CC86" />
      {/* head bud */}
      <circle cx="60" cy="40" r="22" fill="#21A84F" />
      <path d="M60 18c4 4 4 10 0 14-4-4-4-10 0-14Z" fill="#62CC86" />
      {/* cheeks */}
      <circle cx="49" cy="44" r="4" fill={blush} opacity="0.85" />
      <circle cx="71" cy="44" r="4" fill={blush} opacity="0.85" />
      {/* eyes */}
      {mood === 'zzz' ? (
        <g stroke="#0F5E2C" strokeWidth="2.6" strokeLinecap="round">
          <path d="M50 38c2 2 5 2 7 0" /><path d="M63 38c2 2 5 2 7 0" />
        </g>
      ) : mood === 'wink' ? (
        <g fill="#15321f">
          <path d="M50 38c2 2 5 2 7 0" stroke="#0F5E2C" strokeWidth="2.6" fill="none" strokeLinecap="round" />
          <circle cx="67" cy="39" r="3.2" />
        </g>
      ) : (
        <g fill="#15321f">
          <circle cx="53" cy="39" r="3.2" /><circle cx="67" cy="39" r="3.2" />
          <circle cx="54.1" cy="37.9" r="1" fill="#fff" /><circle cx="68.1" cy="37.9" r="1" fill="#fff" />
        </g>
      )}
      {/* smile */}
      <path d="M54 47c3 3 9 3 12 0" stroke="#15321f" strokeWidth="2.6" fill="none" strokeLinecap="round" />
    </svg>
  );
}

/* ─── Dewy — the aqua mascot (pool's companion to Sprig the sprout) ──────
   A cheerful water droplet that rises out of a little pool ripple.
   mood: happy | wave | wink | zzz.                                        */
function Dewy({ size = 96, mood = 'happy', style = {} }) {
  const blush = '#7FE0E8';
  return (
    <svg width={size} height={size} viewBox="0 0 120 120" style={style} aria-hidden="true">
      {/* pool ripple base */}
      <ellipse cx="60" cy="104" rx="38" ry="8" fill="#BDEEF2" />
      <path d="M26 100c5 0 5 3 10 3s5-3 10-3 5 3 10 3 5-3 10-3 5 3 10 3 5-3 8-3" stroke="#54CED9" strokeWidth="2.4" fill="none" strokeLinecap="round" />
      {/* droplet body */}
      <path d="M60 16c10 16 22 27 22 41a22 22 0 0 1-44 0c0-14 12-25 22-41Z" fill="#12BCC9" />
      {/* shine highlight */}
      <path d="M50 44c-4 4-6 9-5 15" stroke="#BDEEF2" strokeWidth="4" fill="none" strokeLinecap="round" opacity="0.9" />
      {/* cheeks */}
      <circle cx="48" cy="63" r="4.5" fill={blush} opacity="0.9" />
      <circle cx="72" cy="63" r="4.5" fill={blush} opacity="0.9" />
      {/* eyes */}
      {mood === 'zzz' ? (
        <g stroke="#075A61" strokeWidth="2.6" strokeLinecap="round">
          <path d="M49 57c2 2 5 2 7 0" /><path d="M64 57c2 2 5 2 7 0" />
        </g>
      ) : mood === 'wink' ? (
        <g fill="#063b40">
          <path d="M49 57c2 2 5 2 7 0" stroke="#075A61" strokeWidth="2.6" fill="none" strokeLinecap="round" />
          <circle cx="68" cy="58" r="3.4" />
        </g>
      ) : (
        <g fill="#063b40">
          <circle cx="52.5" cy="58" r="3.4" /><circle cx="67.5" cy="58" r="3.4" />
          <circle cx="53.7" cy="56.8" r="1.05" fill="#fff" /><circle cx="68.7" cy="56.8" r="1.05" fill="#fff" />
        </g>
      )}
      {/* smile */}
      <path d="M54 66c3 3 9 3 12 0" stroke="#063b40" strokeWidth="2.6" fill="none" strokeLinecap="round" />
      {/* wave arm */}
      {mood === 'wave' && <path d="M82 52c6-3 10-2 13-7" stroke="#12BCC9" strokeWidth="6" fill="none" strokeLinecap="round" />}
    </svg>
  );
}

/* ─── Mascot — pick by vertical: garden -> Sprig, pool -> Dewy ───────── */
function Mascot({ vertical = 'garden', ...props }) {
  return vertical === 'pool' ? <Dewy {...props} /> : <Sprout {...props} />;
}
function Logo({ size = 40, withWord = false, word = 'Mowdew', color = 'var(--ink-900)' }) {
  const rid = (React.useId ? React.useId() : 'tnlg').replace(/[:]/g, '');
  const g = 'tnlg-' + rid, sh = 'tnsh-' + rid;
  const mark = (
    <svg width={size} height={size} viewBox="0 0 48 48" aria-hidden="true">
      <defs>
        <linearGradient id={g} x1="8" y1="7" x2="40" y2="42" gradientUnits="userSpaceOnUse">
          <stop offset="0" stopColor="#21A84F" />
          <stop offset="1" stopColor="#12BCC9" />
        </linearGradient>
        <linearGradient id={sh} x1="16" y1="10" x2="30" y2="41" gradientUnits="userSpaceOnUse">
          <stop offset="0" stopColor="#FFFFFF" />
          <stop offset="1" stopColor="#E4F6F1" />
        </linearGradient>
      </defs>
      <rect width="48" height="48" rx="14" fill={`url(#${g})`} />
      {/* unified mark: a water droplet (pool) whose veins make it a leaf (garden).
          Tip leans, base is round — reads as both. */}
      <path d="M25 8.4c1.5 6.3 9.6 13.2 9.6 21.8a10.6 10.6 0 0 1-21.2 0C13.4 21.9 20.6 15.2 23 9.7c.6-1.4 1.6-2.1 2-1.3Z" fill={`url(#${sh})`} />
      {/* curved leaf midrib (gradient shows through the white) */}
      <path d="M23.5 34.2C22.7 27.4 22.6 19.4 25 12.4" stroke={`url(#${g})`} strokeWidth="2.2" strokeLinecap="round" fill="none" />
      {/* alternating veins branching off the midrib */}
      <path d="M23 29.4c-1.7-.1-3.2-1-4.2-2.6" stroke={`url(#${g})`} strokeWidth="1.7" strokeLinecap="round" fill="none" />
      <path d="M22.9 24.2c1.9 0 3.7-.9 4.9-2.6" stroke={`url(#${g})`} strokeWidth="1.7" strokeLinecap="round" fill="none" />
      <path d="M23.6 18.6c-1.5-.2-2.8-1-3.6-2.3" stroke={`url(#${g})`} strokeWidth="1.6" strokeLinecap="round" fill="none" />
    </svg>
  );
  if (!withWord) return mark;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 11 }}>
      {mark}
      <span style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size * 0.62, letterSpacing: '-0.03em', color }}>{word}</span>
    </span>
  );
}

/* ─── Tag — Mowdew "record mark": line glyph + sentence label + hand-drawn
   underline. No pill, no oval. The underline is a sketchy marker stroke in the
   tone accent; the mark defaults to the mascot motif (leaf garden / droplet
   pool) or any glyph via `icon`. */
const TAG_TONE = {
  green:  { mark: 'leaf',    accent: '#21A84F' },
  aqua:   { mark: 'droplet', accent: '#12BCC9' },
  blue:   { mark: 'droplet', accent: '#46B6F0' },
  yellow: { mark: 'leaf',    accent: '#E0A92E' },
  alert:  { mark: 'bell',    accent: '#F2683C' },
  violet: { mark: 'flask',   accent: '#7C6FF0' },
  ink:    { mark: 'leaf',    accent: 'var(--ink-700)' },
};
function Tag({ children, tone = 'green', icon, onDark = false, style = {} }) {
  const t = TAG_TONE[tone] || TAG_TONE.green;
  const accent = onDark ? '#fff' : t.accent;
  const labelColor = onDark ? '#fff' : 'var(--ink-900)';
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontFamily: 'var(--font-body)', fontWeight: 700, fontSize: 12.5, letterSpacing: '-0.005em', color: labelColor, whiteSpace: 'nowrap', ...style }}>
      <Icon name={icon || t.mark} size={15} strokeWidth={2.4} style={{ color: accent, flexShrink: 0 }} />
      <span style={{ position: 'relative', paddingBottom: 5 }}>
        {children}
        <svg viewBox="0 0 100 6" preserveAspectRatio="none" fill="none" aria-hidden="true" style={{ position: 'absolute', left: 0, bottom: 0, width: '100%', height: 5, overflow: 'visible' }}>
          <path d="M1 4C16 1.6 32 5.4 49 3.6 66 1.6 83 5.2 99 3.2" stroke={accent} strokeWidth="2.4" strokeLinecap="round" />
        </svg>
      </span>
    </span>
  );
}

/* ─── SectionHead — the "ledger entry" header for product surfaces ───────
   A line-mark + sentence-case title (hand-drawn underline) and a dotted ledger
   rule that runs to the edge, optionally meeting a count or action on the
   right. Reads like a line filed into a record — ownable, not an AI eyebrow.   */
function HandUnderline({ color, height = 5, strokeWidth = 2.4 }) {
  return (
    <svg viewBox="0 0 100 6" preserveAspectRatio="none" fill="none" aria-hidden="true" style={{ position: 'absolute', left: 0, bottom: 0, width: '100%', height, overflow: 'visible' }}>
      <path d="M1 4C16 1.6 32 5.4 49 3.6 66 1.6 83 5.2 99 3.2" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" />
    </svg>
  );
}
function SectionHead({ children, tone = 'green', icon, meta, rule = true, onDark = false, size = 15, style = {} }) {
  const t = TAG_TONE[tone] || TAG_TONE.green;
  const accent = onDark ? '#fff' : t.accent;
  const titleColor = onDark ? '#fff' : 'var(--ink-900)';
  const ruleColor = onDark ? 'rgba(255,255,255,.22)' : 'var(--ink-200)';
  const metaColor = onDark ? 'rgba(255,255,255,.7)' : 'var(--ink-500)';
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10, ...style }}>
      <Icon name={icon || t.mark} size={Math.round(size * 1.08)} strokeWidth={2.4} style={{ color: accent, flexShrink: 0 }} />
      <span style={{ position: 'relative', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size, letterSpacing: '-0.02em', color: titleColor, whiteSpace: 'nowrap', paddingBottom: 4, flexShrink: 0 }}>
        {children}
        <HandUnderline color={accent} />
      </span>
      {rule && <span style={{ flex: 1, height: 0, borderTop: `1.5px dotted ${ruleColor}`, minWidth: 14, margin: '3px 0 0' }} />}
      {meta && <span style={{ flexShrink: 0, fontSize: 12.5, fontWeight: 600, color: metaColor, display: 'inline-flex', alignItems: 'center', gap: 6 }}>{meta}</span>}
    </div>
  );
}

/* ─── Status chip — pill with dot ───────────────────────────────────── */
const CHIP_STYLE = {
  done:  { bg: 'var(--green-50)', fg: 'var(--green-700)', bd: 'var(--green-100)', dot: 'var(--green-500)' },
  live:  { bg: 'var(--blue-50)', fg: 'var(--blue-700)', bd: 'var(--blue-100)', dot: 'var(--blue-500)' },
  pool:  { bg: 'var(--aqua-50)', fg: 'var(--aqua-700)', bd: 'var(--aqua-100)', dot: 'var(--aqua-500)' },
  soon:  { bg: 'var(--yellow-50)', fg: 'var(--yellow-700)', bd: 'var(--yellow-100)', dot: 'var(--yellow-500)' },
  alert: { bg: 'var(--alert-50)', fg: 'var(--alert-700)', bd: '#F8D3C5', dot: 'var(--alert-500)' },
  violet: { bg: 'var(--chem-50)', fg: '#5847d6', bd: '#DBD6FB', dot: 'var(--chem-violet)' },
  neutral: { bg: 'var(--ink-50)', fg: 'var(--ink-700)', bd: 'var(--ink-100)', dot: 'var(--ink-300)' },
};
function Chip({ children, tone = 'neutral', icon, pulse = false, style = {} }) {
  const s = CHIP_STYLE[tone] || CHIP_STYLE.neutral;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-body)', fontSize: 13, fontWeight: 700, letterSpacing: '-0.005em', padding: '5px 11px 5px 9px', borderRadius: 999, background: s.bg, color: s.fg, border: `1px solid ${s.bd}`, whiteSpace: 'nowrap', ...style }}>
      {icon ? <Icon name={icon} size={13} strokeWidth={2.4} /> : <span className={pulse ? 'tn-pulse' : ''} style={{ width: 7, height: 7, borderRadius: '50%', background: s.dot, flexShrink: 0 }} />}
      {children}
    </span>
  );
}

/* ─── Mono badge ────────────────────────────────────────────────────── */
function Mono({ children, tone = 'green', icon }) {
  const styles = {
    green: { color: 'var(--green-800)', background: 'var(--green-50)', border: '1px solid var(--green-100)' },
    blue: { color: 'var(--blue-700)', background: 'var(--blue-50)', border: '1px solid var(--blue-100)' },
    aqua: { color: 'var(--aqua-700)', background: 'var(--aqua-50)', border: '1px solid var(--aqua-100)' },
    ink: { color: 'var(--ink-700)', background: 'var(--ink-50)', border: '1px solid var(--ink-100)' },
  }[tone];
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.06em', textTransform: 'uppercase', borderRadius: 8, padding: '4px 8px', whiteSpace: 'nowrap', flexShrink: 0, ...styles }}>
      {icon && <Icon name={icon} size={12} strokeWidth={2.2} />}{children}
    </span>
  );
}

/* ─── Icon badge — white icon in a solid rounded square ─────────────── */
const BADGE_BG = { green: 'var(--green-500)', blue: 'var(--blue-500)', aqua: 'var(--aqua-500)', yellow: 'var(--yellow-500)', ink: 'var(--ink-900)', alert: 'var(--alert-500)', violet: 'var(--chem-violet)' };
function IconBadge({ icon, tone = 'green', size = 44, radius }) {
  const dark = tone === 'yellow';
  return (
    <span style={{ width: size, height: size, borderRadius: radius || Math.round(size * 0.3), background: BADGE_BG[tone], display: 'grid', placeItems: 'center', color: dark ? 'var(--ink-900)' : '#fff', flexShrink: 0 }}>
      <Icon name={icon} size={Math.round(size * 0.5)} strokeWidth={2.2} />
    </span>
  );
}

/* ─── Wallet rail chip ──────────────────────────────────────────────── */
const RAIL = {
  venmo: { bg: '#008CFF', mark: 'V', label: 'Venmo' },
  paypal: { bg: '#003087', mark: 'P', label: 'PayPal' },
  zelle: { bg: '#6D1ED4', mark: 'Z', label: 'Zelle' },
  cash: { bg: 'var(--green-700)', mark: '$', label: 'Cash' },
};
function RailChip({ rail, size = 28 }) {
  const r = RAIL[rail];
  return (
    <span style={{ width: size, height: size, borderRadius: Math.round(size * 0.3), background: r.bg, color: '#fff', display: 'grid', placeItems: 'center', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: Math.round(size * 0.52), flexShrink: 0 }}>{r.mark}</span>
  );
}

/* ─── Brand wallet buttons — real-wordmark pay rails ─────────────────────
   Approximated brand wordmarks/colors (real marks are trademarked): Venmo
   blue, PayPal yellow w/ two-tone italic logotype, Zelle purple.           */
const RAIL_BRAND = {
  venmo:  { bg: '#008CFF', fg: '#fff', handle: PROVIDER => PROVIDER.venmo },
  paypal: { bg: '#FFC439', fg: '#003087' },
  zelle:  { bg: '#6D1ED4', fg: '#fff' },
};
function RailWordmark({ rail, size = 19 }) {
  if (rail === 'paypal') {
    return (
      <span style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size, fontStyle: 'italic', letterSpacing: '-0.03em', lineHeight: 1 }}>
        <span style={{ color: '#003087' }}>Pay</span><span style={{ color: '#0070E0' }}>Pal</span>
      </span>
    );
  }
  const label = rail === 'venmo' ? 'venmo' : rail === 'zelle' ? 'zelle' : RAIL[rail].label;
  return <span style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size, letterSpacing: '-0.01em', color: '#fff', lineHeight: 1 }}>{label}</span>;
}
function RailPayButton({ rail, onClick, height = 54, size = 20 }) {
  const b = RAIL_BRAND[rail] || RAIL_BRAND.zelle;
  return (
    <button onClick={onClick} style={{ width: '100%', minHeight: height, border: 'none', borderRadius: 14, background: b.bg, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 9, boxShadow: 'var(--shadow-sm)', transition: 'transform .12s ease, box-shadow .12s ease' }}
      onMouseDown={e => e.currentTarget.style.transform = 'translateY(1px)'} onMouseUp={e => e.currentTarget.style.transform = 'none'} onMouseLeave={e => e.currentTarget.style.transform = 'none'}>
      <RailWordmark rail={rail} size={size} />
    </button>
  );
}

/* ─── Avatar — initials, deterministic warm color ───────────────────── */
const AV_COLORS = ['#21A84F', '#46B6F0', '#F2A03D', '#15803C', '#1379B5', '#E08A3C'];
function hashStr(s) { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return h >>> 0; }
function Avatar({ name, size = 40, ring = false }) {
  const initials = (name || '?').split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase();
  const c = AV_COLORS[hashStr(name || '') % AV_COLORS.length];
  return (
    <span style={{ width: size, height: size, borderRadius: '50%', background: c, color: '#fff', display: 'grid', placeItems: 'center', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: Math.round(size * 0.38), flexShrink: 0, border: ring ? '2.5px solid #fff' : 'none', boxShadow: ring ? '0 0 0 1.5px var(--ink-100)' : 'none' }}>{initials}</span>
  );
}

/* ─── Real generated sample photos (canvas-rendered) override gradients ── */
const SCENE_IMG = {
  'lawn-before': 'assets/photos/15_lawn_before.jpg',
  'lawn-after':  'assets/photos/16_lawn_after.jpg',
  'hedge':       'assets/scenes/hedge.png',
  'beds':        'assets/scenes/beds.png',
  'pool-clean':  'assets/photos/14_pool_after.jpg',
  'pool-green':  'assets/photos/13_pool_before.jpg',
  'pool-before': 'assets/photos/13_pool_before.jpg',
  'pool-after':  'assets/photos/14_pool_after.jpg',
  'pool-before-2': ['assets/photos/06_pool_before_after_diptych.jpg', 'left center'],
  'pool-after-2':  ['assets/photos/06_pool_before_after_diptych.jpg', 'right center'],
  'lawn-before-2': ['assets/photos/07_lawn_before_after_diptych.jpg', 'left center'],
  'lawn-after-2':  ['assets/photos/07_lawn_before_after_diptych.jpg', 'right center'],
};

/* ─── Photo tile — real sample photos w/ on-brand CSS fallback ──────────
   scene: lawn-before|lawn-after|pool-before|pool-after|hedge|beds|empty   */
const SCENES = {
  'lawn-before': { bg: 'linear-gradient(160deg,#7C8B4A,#5E6E36 55%,#4C5C2C)', tex: 'repeating-linear-gradient(120deg, rgba(0,0,0,.10) 0 7px, rgba(255,255,255,.05) 7px 14px)' },
  'lawn-after':  { bg: 'linear-gradient(160deg,#3FA85E,#2E8C49)', tex: 'repeating-linear-gradient(90deg, rgba(255,255,255,.16) 0 13px, rgba(0,0,0,.06) 13px 26px)' },
  'pool-before': { bg: 'linear-gradient(160deg,#5E8C76,#3F6E5C)', tex: 'radial-gradient(circle at 30% 30%, rgba(255,255,255,.14), transparent 45%)' },
  'pool-after':  { bg: 'linear-gradient(160deg,#56C2EE,#2C9BD6)', tex: 'radial-gradient(circle at 70% 25%, rgba(255,255,255,.4), transparent 40%), repeating-linear-gradient(115deg, rgba(255,255,255,.10) 0 10px, transparent 10px 22px)' },
  'pool-clean':  { bg: 'linear-gradient(160deg,#2FD0D8,#0FA4B0)', tex: 'radial-gradient(circle at 70% 25%, rgba(255,255,255,.45), transparent 40%), repeating-linear-gradient(115deg, rgba(255,255,255,.12) 0 9px, transparent 9px 20px)' },
  'pool-green':  { bg: 'linear-gradient(160deg,#6FA45E,#4E7A3F)', tex: 'radial-gradient(circle at 35% 35%, rgba(255,255,255,.12), transparent 50%)' },
  'test-strip':  { bg: 'linear-gradient(160deg,#1A1830,#0E0C1E)', tex: 'radial-gradient(circle at 50% 38%, rgba(124,111,240,.4), transparent 55%)' },
  'hedge':       { bg: 'linear-gradient(160deg,#2F9E55,#1E7B3F)', tex: 'repeating-linear-gradient(60deg, rgba(0,0,0,.10) 0 6px, rgba(255,255,255,.06) 6px 12px)' },
  'beds':        { bg: 'linear-gradient(160deg,#9A6B3C,#6E4A28)', tex: 'radial-gradient(circle at 60% 40%, rgba(33,168,79,.45), transparent 35%)' },
  'empty':       { bg: 'var(--ink-50)', tex: 'none' },
  'pool-before-2': { bg: 'linear-gradient(160deg,#5E8C76,#3F6E5C)', tex: 'none' },
  'pool-after-2':  { bg: 'linear-gradient(160deg,#56C2EE,#2C9BD6)', tex: 'none' },
  'lawn-before-2': { bg: 'linear-gradient(160deg,#7C8B4A,#4C5C2C)', tex: 'none' },
  'lawn-after-2':  { bg: 'linear-gradient(160deg,#3FA85E,#2E8C49)', tex: 'none' },
};
function PhotoTile({ scene = 'empty', label, time, height = 120, radius = 'var(--radius-lg)', style = {}, children, icon }) {
  const s = SCENES[scene] || SCENES.empty;
  const blank = scene === 'empty';
  const entry = SCENE_IMG[scene];
  const img = Array.isArray(entry) ? entry[0] : entry;
  const imgPos = Array.isArray(entry) ? entry[1] : 'center';
  const imgSize = Array.isArray(entry) ? '200% auto' : 'cover';
  return (
    <div style={{ position: 'relative', height, borderRadius: radius, overflow: 'hidden', background: s.bg, border: blank ? '1.5px dashed var(--ink-200)' : 'none', display: 'grid', placeItems: 'center', ...style }}>
      {img && <div style={{ position: 'absolute', inset: 0, backgroundImage: `url(${img})`, backgroundSize: imgSize, backgroundPosition: imgPos }} />}
      {!blank && !img && <div style={{ position: 'absolute', inset: 0, background: s.tex, mixBlendMode: 'soft-light' }} />}
      {blank && (
        <div style={{ display: 'grid', placeItems: 'center', gap: 6, color: 'var(--ink-300)' }}>
          <Icon name={icon || 'camera'} size={26} />
          {children && <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink-500)' }}>{children}</span>}
        </div>
      )}
      {label && (
        <span style={{ position: 'absolute', top: 8, left: 8, fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, letterSpacing: '.1em', color: '#fff', background: 'rgba(27,26,23,.55)', backdropFilter: 'blur(4px)', padding: '3px 7px', borderRadius: 6 }}>{label}</span>
      )}
      {time && (
        <span style={{ position: 'absolute', bottom: 8, right: 8, fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, color: '#fff', background: 'rgba(27,26,23,.45)', padding: '2px 6px', borderRadius: 6 }}>{time}</span>
      )}
    </div>
  );
}

/* ─── Mini static map — CSS-drawn neighborhood with a check-in pin ──── */
function MiniMap({ height = 150, radius = 'var(--radius-lg)', pin = true, label, style = {} }) {
  return (
    <div style={{ position: 'relative', height, borderRadius: radius, overflow: 'hidden', background: '#E8EEE6', ...style }}>
      {/* parcels */}
      <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(circle at 50% 45%, rgba(70,182,240,.12), transparent 60%)' }} />
      <svg width="100%" height="100%" viewBox="0 0 300 160" preserveAspectRatio="xMidYMid slice" style={{ position: 'absolute', inset: 0 }}>
        <rect width="300" height="160" fill="#E8EEE6" />
        {/* green spaces */}
        <rect x="18" y="14" width="70" height="46" rx="6" fill="#D6E7CE" />
        <rect x="214" y="98" width="74" height="52" rx="6" fill="#D6E7CE" />
        {/* roads */}
        <g fill="none" stroke="#fff" strokeLinejoin="round">
          <path d="M-10 96 H320" strokeWidth="14" />
          <path d="M150 -10 V170" strokeWidth="12" />
          <path d="M-10 40 H120 V170" strokeWidth="8" />
          <path d="M150 70 H320" strokeWidth="7" />
        </g>
        {/* blocks */}
        <g fill="#F3F1EA">
          <rect x="160" y="14" width="54" height="40" rx="4" /><rect x="232" y="14" width="50" height="40" rx="4" />
          <rect x="18" y="112" width="44" height="38" rx="4" /><rect x="74" y="112" width="50" height="38" rx="4" />
        </g>
      </svg>
      {pin && (
        <div style={{ position: 'absolute', left: '50%', top: '46%', transform: 'translate(-50%,-100%)' }}>
          <div style={{ width: 46, height: 46, borderRadius: '50%', background: 'rgba(70,182,240,.22)', display: 'grid', placeItems: 'center', transform: 'translateY(50%)' }}>
            <span style={{ width: 30, height: 30, borderRadius: '50%', background: 'var(--blue-500)', display: 'grid', placeItems: 'center', color: '#fff', border: '3px solid #fff', boxShadow: 'var(--shadow-md)' }}>
              <Icon name="pin" size={15} strokeWidth={2.4} />
            </span>
          </div>
        </div>
      )}
      {label && (
        <span style={{ position: 'absolute', bottom: 8, left: 8, fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, color: 'var(--ink-700)', background: 'rgba(255,255,255,.85)', padding: '3px 7px', borderRadius: 6 }}>{label}</span>
      )}
    </div>
  );
}

/* ─── Vertical helper — garden=green, pool=aqua ─────────────────────── */
function VERT(vertical) {
  return vertical === 'pool'
    ? { name: 'Pool', icon: 'droplet', tone: 'aqua', c500: 'var(--aqua-500)', c700: 'var(--aqua-700)', c50: 'var(--aqua-50)', c100: 'var(--aqua-100)' }
    : { name: 'Garden', icon: 'leaf', tone: 'green', c500: 'var(--green-500)', c700: 'var(--green-700)', c50: 'var(--green-50)', c100: 'var(--green-100)' };
}

/* ─── Chemistry reading definitions — ideal bands ───────────────────── */
const READINGS = {
  ph:   { label: 'pH',               abbr: 'pH',   unit: '',      min: 6.8, max: 8.2,  lo: 7.2,  hi: 7.6,  dp: 1, band: '7.2–7.6' },
  cl:   { label: 'Free Chlorine',    abbr: 'FC',   unit: 'ppm',   min: 0,   max: 5,    lo: 1.0,  hi: 3.0,  dp: 1, band: '1–3 ppm' },
  alk:  { label: 'Total Alkalinity', abbr: 'TA',   unit: 'ppm',   min: 40,  max: 180,  lo: 80,   hi: 120,  dp: 0, band: '80–120 ppm' },
  cya:  { label: 'CYA',              abbr: 'CYA',  unit: 'ppm',   min: 0,   max: 100,  lo: 30,   hi: 50,   dp: 0, band: '30–50 ppm' },
  salt: { label: 'Salt',             abbr: 'SALT', unit: 'ppm',   min: 0,   max: 5000, lo: 2700, hi: 3400, dp: 0, band: '2700–3400 ppm' },
  ch:   { label: 'Calcium Hardness', abbr: 'CH',   unit: 'ppm',   min: 0,   max: 600,  lo: 200,  hi: 400,  dp: 0, band: '200–400 ppm' },
  temp: { label: 'Water Temp',       abbr: 'TEMP', unit: '°F',    min: 50,  max: 100,  lo: 78,   hi: 88,   dp: 0, band: '78–88°F', soft: true },
};
function chemState(key, val) {
  const r = READINGS[key];
  if (val == null || isNaN(val)) return 'empty';
  if (val < r.lo) return 'low';
  if (val > r.hi) return 'high';
  return 'ideal';
}
/* Langelier Saturation Index — credible pool-balance estimate.
   LSI = pH + TF + CF + AF − 12.1, with industry factor approximations. */
function computeLSI({ ph, temp = 80, ch = 300, alk = 100 }) {
  if (ph == null) return null;
  const tf = Math.max(0, Math.min(0.9, 0.0117 * temp - 0.4));     // temperature factor
  const cf = Math.log10(Math.max(25, ch)) - 0.4;                   // calcium factor
  const af = Math.log10(Math.max(25, alk));                        // alkalinity factor
  return ph + tf + cf + af - 12.1;
}
function lsiVerdict(lsi) {
  if (lsi == null) return { key: 'unknown', label: 'Not enough data', tone: 'ink' };
  if (lsi < -0.3) return { key: 'corrosive', label: 'Slightly corrosive', tone: 'soon' };
  if (lsi > 0.3)  return { key: 'scaling',   label: 'Scaling',           tone: 'violet' };
  return { key: 'balanced', label: 'Balanced', tone: 'done' };
}
/* Dosing suggestions for out-of-range readings (≈15,000 gal pool). Estimates. */
function dosingFor(readings) {
  const out = [];
  const { ph, cl, alk } = readings;
  if (ph != null && ph < READINGS.ph.lo) out.push({ key: 'ph', text: 'Add ~2 lb soda ash to raise pH', why: `pH ${ph.toFixed(1)} · below ${READINGS.ph.lo}` });
  else if (ph != null && ph > READINGS.ph.hi) out.push({ key: 'ph', text: 'Add ~12 oz muriatic acid to lower pH', why: `pH ${ph.toFixed(1)} · above ${READINGS.ph.hi}` });
  if (cl != null && cl < READINGS.cl.lo) out.push({ key: 'cl', text: 'Add ~16 oz liquid chlorine', why: `Free chlorine ${cl.toFixed(1)} · below ${READINGS.cl.lo} ppm` });
  if (alk != null && alk < READINGS.alk.lo) out.push({ key: 'alk', text: 'Add ~4 lb sodium bicarb to raise alkalinity', why: `Alkalinity ${alk} · below ${READINGS.alk.lo} ppm` });
  else if (alk != null && alk > READINGS.alk.hi) out.push({ key: 'alk', text: 'Add ~1 qt muriatic acid to lower alkalinity', why: `Alkalinity ${alk} · above ${READINGS.alk.hi} ppm` });
  return out;
}

/* ─── pH gauge — horizontal scale, ideal band, value marker ─────────── */
function PhGauge({ value = 7.4, height = 14 }) {
  const r = READINGS.ph;
  const pct = (v) => Math.max(0, Math.min(100, ((v - r.min) / (r.max - r.min)) * 100));
  const state = chemState('ph', value);
  const col = state === 'ideal' ? 'var(--ph-ideal)' : state === 'low' ? 'var(--ph-low)' : 'var(--ph-high)';
  return (
    <div style={{ paddingTop: 6 }}>
      <div style={{ position: 'relative', height, borderRadius: 99, overflow: 'hidden', background: 'linear-gradient(90deg, #F2683C 0%, #F2A03D 26%, #21A84F 46%, #21A84F 60%, #46B6F0 78%, #7C6FF0 100%)' }}>
        {/* ideal band overlay markers */}
        <span style={{ position: 'absolute', top: 0, bottom: 0, left: `${pct(r.lo)}%`, width: 1.5, background: 'rgba(255,255,255,.85)' }} />
        <span style={{ position: 'absolute', top: 0, bottom: 0, left: `${pct(r.hi)}%`, width: 1.5, background: 'rgba(255,255,255,.85)' }} />
      </div>
      {/* marker */}
      <div style={{ position: 'relative', height: 0 }}>
        <div style={{ position: 'absolute', left: `${pct(value)}%`, top: -height - 4, transform: 'translateX(-50%)', display: 'grid', justifyItems: 'center' }}>
          <span style={{ background: col, color: '#fff', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 12, padding: '2px 8px', borderRadius: 7, boxShadow: 'var(--shadow-md)', whiteSpace: 'nowrap' }}>{value.toFixed(1)}</span>
          <span style={{ width: 0, height: 0, borderLeft: '5px solid transparent', borderRight: '5px solid transparent', borderTop: `6px solid ${col}` }} />
        </div>
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8, fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--ink-300)', letterSpacing: '.04em' }}>
        <span>6.8 ACID</span><span style={{ color: 'var(--green-700)', fontWeight: 600 }}>7.2–7.6 IDEAL</span><span>8.2 BASIC</span>
      </div>
    </div>
  );
}

/* ─── pH badge — a compact, color-coded pH reading icon (droplet + "pH") ──
   Replaces the dark test-strip thumbnail in reading-history rows. Tint encodes
   in-range (green) / low (amber) / high (violet).                          */
const PH_TONE = {
  ideal: { bg: 'var(--green-50)', fg: 'var(--green-700)' },
  low:   { bg: 'var(--yellow-50)', fg: 'var(--yellow-700)' },
  high:  { bg: 'var(--chem-50)', fg: '#5847d6' },
};
function PhBadge({ value = 7.4, size = 42 }) {
  const t = PH_TONE[chemState('ph', value)] || PH_TONE.ideal;
  return (
    <span style={{ width: size, height: size, borderRadius: Math.round(size * 0.28), background: t.bg, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 1, flexShrink: 0 }} aria-hidden="true">
      <Icon name="droplet" size={Math.round(size * 0.44)} strokeWidth={2.2} style={{ color: t.fg }} />
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: Math.max(7.5, size * 0.2), fontWeight: 700, letterSpacing: '.06em', color: t.fg, lineHeight: 1 }}>pH</span>
    </span>
  );
}

/* ─── Chem readout — a single reading row (label, value, state pill) ── */
function ChemReadout({ rkey, value, sub }) {
  const r = READINGS[rkey];
  const state = chemState(rkey, value);
  const tone = state === 'ideal' ? 'done' : state === 'low' ? 'soon' : 'violet';
  const txt = state === 'ideal' ? 'In range' : state === 'low' ? 'Low' : 'High';
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 0' }}>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 13.5, fontWeight: 700 }}>{r.label}</div>
        {sub && <div style={{ fontSize: 11.5, color: 'var(--ink-500)' }}>{sub}</div>}
      </div>
      <span style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 18, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>
        {value.toFixed(r.dp)}{r.unit ? <span style={{ fontSize: 11, color: 'var(--ink-500)', fontWeight: 600 }}> {r.unit}</span> : null}
      </span>
      <Chip tone={tone}>{txt}</Chip>
    </div>
  );
}

/* ─── Chem trend — small SVG area/line chart of a reading over time ─── */
function ChemTrend({ points = [7.6, 7.5, 7.2, 7.8, 7.4, 7.5], labels = [], height = 92, ideal = [7.2, 7.6], rangeMin = 6.8, rangeMax = 8.2, color = 'var(--aqua-500)' }) {
  const W = 300, H = height, pad = 8;
  const n = points.length;
  const x = (i) => pad + (i / (n - 1)) * (W - pad * 2);
  const y = (v) => H - pad - ((v - rangeMin) / (rangeMax - rangeMin)) * (H - pad * 2);
  const line = points.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(' ');
  const area = `${line} L${x(n - 1)} ${H - pad} L${x(0)} ${H - pad} Z`;
  return (
    <div>
      <svg width="100%" viewBox={`0 0 ${W} ${H}`} style={{ display: 'block' }}>
        {/* ideal band */}
        <rect x={pad} y={y(ideal[1])} width={W - pad * 2} height={y(ideal[0]) - y(ideal[1])} fill="var(--green-500)" opacity="0.10" />
        <line x1={pad} x2={W - pad} y1={y(ideal[1])} y2={y(ideal[1])} stroke="var(--green-300)" strokeWidth="1" strokeDasharray="3 4" />
        <line x1={pad} x2={W - pad} y1={y(ideal[0])} y2={y(ideal[0])} stroke="var(--green-300)" strokeWidth="1" strokeDasharray="3 4" />
        <path d={area} fill={color} opacity="0.12" />
        <path d={line} fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
        {points.map((v, i) => {
          const st = v < ideal[0] ? 'var(--ph-low)' : v > ideal[1] ? 'var(--ph-high)' : 'var(--green-500)';
          return <circle key={i} cx={x(i)} cy={y(v)} r={i === n - 1 ? 4.5 : 3} fill="#fff" stroke={st} strokeWidth="2.5" />;
        })}
      </svg>
      {labels.length > 0 && (
        <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4, padding: '0 4px', fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--ink-400,#A8A498)' }}>
          {labels.map((l, i) => <span key={i}>{l}</span>)}
        </div>
      )}
    </div>
  );
}

Object.assign(window, {
  Icon, TN_ICONS, fmtMoney, Amount, Sprout, Dewy, Mascot, Logo, Tag, SectionHead, HandUnderline, Chip, Mono, IconBadge, RailChip, Avatar, PhotoTile, SCENE_IMG, MiniMap, RAIL, hashStr,
  VERT, PhGauge, PhBadge, ChemReadout, ChemTrend, READINGS, chemState, computeLSI, lsiVerdict, dosingFor, RAIL_BRAND, RailWordmark, RailPayButton,
});
