Chem.Academy
  • Home
  • Book
  • Notes
  • Skill Hub
  • Quick Reference
  • Periodic Table

Welcome! This online textbook is a living project.
Content is being added and refined weekly as we build a complete resource for General Chemistry I & II. Thank you for visiting!

  • Introduction
  • Chemistry I
    • 1   Core Concepts
      • Math Review
      • Significant Figures
      • Dimensional Analysis
      • Foundations of Chemistry
      • Matter and Its Properties
      • Review Problems
    • 2   Atoms and the Periodic Table
      • The Atom
      • The Electron
      • The Nucleus
      • The Periodic Table
      • Isotopes and Percent Abundance
      • Ions
      • Review Problems
    • 3   Chemical Nomenclature
      • Representing Particles
      • Naming Ions and Ionic Compounds
      • Naming Molecules
      • Skill Drill: Nomenclature
      • Review Problems
    • 4   Chemical Reactions
      • The Mole
      • Percent Composition by Mass
      • Reaction Basics and Solubility
      • Acids and Bases
      • Oxidation-Reduction Reactions
      • Skill Drill: Solubility Rules
      • Skill Drill: Acids and Bases
      • Review Problems
    • 5   Stoichiometry
      • Stoichiometry
      • Concentration of Solutions
      • Learning Lab: Solution Builder
      • Learning Lab: Solution Mixer
      • Review Problems
    • 6   Thermochemistry
      • Basics
      • Heat
      • Work
      • Review Problems
  • Chemistry II
    • Thermodynamics
// =============================================================================
// 1. DATA (with Corrected Colors)
// =============================================================================
solutes = [
  {
    "group": "Soluble Ionic Compounds",
    "compounds": [
      // --- Primary Input Reactants ---
      { "name": "cobalt(II) chloride", "formula": "CoCl<sub>2</sub>", "molarMass": 129.84, "isSoluble": true, "solubilityLimit": 52.9, "color": "#e91e63", "ions": [{ "formula": "Co<sup>2+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "copper(II) sulfate", "formula": "CuSO<sub>4</sub>", "molarMass": 159.61, "isSoluble": true, "solubilityLimit": 22.0, "color": "#3498db", "ions": [{ "formula": "Cu<sup>2+</sup>", "ratio": 1 }, { "formula": "SO<sub>4</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "lead(II) nitrate", "formula": "Pb(NO<sub>3</sub>)<sub>2</sub>", "molarMass": 331.21, "isSoluble": true, "solubilityLimit": 52.0, "color": "#ecf0f1", "ions": [{ "formula": "Pb<sup>2+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "magnesium chloride", "formula": "MgCl<sub>2</sub>", "molarMass": 95.21, "isSoluble": true, "solubilityLimit": 54.3, "color": "#ecf0f1", "ions": [{ "formula": "Mg<sup>2+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "nickel(II) chloride", "formula": "NiCl<sub>2</sub>", "molarMass": 129.60, "isSoluble": true, "solubilityLimit": 64.2, "color": "#2ecc71", "ions": [{ "formula": "Ni<sup>2+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "potassium iodide", "formula": "KI", "molarMass": 166.00, "isSoluble": true, "solubilityLimit": 144.0, "color": "#ecf0f1", "ions": [{ "formula": "K<sup>+</sup>", "ratio": 1 }, { "formula": "I<sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "potassium permanganate", "formula": "KMnO<sub>4</sub>", "molarMass": 158.03, "isSoluble": true, "solubilityLimit": 7.6, "color": "#8e44ad", "ions": [{ "formula": "K<sup>+</sup>", "ratio": 1 }, { "formula": "MnO<sub>4</sub><sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "silver nitrate", "formula": "AgNO<sub>3</sub>", "molarMass": 169.87, "isSoluble": true, "solubilityLimit": 256.0, "color": "#ecf0f1", "ions": [{ "formula": "Ag<sup>+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "sodium carbonate", "formula": "Na<sub>2</sub>CO<sub>3</sub>", "molarMass": 105.99, "isSoluble": true, "solubilityLimit": 30.7, "color": "#ecf0f1", "ions": [{ "formula": "Na<sup>+</sup>", "ratio": 2 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "sodium chloride", "formula": "NaCl", "molarMass": 58.44, "isSoluble": true, "solubilityLimit": 36.0, "color": "#ecf0f1", "ions": [{ "formula": "Na<sup>+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "sodium phosphate", "formula": "Na<sub>3</sub>PO<sub>4</sub>", "molarMass": 163.94, "isSoluble": true, "solubilityLimit": 12.1, "color": "#ecf0f1", "ions": [{ "formula": "Na<sup>+</sup>", "ratio": 3 }, { "formula": "PO<sub>4</sub><sup>3&minus;</sup>", "ratio": 1 }] },

      // --- Soluble Reaction Products ---
      { "name": "barium nitrate", "formula": "Ba(NO<sub>3</sub>)<sub>2</sub>", "molarMass": 261.34, "isSoluble": true, "solubilityLimit": 10.2, "color": "#ecf0f1", "ions": [{ "formula": "Ba<sup>2+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "cobalt(II) nitrate", "formula": "Co(NO<sub>3</sub>)<sub>2</sub>", "molarMass": 182.94, "isSoluble": true, "solubilityLimit": 101.9, "color": "#e91e63", "ions": [{ "formula": "Co<sup>2+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "copper(II) nitrate", "formula": "Cu(NO<sub>3</sub>)<sub>2</sub>", "molarMass": 187.56, "isSoluble": true, "solubilityLimit": 137.0, "color": "#3498db", "ions": [{ "formula": "Cu<sup>2+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "magnesium nitrate", "formula": "Mg(NO<sub>3</sub>)<sub>2</sub>", "molarMass": 148.32, "isSoluble": true, "solubilityLimit": 71.2, "color": "#ecf0f1", "ions": [{ "formula": "Mg<sup>2+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "nickel(II) nitrate", "formula": "Ni(NO<sub>3</sub>)<sub>2</sub>", "molarMass": 182.70, "isSoluble": true, "solubilityLimit": 238.5, "color": "#2ecc71", "ions": [{ "formula": "Ni<sup>2+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "potassium chloride", "formula": "KCl", "molarMass": 74.55, "isSoluble": true, "solubilityLimit": 36.0, "color": "#ecf0f1", "ions": [{ "formula": "K<sup>+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "potassium nitrate", "formula": "KNO<sub>3</sub>", "molarMass": 101.10, "isSoluble": true, "solubilityLimit": 38.3, "color": "#ecf0f1", "ions": [{ "formula": "K<sup>+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "sodium iodide", "formula": "NaI", "molarMass": 149.89, "isSoluble": true, "solubilityLimit": 184.0, "color": "#ecf0f1", "ions": [{ "formula": "Na<sup>+</sup>", "ratio": 1 }, { "formula": "I<sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "sodium nitrate", "formula": "NaNO<sub>3</sub>", "molarMass": 85.00, "isSoluble": true, "solubilityLimit": 91.2, "color": "#ecf0f1", "ions": [{ "formula": "Na<sup>+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "sodium sulfate", "formula": "Na<sub>2</sub>SO<sub>4</sub>", "molarMass": 142.04, "isSoluble": true, "solubilityLimit": 28.2, "color": "#ecf0f1", "ions": [{ "formula": "Na<sup>+</sup>", "ratio": 2 }, { "formula": "SO<sub>4</sub><sup>2&minus;</sup>", "ratio": 1 }] }
    ]
  },
  {
    "group": "Acids & Bases",
    "compounds": [
      { "name": "barium hydroxide (strong)", "formula": "Ba(OH)<sub>2</sub>", "molarMass": 171.34, "isSoluble": true, "solubilityLimit": 3.89, "color": "#ecf0f1", "ions": [{ "formula": "Ba<sup>2+</sup>", "ratio": 1 }, { "formula": "OH<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "hydrochloric acid (strong)", "formula": "HCl", "molarMass": 36.46, "isSoluble": true, "solubilityLimit": "Infinity", "color": "#ecf0f1", "ions": [{ "formula": "H<sup>+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "nitric acid (strong)", "formula": "HNO<sub>3</sub>", "molarMass": 63.01, "isSoluble": true, "solubilityLimit": "Infinity", "color": "#ecf0f1", "ions": [{ "formula": "H<sup>+</sup>", "ratio": 1 }, { "formula": "NO<sub>3</sub><sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "sodium hydroxide (strong)", "formula": "NaOH", "molarMass": 40.00, "isSoluble": true, "solubilityLimit": 129.0, "color": "#ecf0f1", "ions": [{ "formula": "Na<sup>+</sup>", "ratio": 1 }, { "formula": "OH<sup>&minus;</sup>", "ratio": 1 }] }
    ]
  },
  {
    "group": "Insoluble Compounds",
    "compounds": [
      { "name": "barium carbonate", "formula": "BaCO<sub>3</sub>", "molarMass": 197.34, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Ba<sup>2+</sup>", "ratio": 1 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "barium phosphate", "formula": "Ba<sub>3</sub>(PO<sub>4</sub>)<sub>2</sub>", "molarMass": 601.93, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Ba<sup>2+</sup>", "ratio": 3 }, { "formula": "PO<sub>4</sub><sup>3&minus;</sup>", "ratio": 2 }] },
      { "name": "barium sulfate", "formula": "BaSO<sub>4</sub>", "molarMass": 233.39, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Ba<sup>2+</sup>", "ratio": 1 }, { "formula": "SO<sub>4</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "cobalt(II) carbonate", "formula": "CoCO<sub>3</sub>", "molarMass": 118.94, "isSoluble": false, "solubilityLimit": 0.0, "color": "#e91e63", "ions": [{ "formula": "Co<sup>2+</sup>", "ratio": 1 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "cobalt(II) hydroxide", "formula": "Co(OH)<sub>2</sub>", "molarMass": 92.95, "isSoluble": false, "solubilityLimit": 0.0, "color": "#e91e63", "ions": [{ "formula": "Co<sup>2+</sup>", "ratio": 1 }, { "formula": "OH<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "copper(II) carbonate", "formula": "CuCO<sub>3</sub>", "molarMass": 123.55, "isSoluble": false, "solubilityLimit": 0.0, "color": "#3498db", "ions": [{ "formula": "Cu<sup>2+</sup>", "ratio": 1 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "copper(II) hydroxide", "formula": "Cu(OH)<sub>2</sub>", "molarMass": 97.56, "isSoluble": false, "solubilityLimit": 0.0, "color": "#3498db", "ions": [{ "formula": "Cu<sup>2+</sup>", "ratio": 1 }, { "formula": "OH<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "copper(II) phosphate", "formula": "Cu<sub>3</sub>(PO<sub>4</sub>)<sub>2</sub>", "molarMass": 380.58, "isSoluble": false, "solubilityLimit": 0.0, "color": "#3498db", "ions": [{ "formula": "Cu<sup>2+</sup>", "ratio": 3 }, { "formula": "PO<sub>4</sub><sup>3&minus;</sup>", "ratio": 2 }] },
      { "name": "lead(II) carbonate", "formula": "PbCO<sub>3</sub>", "molarMass": 267.21, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Pb<sup>2+</sup>", "ratio": 1 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "lead(II) chloride", "formula": "PbCl<sub>2</sub>", "molarMass": 278.10, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Pb<sup>2+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "lead(II) hydroxide", "formula": "Pb(OH)<sub>2</sub>", "molarMass": 241.21, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Pb<sup>2+</sup>", "ratio": 1 }, { "formula": "OH<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "lead(II) iodide", "formula": "PbI<sub>2</sub>", "molarMass": 461.01, "isSoluble": false, "solubilityLimit": 0.0, "color": "#f1c40f", "ions": [{ "formula": "Pb<sup>2+</sup>", "ratio": 1 }, { "formula": "I<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "lead(II) phosphate", "formula": "Pb<sub>3</sub>(PO<sub>4</sub>)<sub>2</sub>", "molarMass": 811.54, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Pb<sup>2+</sup>", "ratio": 3 }, { "formula": "PO<sub>4</sub><sup>3&minus;</sup>", "ratio": 2 }] },
      { "name": "lead(II) sulfate", "formula": "PbSO<sub>4</sub>", "molarMass": 303.26, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Pb<sup>2+</sup>", "ratio": 1 }, { "formula": "SO<sub>4</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "magnesium carbonate", "formula": "MgCO<sub>3</sub>", "molarMass": 84.31, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Mg<sup>2+</sup>", "ratio": 1 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "magnesium hydroxide", "formula": "Mg(OH)<sub>2</sub>", "molarMass": 58.32, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Mg<sup>2+</sup>", "ratio": 1 }, { "formula": "OH<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "nickel(II) carbonate", "formula": "NiCO<sub>3</sub>", "molarMass": 118.70, "isSoluble": false, "solubilityLimit": 0.0, "color": "#2ecc71", "ions": [{ "formula": "Ni<sup>2+</sup>", "ratio": 1 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "nickel(II) hydroxide", "formula": "Ni(OH)<sub>2</sub>", "molarMass": 92.71, "isSoluble": false, "solubilityLimit": 0.0, "color": "#2ecc71", "ions": [{ "formula": "Ni<sup>2+</sup>", "ratio": 1 }, { "formula": "OH<sup>&minus;</sup>", "ratio": 2 }] },
      { "name": "silver carbonate", "formula": "Ag<sub>2</sub>CO<sub>3</sub>", "molarMass": 275.75, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Ag<sup>+</sup>", "ratio": 2 }, { "formula": "CO<sub>3</sub><sup>2&minus;</sup>", "ratio": 1 }] },
      { "name": "silver chloride", "formula": "AgCl", "molarMass": 143.32, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Ag<sup>+</sup>", "ratio": 1 }, { "formula": "Cl<sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "silver iodide", "formula": "AgI", "molarMass": 234.77, "isSoluble": false, "solubilityLimit": 0.0, "color": "#f1c40f", "ions": [{ "formula": "Ag<sup>+</sup>", "ratio": 1 }, { "formula": "I<sup>&minus;</sup>", "ratio": 1 }] },
      { "name": "silver phosphate", "formula": "Ag<sub>3</sub>PO<sub>4</sub>", "molarMass": 418.58, "isSoluble": false, "solubilityLimit": 0.0, "color": "#f1c40f", "ions": [{ "formula": "Ag<sup>+</sup>", "ratio": 3 }, { "formula": "PO<sub>4</sub><sup>3&minus;</sup>", "ratio": 1 }] },
      { "name": "silver sulfate", "formula": "Ag<sub>2</sub>SO<sub>4</sub>", "molarMass": 311.80, "isSoluble": false, "solubilityLimit": 0.0, "color": "#bdc3c7", "ions": [{ "formula": "Ag<sup>+</sup>", "ratio": 2 }, { "formula": "SO<sub>4</sub><sup>2&minus;</sup>", "ratio": 1 }] }
    ]
  },
  {
    "group": "Molecular Compounds",
    "compounds": [
      { "name": "glucose", "formula": "C<sub>6</sub>H<sub>12</sub>O<sub>6</sub>", "molarMass": 180.16, "isSoluble": true, "solubilityLimit": 91.0, "color": "#ecf0f1", "ions": [] },
      { "name": "sucrose (sugar)", "formula": "C<sub>12</sub>H<sub>22</sub>O<sub>11</sub>", "molarMass": 342.30, "isSoluble": true, "solubilityLimit": 204.0, "color": "#ecf0f1", "ions": [] }
    ]
  }
]
// =============================================================================
// 1.5 DYNAMIC MATHJAX LOADER
// =============================================================================
mathjax_dependency = {
  if (window.our_mathjax_loaded) {
    return Promise.resolve("MathJax already loaded.");
  }

  return new Promise(resolve => {
    window.MathJax = {
      loader: {
        load: ['[tex]/html']
      },
      tex: {
        packages: {'[+]': ['html']}
      },
      chtml: {
        scale: 0.8
      },
      startup: {
        ready: () => {
          console.log("Dynamic MathJax is ready with custom \\molar command.");
          window.MathJax.startup.defaultReady();
          window.our_mathjax_loaded = true;
          resolve("MathJax is now ready.");
        }
      }
    };

    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js';
    script.async = true;
    document.head.appendChild(script);
  });
}
// =============================================================================
// 2. CSS STYLING for Precipitation Lab
// =============================================================================
precipitationLabStyles = htl.html`<style>
    .precip-lab-container {
      display: grid;
      grid-template-columns: 1fr auto 1fr; /* Let the middle column shrink-to-fit */
        grid-template-areas:
        "title-area title-area title-area"   /* NEW: Row for the main title */
        "panel-a    mix-panel panel-b"      /* Row 1: Inputs */
        "output-a   .         output-b"      /* Row 2: Initial Outputs */
        "output-mix output-mix output-mix";  /* Row 3: Final Mixture */
      gap: 20px;
      font-family: sans-serif;
      max-width: 1100px;
      margin: auto;
      padding: 20px 20px 5px 20px;
      border: 1px solid #ccc;
      border-radius: 8px;
    }
    .widget-title {
      grid-area: title-area;
      text-align: center;
      font-size: 1.5em;
      font-weight: 600;
      color: #2c3e50;
      margin-bottom: 15px; /* Adds space below the title */
      padding-bottom: 10px;
      border-bottom: 1px solid #e0e0e0;
    }
    .widget-subtitle {
      font-size: 0.7rem;
      font-style: italic;
      font-weight: 400; /* Resets bolding from parent */
      color: #7f8c8d;   /* Same color as the solute info */
      margin-top: 4px;   /* Adds a little space below the main title */
    }
    .saturation-alert {
      margin-top: 10px;
      padding: 8px 12px;
      border: 1px solid #f39c12;
      background-color: #fef9e7;
      border-radius: 5px;
      color: #b7791f;
      font-size: 0.9rem;
      text-align: center;
    }
    .reaction-warning-panel {
      max-width: 550px; /* Match the width of the final output */
      width: 100%;
      margin-top: 15px;
      padding: 15px;
      border: 1px solid #c0392b; /* Red border */
      background-color: #fdedec; /* Light red background */
      border-radius: 5px;
      text-align: center;
    }
    .reaction-warning-panel h4 {
      margin-top: 0;
      color: #c0392b;
    }
    .reaction-warning-panel p {
      margin-bottom: 0;
      color: #78281f;
    }
    .beaker-container {
      width: 100%; /* Make container as wide as its parent */
      display: flex;
      flex-direction: column;
      align-items: center; /* Center the h4 and svg inside */
    }
    .unit-molar {
      font-variant-caps: small-caps;
      font-size: 0.86em !important;
      font-weight: 500;
    }
    b .unit-molar {
      font-weight: 700; /* '700' is the numerical equivalent of 'bold' */
    }
    .mjx-molar mjx-c {
      font-size: 0.80em !important;
      font-weight: 500 !important;
    }
    .dissolution-equation {
      text-align: center;
      margin-top: 8px;
      margin-bottom: 12px;
      line-height: 1.4; /* Set a consistent line height */
      min-height: 1.4em; /* Guarantee a minimum height even if empty */
    }
    .mjx-molar mjx-mtext {
      vertical-align: bottom !important;
    }
    .input-panel {
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 15px;
      background-color: #f9f9f9;
      display: flex;
      flex-direction: column;
      gap: 5px;
    }
    #panel-a { grid-area: panel-a; }
    #panel-b { grid-area: panel-b; }
    #mix-panel {
      grid-area: mix-panel;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 15px;
    }
    /* This single rule now correctly styles all output elements */
    .solution-output {
      justify-self: center; 
      width: 100%;
      max-width: 100%;
      
      display: flex;
      flex-direction: column;
      align-items: center;
    
      &#solution-a-output { grid-area: output-a; }
      &#solution-b-output { grid-area: output-b; }
      &#final-mixture-output { 
        grid-area: output-mix;
        max-width: 550px;
      }
    }
    .solution-output h4 {
      text-align: center;
      font-size: 1.10em; /* You can adjust this value as needed */
      font-weight: 700;  /* 700 is the numeric equivalent of 'bold' */
    }
    .initial-output-wrapper {
      width: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 20px;
    }
    .beaker-container {
      width: 100%; /* Make container as wide as its parent */
      display: flex;
      flex-direction: column;
      align-items: center; /* Center the h4 and svg inside */
    }
    #mix-button.mix-button-clean {
      background-color: #2ecc71; /* A nice green */
      border-color: #27ae60;
    }

    #mix-button.mix-button-clean:hover {
      background-color: #27ae60;
    }
    .calculation-panel {
      width: 100%;
      margin-top: 15px;
      padding: 15px 30px;
      border: 1px solid #e0e0e0;
      border-radius: 5px;
      background-color: #fdfdfd;
      text-align: center; /* This will center the math block */
    }

    .panel-title { 
      margin-top: 0; 
      margin-bottom: 0;
      color: #333; 
      border-bottom: 2px solid #3498db; 
      padding-bottom: 5px; 
    }
    .form-group { display: flex; flex-direction: column; gap: 5px; }
    .input-unit {
      white-space: nowrap;
    }
    .solute-info {
      margin-top: 5px;
      padding: 8px;
      background-color: #ecf0f1;
      border-radius: 4px;
      font-size: 0.85rem;
      color: #7f8c8d;
    }
    .input-element { width: 100%; box-sizing: border-box; }
    .input-element { padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
    .radio-group { display: flex; gap: 10px; align-items: center; }
    .input-with-unit { display: flex; align-items: center; gap: 5px; }
    #mix-button {
      width: 100px; height: 100px; border-radius: 50%; font-size: 18px; font-weight: bold;
      background-color: #e74c3c; border: 3px solid #c0392b;
    }
    #mix-button:hover { background-color: #c0392b; }
    #mix-button:disabled { background-color: #bdc3c7; border-color: #95a5a6; cursor: not-allowed; }

    .results-table {
      min-width: 300px; /* Give the table a sensible minimum width */
      border-collapse: collapse; /* Ensures padding is respected */
    }
    .results-table th, .results-table td {
      white-space: nowrap; /* Prevent all cells from wrapping */
      padding-right: 1.5em; /* Add consistent padding to ALL cells */
      text-align: left; /* Align all cells to the left */
    }
    .results-table th {
      font-weight: bold; /* Ensure headers are bold */
    }

    .placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: #aaa; text-align: center; }

    .equation-block {
      width: 100%;
      margin-top: 15px;
      padding: 10px 25px 5px 25px;
      text-align: center;
      font-family: sans-serif;
    }
    .molecular-equation {
      margin-bottom: 1em; /* Controls space BETWEEN equations */
    }
    .molecular-equation p {
      margin: 0.2em 0; /* Reduces space around the equation text itself */
    }
    .equation-products {
      display: block; /* Forces the products onto a new line */
      margin-left: 3em;  /* Indents the products */
      text-align: left; /* Aligns the indented line to the left */
    }
    .calculation-details {
      width: 100%;
      margin-top: 10px;
      border: 1px solid #e0e0e0;
      border-radius: 5px;
      background-color: #fdfdfd;
      position: relative;
      z-index: 1;
    }
    body > div[style*="position: absolute"][style*="z-index"] {
      z-index: 10000 !important;
    }
    .calculation-panel-content {
      padding: 5px 30px; /* 15px top/bottom, 30px left/right */
      text-align: center;
      overflow-x: auto;
    }
    .calculation-details summary {
      font-size: 0.9em;
      padding: 10px 15px;
      font-weight: bold;
      cursor: pointer;
      outline: none;
      list-style-position: inside;
    }
    .calculation-details[open] > summary {
      border-bottom: 1px solid #e0e0e0;
    }
    /* The .calculation-panel is now just for padding and centering the math */
    .calculation-panel {
      padding: 15px;
      text-align: center;
    }
    #solution-a-output .calculation-panel,
    #solution-b-output .calculation-panel {
      padding: 15px 40px; /* Increase left/right padding here */
    }
    mjx-container {
      position: relative;
      z-index: 10000 !important;
    }
    @media (max-width: 900px) {
          .precip-lab-container {
            /* Switch to a single column layout */
            grid-template-columns: 1fr;
            /* Define a new stacking order for all elements */
            grid-template-areas:
              "title-area"
              "panel-a"
              "panel-b"
              "mix-panel"
              "output-a"
              "output-b"
              "output-mix";
          }
    
          /* To improve spacing on mobile, let's remove the fixed width
             of the mix button and allow it to be more flexible */
          #mix-panel {
            padding: 20px 0;
          }
          #mix-button {
              width: auto;
              height: auto;
              padding: 15px 30px;
              border-radius: 8px;
          }
    }
</style>`
// =============================================================================
// 1. REACTIVE STATE for the Mixing Lab
// =============================================================================
mutable appState = ({
  solutionA: {
    // User Input State
    soluteFormula: "NaCl",
    userInputType: 'mass',
    userInputAmount: 1.00,
    userInputVolume: 100.0,
    // Calculated State (populated after mixing)
    isPrepared: false,
    solute: null,
    concentration: null,
    moles: null,
    volume: null,
    ions: [],
    latexInitialCalc: null
  },
  solutionB: {
    // User Input State
    soluteFormula: "Pb(NO<sub>3</sub>)<sub>2</sub>",
    userInputType: 'mass',
    userInputAmount: 1.00,
    userInputVolume: 100.0,
    // Calculated State
    isPrepared: false,
    solute: null,
    concentration: null,
    moles: null,
    volume: null,
    ions: [],
    latexInitialCalc: null
  },
  mixture: {
    isMixed: false,
    isWarningState: false,
    warningMessage: null,
    precipitates: [],
    limitingReactant: null,
    neutralizationLR: null,
    finalContents: [],
    final_pH: null,
    molecularEquation: null,
    completeIonicEquation: null,
    netIonicEquation: null,
    latexPrecipitateCalc: null,
    latexExcessReactantCalc: null,
    latexNeutralizationCalc: null,
    latexIonCalc: null
  }
});
// =============================================================================
// 5: JAVASCRIPT LOGIC & RENDERER (FINAL, COMPLETE ARCHITECTURE)
// =============================================================================
{
  // --- DEPENDENCIES ---
  mathjax_dependency;
  precipitationLabStyles;
  appState;

  const allCompounds = solutes.flatMap(g => g.compounds);

  // --- INTELLIGENT DATA VALIDATION ---
  const runDataValidation = () => {
    console.group("Running Solution Mixer Data Validation...");
    let issuesFound = 0;
    // ... (This function can be collapsed for brevity in the final version if desired)
    solutes.forEach(groupObject => {
      groupObject.compounds.forEach(compound => {
        if (compound.ions.length === 0 && (groupObject.group.includes("Salts") || groupObject.group.includes("Acids") || groupObject.group.includes("Bases"))) {
          console.warn(`Data Issue: The ionic compound "${compound.name}" in group "${groupObject.group}" has an empty 'ions' array.`, compound);
          issuesFound++;
        }
        if (compound.ions.length > 0) {
          const hasCation = compound.ions.some(i => i.formula.includes('+'));
          const hasAnion = compound.ions.some(i => i.formula.includes('&minus;'));
          if (!hasCation) { console.warn(`Data Issue: The ionic compound "${compound.name}" is missing a cation.`, compound); issuesFound++; }
          if (!hasAnion) { console.warn(`Data Issue: The ionic compound "${compound.name}" is missing an anion.`, compound); issuesFound++; }
        }
      });
    });
    if (issuesFound === 0) {
      console.log("Validation complete. No immediate issues found.");
    } else {
      console.error(`Validation complete. Found ${issuesFound} potential issue(s).`);
    }
    console.groupEnd();
  };
  runDataValidation();
  
  // --- HTML GENERATION ---
  const createSolutionPanel = (id, title) => {
    const panel = htl.html`<div class="input-panel" id="panel-${id}">
      <h3 class="panel-title">${title}</h3>
      <div class="form-group">
        <label for="solute-select-${id}">Select a Solute:</label>
        <select id="solute-select-${id}" class="input-element">
          ${solutes.map(group => htl.html`<optgroup label="${group.group}">${
            [...group.compounds].sort((a, b) => a.name.localeCompare(b.name)).map(c => htl.html`<option value="${c.formula}">${c.name}</option>`)
          }</optgroup>`)}
        </select>
        <div id="solute-info-${id}" class="solute-info"></div>
      </div>
      <div class="form-group">
        <label>Specify Solute Amount:</label>
        <div class="radio-group">
          <input type="radio" id="mass-radio-${id}" name="amount-type-${id}" value="mass" checked><label for="mass-radio-${id}">Mass</label>
          <input type="radio" id="moles-radio-${id}" name="amount-type-${id}" value="moles"><label for="moles-radio-${id}">Moles</label>
          <input type="radio" id="molarity-radio-${id}" name="amount-type-${id}" value="molarity"><label for="molarity-radio-${id}">Molarity</label>
        </div>
        <div class="input-with-unit">
          <input type="number" id="amount-input-${id}" class="input-element" min="0" step="0.01"><span id="amount-unit-${id}" class="input-unit">(g)</span>
        </div>
      </div>
      <div class="form-group">
        <label for="volume-input-${id}">Set Final Solution Volume:</label>
        <div class="input-with-unit">
          <input type="number" id="volume-input-${id}" class="input-element" min="0" max="1000" step="0.1"><span class="input-unit">(mL)</span>
        </div>
      </div>
    </div>`;
    return panel;
  };

  const container = htl.html`
    <div class="precip-lab-container">
      <div class="widget-title">Solution Mixer<p class="widget-subtitle"><i>&rho;</i>(H<sub>2</sub>O) = 1.00 g mL<sup>&minus;1</sup> for all relevant calculations.</p></div>
      ${createSolutionPanel('a', 'Solution A')}
      <div id="mix-panel"><button id="mix-button" class="action-button">Mix</button></div>
      ${createSolutionPanel('b', 'Solution B')}
      <div id="solution-a-output" class="solution-output"></div>
      <div id="solution-b-output" class="solution-output"></div>
      <div id="final-mixture-output" class="solution-output" style="display: none;"></div>
    </div>
  `;

  // --- ELEMENT SELECTORS ---
  const mixButton = container.querySelector("#mix-button");
  const solutionAOutput = container.querySelector("#solution-a-output");
  const solutionBOutput = container.querySelector("#solution-b-output");
  const finalMixtureOutput = container.querySelector("#final-mixture-output");

  // --- HELPER FUNCTIONS ---
  const countDecimalPlaces = (numStr) => { const s = String(numStr); if (s.includes('.')) { return s.split('.')[1].length; } return 0; };
  const calculateSolutionFromInputs = (inputs) => { 
    if (!inputs) return null;
    const { selectedSolute, amountType, amountValue, amountValueString, volumeValue } = inputs; 
    let requestedMass; 
    if (amountType === 'mass') { requestedMass = amountValue; } 
    else if (amountType === 'moles') { requestedMass = amountValue * selectedSolute.molarMass; } 
    else { const volumeInL = volumeValue / 1000; const molesNeeded = amountValue * volumeInL; requestedMass = molesNeeded * selectedSolute.molarMass; } 
    let dissolvedMass = 0, undissolvedMass = 0, isSaturated = false; 
    if (!selectedSolute.isSoluble) { dissolvedMass = 0; undissolvedMass = requestedMass; isSaturated = true; } 
    else if (selectedSolute.solubilityLimit === "Infinity") { dissolvedMass = requestedMass; } 
    else { const maxDissolvableMass = (selectedSolute.solubilityLimit / 100.0) * volumeValue; if (requestedMass > maxDissolvableMass) { isSaturated = true; dissolvedMass = maxDissolvableMass; undissolvedMass = requestedMass - dissolvedMass; } else { dissolvedMass = requestedMass; } } 
    const moles = dissolvedMass > 0 ? dissolvedMass / selectedSolute.molarMass : 0; 
    const concentration = volumeValue > 0 ? moles / (volumeValue / 1000) : 0; 
    const ions = selectedSolute.ions.map(ion => ({ formula: ion.formula, moles: moles * ion.ratio, concentration: concentration * ion.ratio })); 
    let latexInitialCalc = null;
    if (amountType !== 'molarity' && dissolvedMass > 0) {
      const sigFigs = 4;
      const soluteFormula = cleanFormulaForLatex(selectedSolute.formula);
      const molarMassFormatted = formatWithBar(String(selectedSolute.molarMass));
      const volumeL = volumeValue / 1000;
      const volumeDecimalPlaces = countDecimalPlaces(String(volumeValue));
      const volumeLStr = volumeL.toFixed(volumeDecimalPlaces + 3);
      const volumeLFormatted = formatWithBar(volumeLStr);
      let blueprint = '', substitution = '';
      if (amountType === 'mass') {
        blueprint = `m(\\mathrm{${soluteFormula}}) ~ M(\\mathrm{${soluteFormula}})^{-1} ~ V^{-1}`;
        substitution = `&= (${formatWithBar(amountValueString)})~\\mathrm{g} \\left( \\dfrac{1~\\mathrm{mol}}{${molarMassFormatted}~\\mathrm{g}} \\right) \\left( \\dfrac{1}{${volumeLFormatted}~\\mathrm{L}} \\right)`;
      } else {
        blueprint = `n(\\mathrm{${soluteFormula}}) ~ V^{-1}`;
        substitution = `&= (${formatWithBar(amountValueString)})~\\mathrm{mol} \\left( \\dfrac{1}{${volumeLFormatted}~\\mathrm{L}} \\right)`;
      }
      latexInitialCalc = `$$ \\begin{align*} c(\\mathrm{${soluteFormula}}) &= ${blueprint} \\\\[1.5ex] ${substitution} \\\\[1.5ex] &= ${getUnroundedWithBar(concentration, sigFigs)}~\\class{mjx-molar}{\\mathrm{M}} \\\\[1.5ex] &= ${roundBankers(concentration, sigFigs).toPrecision(sigFigs)}~\\class{mjx-molar}{\\mathrm{M}} \\end{align*} $$`;
    }
    return { isPrepared: true, solute: selectedSolute, concentration, moles, volume: volumeValue, ions, isSaturated, undissolvedMass, latexInitialCalc }; 
  };
  const formatWithBar = (numStr) => { if (!numStr || typeof numStr !== 'string') return ''; if (numStr.includes('e')) return numStr; if (numStr.length === 1) return `\\bar{${numStr}}`; return `${numStr.slice(0, -1)}\\bar{${numStr.slice(-1)}}`; };
  const getUnroundedWithBar = (num, sf) => { if (num === 0) return "0"; const precisionStr = num.toPrecision(sf); const fullStr = num.toPrecision(sf + 2); const guardDigits = fullStr.substring(precisionStr.length); let significantPart = precisionStr; let exponentPart = ''; if (precisionStr.includes('e')) { const parts = precisionStr.split('e'); significantPart = parts[0]; exponentPart = 'e' + parts[1]; } const barredPart = `${significantPart.slice(0, -1)}\\bar{${significantPart.slice(-1)}}`; return `${barredPart}${guardDigits}${exponentPart}`; };
  const roundBankers = (num, sf) => { if (num === 0) return 0; if (!sf || sf < 1) return num; const magnitude = Math.pow(10, sf - Math.floor(Math.log10(Math.abs(num))) - 1); const scaled = num * magnitude; const floorScaled = Math.floor(scaled); const diff = scaled - floorScaled; let roundedScaled; if (diff === 0.5) { roundedScaled = (floorScaled % 2 === 0) ? floorScaled : floorScaled + 1; } else { roundedScaled = Math.round(scaled); } return roundedScaled / magnitude; };
  const cleanFormulaForLatex = (formula) => { return formula.replace(/<sub>/g, '_{').replace(/<\/sub>/g, '}').replace(/<sup>/g, '{^{').replace(/<\/sup>/g, '}}').replace(/&minus;/g, '-'); };
  const hexToRgb = (hex) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; };
  const rgbToHex = (r, g, b) => { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).padStart(6, '0'); };
  const updateInputsFromState = () => { for (const id of ['a', 'b']) { const solutionKey = id === 'a' ? 'solutionA' : 'solutionB'; const solutionState = appState[solutionKey]; const amountInput = container.querySelector(`#amount-input-${id}`); container.querySelector(`#solute-select-${id}`).value = solutionState.soluteFormula; container.querySelector(`#volume-input-${id}`).value = Number(solutionState.userInputVolume).toFixed(1); switch (solutionState.userInputType) { case 'moles': amountInput.value = Number(solutionState.userInputAmount).toFixed(3); container.querySelector(`#moles-radio-${id}`).checked = true; container.querySelector(`#amount-unit-${id}`).textContent = '(mol)'; break; case 'molarity': amountInput.value = Number(solutionState.userInputAmount).toFixed(4); container.querySelector(`#molarity-radio-${id}`).checked = true; container.querySelector(`#amount-unit-${id}`).innerHTML = '(mol L<sup>&minus;1</sup>)'; break; case 'mass': default: amountInput.value = Number(solutionState.userInputAmount).toFixed(2); container.querySelector(`#mass-radio-${id}`).checked = true; container.querySelector(`#amount-unit-${id}`).textContent = '(g)'; break; } } };
  
  // --- CORE LOGIC FUNCTION ---
  const handleMix = () => {
    const formulaA = appState.solutionA.soluteFormula;
    const formulaB = appState.solutionB.soluteFormula;
    
    const inputsA = { selectedSolute: allCompounds.find(c => c.formula === appState.solutionA.soluteFormula), amountType: appState.solutionA.userInputType, amountValue: appState.solutionA.userInputAmount, amountValueString: container.querySelector("#amount-input-a").value, volumeValue: appState.solutionA.userInputVolume, volumeDecimalPlaces: countDecimalPlaces(appState.solutionA.userInputVolume.toString()) };
    const inputsB = { selectedSolute: allCompounds.find(c => c.formula === appState.solutionB.soluteFormula), amountType: appState.solutionB.userInputType, amountValue: appState.solutionB.userInputAmount, amountValueString: container.querySelector("#amount-input-b").value, volumeValue: appState.solutionB.userInputVolume, volumeDecimalPlaces: countDecimalPlaces(appState.solutionB.userInputVolume.toString()) };
    
    if (!inputsA.selectedSolute || !inputsB.selectedSolute) {
      alert("A valid solute must be selected for both solutions.");
      return;
    }
    const calculatedA = calculateSolutionFromInputs(inputsA);
    const calculatedB = calculateSolutionFromInputs(inputsB);

    // --- Final, Comprehensive Safety Check ---
    const isPermanganate = (f) => f === "KMnO<sub>4</sub>";
    const isNitricAcid = (f) => f === "HNO<sub>3</sub>";
    const isSugar = (f) => f === "C<sub>12</sub>H<sub>22</sub>O<sub>11</sub>" || f === "C<sub>6</sub>H<sub>12</sub>O<sub>6</sub>";
    const isIodide = (f) => f === "KI" || f === "NaI";
    const isAcid = (f) => f === "HNO<sub>3</sub>" || f === "HCl";
    const isCarbonate = (f) => f === "Na<sub>2</sub>CO<sub>3</sub>";
    const isPhosphate = (f) => f === "Na<sub>3</sub>PO<sub>4</sub>";
    const isCopperII = (f) => f === "CuSO<sub>4</sub>" || f === "Cu(NO<sub>3</sub>)<sub>2</sub>"; // NEW CHECK

    let incompatibilityType = null;
    if ((isPermanganate(formulaA) && isSugar(formulaB)) || (isSugar(formulaA) && isPermanganate(formulaB))) {
      incompatibilityType = 'redox-permanganate-sugar';
    } else if ((isPermanganate(formulaA) && isIodide(formulaB)) || (isIodide(formulaA) && isPermanganate(formulaB))) {
      incompatibilityType = 'redox-permanganate-iodide';
    } else if ((isNitricAcid(formulaA) && isSugar(formulaB)) || (isSugar(formulaA) && isNitricAcid(formulaB))) {
      incompatibilityType = 'redox-nitric-sugar';
    } else if ((isAcid(formulaA) && isCarbonate(formulaB)) || (isCarbonate(formulaA) && isAcid(formulaB))) {
      incompatibilityType = 'gas-forming-carbonate';
    } else if ((isAcid(formulaA) && isPhosphate(formulaB)) || (isPhosphate(formulaA) && isAcid(formulaB))) {
      incompatibilityType = 'weak-acid-base-phosphate';
    } else if ((isCopperII(formulaA) && isIodide(formulaB)) || (isIodide(formulaA) && isCopperII(formulaB))) { // NEW CASE
      incompatibilityType = 'redox-copper-iodide';
    }

    if (incompatibilityType) {
      let warningTitle = "Reaction Blocked: Advanced Chemistry Detected";
      let warningBody = "";

      switch (incompatibilityType) {
        case 'redox-permanganate-sugar':
          warningTitle = "Reaction Blocked: Oxidation of a Carbohydrate";
          warningBody = `Potassium permanganate is a powerful oxidizing agent. It will react with the sugar in a complex redox reaction, where the deep purple permanganate ion (MnO<sub>4</sub><sup>&minus;</sup>) is reduced, typically to brown, solid manganese dioxide (MnO<sub>2</sub>(s)).`;
          break;
        case 'redox-permanganate-iodide':
          warningTitle = "Reaction Blocked: Redox Reaction";
          warningBody = `Potassium permanganate is a strong oxidizing agent that reacts with iodide ions (I<sup>&minus;</sup>). The purple MnO<sub>4</sub><sup>&minus;</sup> is reduced, while the colorless I<sup>&minus;</sup> is oxidized to form brown aqueous iodine (I<sub>2</sub>). A representative reaction is:<p>2 MnO<sub>4</sub><sup>&minus;</sup>(aq) + 10 I<sup>&minus;</sup>(aq) &rarr; 2 Mn<sup>2+</sup>(aq) + 5 I<sub>2</sub>(aq)</p>`;
          break;
        case 'redox-nitric-sugar':
          warningTitle = "Reaction Blocked: Oxidation of a Carbohydrate";
          warningBody = `Nitric acid is a strong oxidizing agent that will vigorously react with and break down the sugar molecule. This complex redox reaction produces various products, including toxic brown nitrogen dioxide (NO<sub>2</sub>) gas.`;
          break;
        case 'gas-forming-carbonate':
          warningTitle = "Reaction Blocked: Gas-Forming Reaction";
          warningBody = `An acid reacts with a carbonate to form carbonic acid (H<sub>2</sub>CO<sub>3</sub>), which is unstable and immediately decomposes into water and carbon dioxide gas, which you would observe as fizzing or bubbling. The net ionic equation is:<p>2 H<sup>+</sup>(aq) + CO<sub>3</sub><sup>2&minus;</sup>(aq) &rarr; H<sub>2</sub>O(l) + CO<sub>2</sub>(g)</p>`;
          break;
        case 'weak-acid-base-phosphate':
          warningTitle = "Reaction Blocked: Weak Acid-Base Equilibrium";
          warningBody = `The phosphate ion (PO<sub>4</sub><sup>3&minus;</sup>) is a weak base. It reacts with a strong acid in a stepwise equilibrium, making it a buffer problem that involves complex calculations beyond the scope of this simulation. The first step of the reaction is:<p>H<sup>+</sup>(aq) + PO<sub>4</sub><sup>3&minus;</sup>(aq) &rightleftharpoons; HPO<sub>4</sub><sup>2&minus;</sup>(aq)</p>`;
          break;
        case 'redox-copper-iodide':
          warningTitle = "Reaction Blocked: Redox Reaction";
          warningBody = `Copper(II) ions are strong enough to oxidize iodide ions. In this spontaneous redox reaction, Cu<sup>2+</sup> is reduced to Cu<sup>+</sup>, which precipitates as solid copper(I) iodide, while I<sup>&minus;</sup> is oxidized to form brown aqueous iodine. The net ionic equation is:<p>2 Cu<sup>2+</sup>(aq) + 4 I<sup>&minus;</sup>(aq) &rarr; 2 CuI(s) + I<sub>2</sub>(aq)</p>`;
          break;
      }
      
      const fullMessage = `<h4>${warningTitle}</h4><p>${warningBody}</p>`;
      
      appState.mixture = {
        isMixed: true, isWarningState: true, warningMessage: fullMessage,
        precipitates: [], limitingReactant: null, neutralizationLR: null, finalContents: [],
        final_pH: null, molecularEquation: null, completeIonicEquation: null, netIonicEquation: null,
        latexPrecipitateCalc: null, latexExcessReactantCalc: null, latexNeutralizationCalc: null, latexIonCalc: null
      };
      appState.solutionA = { ...appState.solutionA, ...calculatedA };
      appState.solutionB = { ...appState.solutionB, ...calculatedB };
      render();
      return; // Stop execution
    }
    
    const finalVolume = (calculatedA.volume + calculatedB.volume) / 1000;
    const sigFigs = 4;
    const finalVolumeDecimalPlaces = Math.min(inputsA.volumeDecimalPlaces, inputsB.volumeDecimalPlaces);
    const finalVolumeInMLStr = (calculatedA.volume + calculatedB.volume).toFixed(finalVolumeDecimalPlaces);
    const finalVolumeSigFigs = finalVolumeInMLStr.replace('.', '').length;
    const consolidatedSpecies = new Map();
    const addSpeciesToMap = (solution) => {
      if (solution.ions.length > 0) {
        solution.ions.forEach(ion => {
          const existingMoles = consolidatedSpecies.get(ion.formula) || 0;
          consolidatedSpecies.set(ion.formula, existingMoles + ion.moles);
        });
      } else if (solution.solute.isSoluble) {
        const existingMoles = consolidatedSpecies.get(solution.solute.formula) || 0;
        consolidatedSpecies.set(solution.solute.formula, existingMoles + solution.moles);
      }
    };
    addSpeciesToMap(calculatedA);
    addSpeciesToMap(calculatedB);

    let finalPrecipitates = [];
    let limitingReactant = null,
      latexPrecipitateCalc = null,
      latexExcessReactantCalc = null,
      latexNeutralizationCalc = null,
      neutralizationLR = null,
      latexIonCalc = null;
    let molecularEquation = null,
      completeIonicEquation = null,
      netIonicEquation = null;
    let final_pH = null;

    const molesH = consolidatedSpecies.get('H<sup>+</sup>') || 0;
    const molesOH = consolidatedSpecies.get('OH<sup>&minus;</sup>') ||
      0;
    if (molesH > 1e-9 && molesOH > 1e-9) {
      let lrFormula, erFormula, molesLR, molesER, molesER_final;
      if (molesH < molesOH && Math.abs(molesH - molesOH) > 1e-9) {
        lrFormula = 'H<sup>+</sup>';
        erFormula = 'OH<sup>&minus;</sup>';
        molesLR = molesH;
        molesER = molesOH;
        molesER_final = molesER - molesLR;
        const concOH_final = finalVolume > 0 ? molesER_final /
          finalVolume : 0;
        final_pH = concOH_final > 0 ? 14.00 + Math.log10(
          concOH_final) : 7.00;
        neutralizationLR = {
          formula: lrFormula
        };
      } else if (molesOH < molesH && Math.abs(molesH - molesOH) >
        1e-9) {
        lrFormula = 'OH<sup>&minus;</sup>';
        erFormula = 'H<sup>+</sup>';
        molesLR = molesOH;
        molesER = molesH;
        molesER_final = molesER - molesLR;
        const concH_final = finalVolume > 0 ? molesER_final /
          finalVolume : 0;
        final_pH = concH_final > 0 ? -Math.log10(concH_final) :
          7.00;
        neutralizationLR = {
          formula: lrFormula
        };
      } else {
        molesLR = molesH;
        molesER = molesOH;
        molesER_final = 0;
        final_pH = 7.00;
        neutralizationLR = {
          formula: 'Stoichiometric Mixture'
        };
      }
      consolidatedSpecies.set('H<sup>+</sup>', 0);
      consolidatedSpecies.set('OH<sup>&minus;</sup>', 0);
      if (molesER_final > 1e-9) {
        consolidatedSpecies.set(erFormula, molesER_final);
      }
      if (molesER_final > 1e-9) {
        latexNeutralizationCalc =
          `$$ \\begin{align*} \\text{Net Ionic Eq: } & \\quad \\mathrm{H^+(aq) + OH^-(aq) \\rightarrow H_2O(l)} \\\\[1.5ex] n(\\mathrm{${cleanFormulaForLatex(erFormula)}})_{\\text{final}} &= n(\\mathrm{${cleanFormulaForLatex(erFormula)}})_{\\text{initial}} - n(\\mathrm{${cleanFormulaForLatex(lrFormula)}})_{\\text{initial}} \\\\[1.5ex] &= ${formatWithBar(molesER.toPrecision(sigFigs))}~\\mathrm{mol} - ${formatWithBar(molesLR.toPrecision(sigFigs))}~\\mathrm{mol} \\\\[1.5ex] &= ${getUnroundedWithBar(molesER_final, sigFigs)}~\\mathrm{mol} \\\\[1.5ex] &= ${roundBankers(molesER_final, sigFigs).toPrecision(sigFigs)}~\\mathrm{mol} \\end{align*} $$`;
      }
      let acidSolute, baseSolute;
      if (calculatedA.solute.ions.some(i => i.formula ===
          'H<sup>+</sup>')) {
        acidSolute = calculatedA.solute;
        baseSolute = calculatedB.solute;
      } else {
        acidSolute = calculatedB.solute;
        baseSolute = calculatedA.solute;
      }
      const spectatorCation = baseSolute.ions.find(i => i.formula !==
        'OH<sup>&minus;</sup>');
      const spectatorAnion = acidSolute.ions.find(i => i.formula !==
        'H<sup>+</sup>');
      if (spectatorAnion && spectatorCation) {
        const saltFormula =
          `${spectatorCation.formula.replace(/<sup>.*<\/sup>/, '')}${spectatorAnion.formula.replace(/<sup>.*<\/sup>/, '')}`;
        molecularEquation =
          `${acidSolute.formula}(aq) + ${baseSolute.formula}(aq) &rarr; H<sub>2</sub>O(l) + ${saltFormula}(aq)`;
      } else {
        molecularEquation = "Acid-base reaction occurred.";
      }
    }
    if (calculatedA.concentration > 0 && calculatedB.concentration >
      0 && calculatedA.ions.length > 0 && calculatedB.ions.length > 0
    ) {
      const ionsA = calculatedA.ions;
      const ionsB = calculatedB.ions;
      const cationA = ionsA.find(ion => ion.formula.includes('+'));
      const anionA = ionsA.find(ion => ion.formula.includes(
        '&minus;'));
      const cationB = ionsB.find(ion => ion.formula.includes('+'));
      const anionB = ionsB.find(ion => ion.formula.includes(
        '&minus;'));
      if (cationA && anionA && cationB && anionB) {
        const findProduct = (ion1, ion2) => allCompounds.find(c => c
          .ions.length >= 2 && c.ions.some(i => i.formula ===
            ion1.formula) && c.ions.some(i => i.formula ===
            ion2.formula));
        const p1 = findProduct(cationA, anionB);
        const p2 = findProduct(cationB, anionA);
        const precipitateCompound = [p1, p2].find(p => p && !p
          .isSoluble);
        if (precipitateCompound) {
          const rIonInfo1 = precipitateCompound.ions.find(i => i
            .formula === cationA.formula || i.formula ===
            cationB.formula);
          const rIonInfo2 = precipitateCompound.ions.find(i => i
            .formula === anionA.formula || i.formula ===
            anionB.formula);
          const reactant1 = (calculatedA.solute.ions.some(i => i
                .formula === rIonInfo1.formula) ||
              calculatedA.solute.ions.some(i => i.formula ===
                rIonInfo2.formula)) ? calculatedA.solute :
            calculatedB.solute;
          const reactant2 = reactant1 === calculatedA.solute ?
            calculatedB.solute : calculatedA.solute;

          const specIon1 = reactant1.ions.find(i => i.formula !==
            rIonInfo1.formula && i.formula !== rIonInfo2
            .formula);
          const specIon2 = reactant2.ions.find(i => i.formula !==
            rIonInfo1.formula && i.formula !== rIonInfo2
            .formula);
          const solubleProduct = allCompounds.find(c =>
            specIon1 && specIon2 && c.ions.some(i => i
              .formula === specIon1.formula) && c.ions
            .some(i => i.formula === specIon2.formula));

          if (solubleProduct) {
            const getCharge = (ionFormula) => {
              const match = ionFormula.match(
                /<sup>([0-9])?([+-])/);
              if (!match) return 0;
              const num = match[1] ? parseInt(match[1]) :
                1;
              return match[2] === '+' ? num : -num;
            };
            const cation1Charge = Math.abs(getCharge(reactant1
              .ions.find(i => i.formula.includes('+'))
              .formula));
            const cation2Charge = Math.abs(getCharge(reactant2
              .ions.find(i => i.formula.includes('+'))
              .formula));
            const coeffR1 = cation2Charge;
            const coeffR2 = cation1Charge;
            const coeffPpt = 1;
            const coeffSol = 1;
            const compoundStr = (coeff, formula) =>
              `${coeff > 1 ? coeff + ' ' : ''}${formula}`;
            molecularEquation =
              `${compoundStr(coeffR1, reactant1.formula)}(aq) + ${compoundStr(coeffR2, reactant2.formula)}(aq) &rarr; ${compoundStr(coeffPpt, precipitateCompound.formula)}(s) + ${compoundStr(coeffSol * coeffR1, solubleProduct.formula)}(aq)`;

            const dissociate = (compound, coeff) => compound
              .ions.map(ion =>
                `${coeff * ion.ratio > 1 ? coeff * ion.ratio + ' ' : ''}${ion.formula}(aq)`
              ).join(' + ');

            const cieReactants =
              `${dissociate(reactant1, coeffR1)} + ${dissociate(reactant2, coeffR2)}`;
            const cieProducts =
              `${compoundStr(coeffPpt, precipitateCompound.formula)}(s) + ${dissociate(solubleProduct, coeffSol * coeffR1)}`;
            completeIonicEquation =
              `${cieReactants} <span class="equation-products">&rarr; ${cieProducts}</span>`;

            netIonicEquation =
              `${dissociate(precipitateCompound, 1).replace(/\(aq\)/g, '(aq)').replace(/\(s\)/g, '(aq)')} &rarr; ${precipitateCompound.formula}(s)`;
          }

          const ion1_moles = consolidatedSpecies.get(rIonInfo1
            .formula);
          const ion2_moles = consolidatedSpecies.get(rIonInfo2
            .formula);
          const moles1_needed = ion1_moles / rIonInfo1.ratio;
          const moles2_needed = ion2_moles / rIonInfo2.ratio;
          let molesPrecipitate, molesExcess, excessReactantInfo;

          if (moles1_needed < moles2_needed && Math.abs(
              moles1_needed - moles2_needed) > 1e-9) {
            limitingReactant = {
              formula: rIonInfo1.formula,
              moles: ion1_moles,
              ratio: rIonInfo1.ratio
            };
            excessReactantInfo = {
              formula: rIonInfo2.formula,
              moles: ion2_moles,
              ratio: rIonInfo2.ratio
            };
            molesPrecipitate = moles1_needed;
            molesExcess = ion2_moles - (molesPrecipitate *
              rIonInfo2.ratio);
            consolidatedSpecies.set(rIonInfo1.formula, 0);
            consolidatedSpecies.set(rIonInfo2.formula,
              molesExcess);
          } else if (moles2_needed < moles1_needed && Math.abs(
              moles1_needed - moles2_needed) > 1e-9) {
            limitingReactant = {
              formula: rIonInfo2.formula,
              moles: ion2_moles,
              ratio: rIonInfo2.ratio
            };
            excessReactantInfo = {
              formula: rIonInfo1.formula,
              moles: ion1_moles,
              ratio: rIonInfo1.ratio
            };
            molesPrecipitate = moles2_needed;
            molesExcess = ion1_moles - (molesPrecipitate *
              rIonInfo1.ratio);
            consolidatedSpecies.set(rIonInfo2.formula, 0);
            consolidatedSpecies.set(rIonInfo1.formula,
              molesExcess);
          } else {
            limitingReactant = {
              formula: 'Stoichiometric Mixture'
            };
            molesPrecipitate = moles1_needed;
            consolidatedSpecies.set(rIonInfo1.formula, 0);
            consolidatedSpecies.set(rIonInfo2.formula, 0);
          }
          const precipitateMass = molesPrecipitate *
            precipitateCompound.molarMass;
          finalPrecipitates.push({
            formula: precipitateCompound.formula,
            mass: precipitateMass,
            moles: molesPrecipitate,
            color: precipitateCompound.color,
            ions: precipitateCompound.ions
          });

          if (limitingReactant.formula !==
            'Stoichiometric Mixture') {
            const pptFormula = cleanFormulaForLatex(
              precipitateCompound.formula);
            const lrFormula = cleanFormulaForLatex(
              limitingReactant.formula);
            const erFormula = cleanFormulaForLatex(
              excessReactantInfo.formula);
            const pptRatio = 1;
            const lrRatio = limitingReactant.ratio;
            const erRatio = excessReactantInfo.ratio;

            latexPrecipitateCalc =
              `$$ \\begin{align*} m(\\mathrm{${pptFormula}}) &= n(\\mathrm{${lrFormula}}) ~ r(\\mathrm{${pptFormula}}, \\mathrm{${lrFormula}}) ~ M(\\mathrm{${pptFormula}}) \\\\[1.5ex] &= (${formatWithBar(limitingReactant.moles.toPrecision(sigFigs))}~\\mathrm{mol}) \\left( \\dfrac{${pptRatio}~\\mathrm{mol}~\\mathrm{${pptFormula}}}{${lrRatio}~\\mathrm{mol}~\\mathrm{${lrFormula}}} \\right) \\left( \\dfrac{${formatWithBar(precipitateCompound.molarMass.toPrecision(4))}~\\mathrm{g}}{\\mathrm{mol}} \\right) \\\\[1.5ex] &= ${getUnroundedWithBar(precipitateMass, sigFigs)}~\\mathrm{g} \\\\[1.5ex] &= ${roundBankers(precipitateMass, sigFigs).toPrecision(sigFigs)}~\\mathrm{g} \\end{align*} $$`;

            latexExcessReactantCalc =
              `$$ \\begin{align*} n(\\mathrm{${erFormula}})_{\\text{final}} &= n(\\mathrm{${erFormula}})_{\\text{initial}} - n(\\mathrm{${erFormula}})_{\\text{reacted}} \\\\[1.5ex] &= n(\\mathrm{${erFormula}})_{\\text{initial}} - n(\\mathrm{${lrFormula}})_{\\text{initial}} ~ r(\\mathrm{${erFormula}}, \\mathrm{${lrFormula}}) \\\\[1.5ex] &= ${formatWithBar(excessReactantInfo.moles.toPrecision(sigFigs))}~\\mathrm{mol} - (${formatWithBar(limitingReactant.moles.toPrecision(sigFigs))}~\\mathrm{mol}) \\left( \\dfrac{${erRatio}~\\mathrm{mol}~\\mathrm{${erFormula}}}{${lrRatio}~\\mathrm{mol}~\\mathrm{${lrFormula}}} \\right) \\\\[1.5ex] &= ${getUnroundedWithBar(molesExcess, sigFigs)}~\\mathrm{mol} \\\\[1.5ex] &= ${roundBankers(molesExcess, sigFigs).toPrecision(sigFigs)}~\\mathrm{mol} \\end{align*} $$`;
          }
        }
      }
    }

    if (calculatedA.undissolvedMass > 0) {
      const moles = calculatedA.undissolvedMass / calculatedA.solute
        .molarMass;
      finalPrecipitates.push({
        formula: calculatedA.solute.formula,
        mass: calculatedA.undissolvedMass,
        moles: moles,
        color: calculatedA.solute.color,
        ions: calculatedA.solute.ions
      });
    }
    if (calculatedB.undissolvedMass > 0) {
      const moles = calculatedB.undissolvedMass / calculatedB.solute
        .molarMass;
      finalPrecipitates.push({
        formula: calculatedB.solute.formula,
        mass: calculatedB.undissolvedMass,
        moles: moles,
        color: calculatedB.solute.color,
        ions: calculatedB.solute.ions
      });
    }

    const combinedPrecipitatesMap = new Map();
    finalPrecipitates.forEach(p => {
      if (combinedPrecipitatesMap.has(p.formula)) {
        const existing = combinedPrecipitatesMap.get(p
          .formula);
        existing.mass += p.mass;
        existing.moles += p.moles;
      } else {
        combinedPrecipitatesMap.set(p.formula, {
          ...p
        });
      }
    });
    const uniquePrecipitates = Array.from(combinedPrecipitatesMap
      .values());

    const finalVolumeLStr = finalVolume.toFixed(finalVolumeDecimalPlaces + 3);
    const finalVolumeLFormatted = formatWithBar(finalVolumeLStr);
    
    consolidatedSpecies.forEach((moles, formula) => { 
      if (moles > 1e-9 && formula.includes('<sup>')) { 
        if (latexIonCalc === null) { 
          latexIonCalc = ``; 
        } 
        const concentration = finalVolume > 0 ? moles / finalVolume : 0; 
        const cleanFormula = cleanFormulaForLatex(formula); 
        latexIonCalc += `$$ \\begin{align*} c(\\mathrm{${cleanFormula}}) &= n(\\mathrm{${cleanFormula}}) ~ V_{\\mathrm{total}}{^{-1}} \\\\[1.5ex] 
                           &= \\dfrac{${formatWithBar(moles.toPrecision(sigFigs))}~\\mathrm{mol}}{${finalVolumeLFormatted}~\\mathrm{L}} \\\\[1.5ex] 
                           &= ${getUnroundedWithBar(concentration, sigFigs)}~\\class{mjx-molar}{\\mathrm{M}} \\\\[1.5ex] 
                           &= ${roundBankers(concentration, sigFigs).toPrecision(sigFigs)}~\\class{mjx-molar}{\\mathrm{M}} 
                         \\end{align*} $$`; 
      } 
    });
    const formattedContents = [];
    consolidatedSpecies.forEach((moles, formula) => {
      formattedContents.push({
        formula: formula,
        moles: moles,
        concentration: finalVolume > 0 ? moles /
          finalVolume : 0
      });
    });

    const mixtureResult = {
      isMixed: true,
      precipitates: uniquePrecipitates,
      limitingReactant: limitingReactant,
      neutralizationLR: neutralizationLR,
      finalContents: formattedContents,
      final_pH: final_pH,
      molecularEquation,
      completeIonicEquation,
      netIonicEquation,
      latexPrecipitateCalc,
      latexExcessReactantCalc,
      latexNeutralizationCalc,
      latexIonCalc
    };

    appState.solutionA = {
      ...appState.solutionA,
      ...calculatedA
    };
    appState.solutionB = {
      ...appState.solutionB,
      ...calculatedB
    };
    appState.mixture = mixtureResult;

    render();

    mixButton.style.backgroundColor = '#2ecc71';
    mixButton.style.borderColor = '#27ae60';
  };

  // --- CORE RENDERING FUNCTION ---
  const render = () => {
    updateInputsFromState();
    const renderSolution = (solutionState, outputElement, title) => {
      if (!solutionState.isPrepared) { outputElement.innerHTML = ''; outputElement.style.display = 'none'; return; }
      outputElement.style.display = 'block';
      outputElement.innerHTML = '';
      
      const wrapper = document.createElement('div');
      wrapper.className = 'initial-output-wrapper';

      const { solute, volume, concentration, ions, isSaturated, undissolvedMass, latexInitialCalc } = solutionState;
      const sigFigs = 4;
      const dissolvedMass = solutionState.moles * solute.molarMass;
      
      const beakerWrapper = document.createElement('div');
      beakerWrapper.className = 'beaker-container';
      const svgWidth = 180, svgHeight = 160;
      const titleElement = document.createElement('h4');
      titleElement.textContent = title;
      beakerWrapper.append(titleElement);
      const svg = d3.create("svg").attr("viewBox", [0, 0, svgWidth, svgHeight]).attr("style", `width: 150px; height: auto;`);
      
      const beakerBodyWidth = 90,
        beakerBodyHeight = 135,
        beakerX = (svgWidth - beakerBodyWidth) / 2,
        beakerY = 10;
      const beakerStrokeWidth = 2.5,
        liquidPadding = 3.0,
        innerWallOffset = beakerStrokeWidth / 2;

      svg.append("path").attr("d",
        `M ${beakerX + beakerBodyWidth + 8} ${beakerY} L ${beakerX} ${beakerY} L ${beakerX} ${beakerY + beakerBodyHeight} L ${beakerX + beakerBodyWidth} ${beakerY + beakerBodyHeight} L ${beakerX + beakerBodyWidth} ${beakerY}`
      ).attr("fill", "none").attr("stroke", "black").attr(
        "stroke-width", beakerStrokeWidth);
      svg.append("path").attr("d",
        `M ${beakerX + beakerBodyWidth + 8} ${beakerY} L ${beakerX} ${beakerY} L ${beakerX} ${beakerY + beakerBodyHeight} L ${beakerX + beakerBodyWidth} ${beakerY + beakerBodyHeight} L ${beakerX + beakerBodyWidth} ${beakerY}`
      ).attr("fill", "none").attr("stroke", "black").attr(
        "stroke-width", beakerStrokeWidth);
      const liquidX = beakerX + innerWallOffset + liquidPadding;
      const liquidWidth = beakerBodyWidth - 2 * (innerWallOffset +
        liquidPadding);
      const maxLiquidHeight = beakerBodyHeight - innerWallOffset -
        liquidPadding;
      const liquidHeight = (volume / 1000) * maxLiquidHeight;
      const liquidY = beakerY + beakerBodyHeight -
        innerWallOffset - liquidPadding - liquidHeight;
      const opacityScale = d3.scaleLinear().domain([0, 5]).range([
        0.1, 1.0
      ]).clamp(true);
      const liquid = svg.append("rect").attr("x", liquidX).attr(
        "y", liquidY).attr("width", liquidWidth).attr(
        "height", liquidHeight);
      if (!solute.isSoluble) {
        liquid.attr("fill", "#aed6f1").attr("fill-opacity",
          0.4);
      } else {
        liquid.attr("fill", solute.color).attr("fill-opacity",
          opacityScale(concentration));
      }
      if (isSaturated || !solute.isSoluble) {
        for (let i = 0; i < 35; i++) {
          svg.append("circle").attr("cx", beakerX + 10 + Math
              .random() * (beakerBodyWidth - 20)).attr(
              "cy", beakerY + beakerBodyHeight - 5 - Math
              .random() * 10).attr("r", 1.5 + Math
              .random() * 1).attr("fill", solute.color)
            .attr(
              "stroke", "rgba(0,0,0,0.2)").attr(
              "stroke-width", 1);
        }
      }
      beakerWrapper.append(svg.node());
      outputElement.append(beakerWrapper);
      const mainTableNode = document.createElement('div');
      mainTableNode.innerHTML = `<table class="results-table"><tbody><tr><th>Solute</th><td>${solute.name}</td></tr><tr><th>Concentration</th><td>${roundBankers(concentration, sigFigs).toPrecision(sigFigs)} <span class='unit-molar'>M</span></td></tr><tr><th>Dissolved Mass</th><td>${roundBankers(dissolvedMass, sigFigs).toPrecision(sigFigs)} g</td></tr><tr><th>Dissolved Moles</th><td>${roundBankers(solutionState.moles, sigFigs).toPrecision(sigFigs)} mol</td></tr><tr><th>Volume</th><td>${volume.toFixed(1)} mL</td></tr></tbody></table>`;
      
      const equationDiv = document.createElement('div');
      equationDiv.className = 'dissolution-equation';
      let equationHTML = '';
      if (solute.isSoluble) {
        const initialState = (solute.formula === "HCl" || solute
          .formula === "HNO<sub>3</sub>") ? '(aq)' : '(s)';
        if (solute.ions.length > 0) {
          const products = solute.ions.map(ion => {
            const coefficient = ion.ratio > 1 ? ion
              .ratio + ' ' : '';
            return `${coefficient}${ion.formula}(aq)`;
          }).join(' + ');
          equationHTML =
            `${solute.formula}${initialState} &rarr; ${products}`;
        } else {
          equationHTML =
            `${solute.formula}(s) &rarr; ${solute.formula}(aq)`;
        }
      } else {
        equationHTML = `<strong>Insoluble in water.</strong>`;
      }
      equationDiv.innerHTML = equationHTML;
      outputElement.append(equationDiv);
      
      outputElement.append(mainTableNode);
      if (solutionState.moles > 1e-9) {
        const speciesTableNode = document.createElement('div');
        let speciesTableHTML =
          `<table class="results-table" style="margin-top: 10px;"><tbody>`;
        if (ions && ions.length > 0) {
          speciesTableHTML +=
            '<tr><th>Species</th><th>Moles</th><th>Concentration</th></tr>';
          ions.forEach(ion => {
            speciesTableHTML +=
              `<tr><th style="padding-left: 20px;">${ion.formula}</th><td>${roundBankers(ion.moles, sigFigs).toPrecision(sigFigs)}</td><td>${roundBankers(ion.concentration, sigFigs).toPrecision(sigFigs)} <span class='unit-molar'>M</span></td></tr>`;
          });
        } else {
          speciesTableHTML +=
            '<tr><th>Species</th><th>Moles</th><th>Concentration</th></tr>';
          speciesTableHTML +=
            `<tr><th style="padding-left: 20px;">${solute.formula}</th><td>${roundBankers(solutionState.moles, sigFigs).toPrecision(sigFigs)}</td><td>${roundBankers(concentration, sigFigs).toPrecision(sigFigs)} <span class='unit-molar'>M</span></td></tr>`;
        }
        speciesTableHTML += `</tbody></table>`;
        speciesTableNode.innerHTML = speciesTableHTML;
        outputElement.append(speciesTableNode);
      }

      if (isSaturated && solute.isSoluble) {
        const alertDiv = document.createElement('div');
        alertDiv.className = 'saturation-alert';
        alertDiv.innerHTML =
          `<strong>Saturated:</strong> ${undissolvedMass.toFixed(2)} g of solid has not dissolved.`;
        outputElement.append(alertDiv);
      }
      if (latexInitialCalc) {
        const details = document.createElement('details');
        details.className = 'calculation-details';
        
        const summary = document.createElement('summary');
        summary.textContent = 'Initial Concentration Calculation';
        
        const contentDiv = document.createElement('div');
        contentDiv.className = 'calculation-panel-content';
        contentDiv.innerHTML = latexInitialCalc;
        
        details.append(summary);
        details.append(contentDiv);
        
        outputElement.append(details);
        
        if (window.MathJax) {
            setTimeout(() => { window.MathJax.typesetPromise([contentDiv]); }, 0);
        }
      }
    };
    renderSolution(appState.solutionA, solutionAOutput, "Solution A");
    renderSolution(appState.solutionB, solutionBOutput, "Solution B");

    if (appState.mixture.isMixed) {
      finalMixtureOutput.style.display = 'block';
      finalMixtureOutput.innerHTML = '';
      const mixture = appState.mixture;
      if (mixture.isWarningState) {
        const warningPanel = document.createElement('div');
        warningPanel.className = 'reaction-warning-panel';
        warningPanel.innerHTML = mixture.warningMessage;
        finalMixtureOutput.append(warningPanel);
        return; // Stop rendering the normal output
      }
      const sigFigs = 4;
      const mainContentWrapper = document.createElement('div');
      mainContentWrapper.style.display = 'flex';
      mainContentWrapper.style.flexDirection = 'column';
      mainContentWrapper.style.alignItems = 'center';
      const beakerWrapper = document.createElement('div');
      beakerWrapper.className = 'beaker-container';
      const svgWidth = 220,
        svgHeight = 200;
      const titleElement = document.createElement('h4');
      titleElement.textContent = "Final Mixture";
      beakerWrapper.append(titleElement);
      const svg = d3.create("svg").attr("viewBox", [0, 0, svgWidth,
        svgHeight
      ]).attr("style", `width: 220px; height: auto;`);
      const beakerBodyWidth = 110,
        beakerBodyHeight = 170,
        beakerX = (svgWidth - beakerBodyWidth) / 2,
        beakerY = 15;
      const beakerStrokeWidth = 2.5,
        liquidPadding = 3.0,
        innerWallOffset = beakerStrokeWidth / 2;
      svg.append("path").attr("d",
        `M ${beakerX + beakerBodyWidth + 8} ${beakerY} L ${beakerX} ${beakerY} L ${beakerX} ${beakerY + beakerBodyHeight} L ${beakerX + beakerBodyWidth} ${beakerY + beakerBodyHeight} L ${beakerX + beakerBodyWidth} ${beakerY}`
      ).attr("fill", "none").attr("stroke", "black").attr(
        "stroke-width", beakerStrokeWidth);
      const liquidX = beakerX + innerWallOffset + liquidPadding;
      const liquidWidth = beakerBodyWidth - 2 * (innerWallOffset +
        liquidPadding);
      const maxLiquidHeight = beakerBodyHeight - innerWallOffset -
        liquidPadding;
      const liquidHeight = ((appState.solutionA.volume + appState
        .solutionB.volume) / 2000) * maxLiquidHeight;
      const liquidY = beakerY + beakerBodyHeight - innerWallOffset -
        liquidPadding - liquidHeight;
      let finalColorHex = '#aed6f1';
      let totalColoredConcentration = 0;
      const coloredSpeciesInMixture = mixture.finalContents.map(
        item => {
          const compoundData = allCompounds.find(c => c
            .formula === item.formula || c.ions.some(
              i => i.formula === item.formula));
          if (compoundData && compoundData.color !==
            '#ecf0f1') {
            return {
              concentration: item.concentration,
              rgb: hexToRgb(compoundData.color)
            };
          }
          return null;
        }).filter(Boolean);
      if (coloredSpeciesInMixture.length > 0) {
        totalColoredConcentration = coloredSpeciesInMixture.reduce((
          sum, s) => sum + s.concentration, 0);
        if (coloredSpeciesInMixture.length === 1) {
          const singleRgb = coloredSpeciesInMixture[0].rgb;
          finalColorHex = rgbToHex(singleRgb.r, singleRgb.g,
            singleRgb.b);
        } else {
          let totalR = 0,
            totalG = 0,
            totalB = 0;
          coloredSpeciesInMixture.forEach(s => {
            totalR += s.rgb.r * s.concentration;
            totalG += s.rgb.g * s.concentration;
            totalB += s.rgb.b * s.concentration;
          });
          const avgR = Math.round(totalR /
            totalColoredConcentration);
          const avgG = Math.round(totalG /
            totalColoredConcentration);
          const avgB = Math.round(totalB /
            totalColoredConcentration);
          finalColorHex = rgbToHex(avgR, avgG, avgB);
        }
      }
      const liquid = svg.append("rect")
        .attr("x", liquidX).attr("y", liquidY)
        .attr("width", liquidWidth).attr("height", liquidHeight);
      const opacityScale = d3.scaleLinear().domain([0, 2.0]).range([
        0.1, 1.0
      ]).clamp(true);
      liquid.attr("fill", finalColorHex)
        .attr("fill-opacity", totalColoredConcentration > 0 ?
          opacityScale(totalColoredConcentration) : 0.3);
      if (mixture.precipitates && mixture.precipitates.length > 0) {
        const precipitateColor = mixture.precipitates[0].color;
        for (let i = 0; i < 50; i++) {
          svg.append("circle").attr("cx", beakerX + 10 + Math
            .random() * (beakerBodyWidth - 20)).attr("cy",
            beakerY + beakerBodyHeight - 8 - Math.random() *
            20).attr("r", 1.5 + Math.random() * 2).attr(
            "fill", precipitateColor).attr("stroke",
            "rgba(0,0,0,0.2)").attr("stroke-width", 1);
        }
      }
      beakerWrapper.append(svg.node());
      mainContentWrapper.append(beakerWrapper);
      finalMixtureOutput.append(mainContentWrapper);
      const bottomWrapper = document.createElement('div');
      bottomWrapper.style.width = '100%';
      bottomWrapper.style.display = 'flex';
      bottomWrapper.style.flexDirection = 'column';
      bottomWrapper.style.alignItems = 'center';
      const textResultsNode = document.createElement('div');
      textResultsNode.style.display = 'flex';
      textResultsNode.style.flexDirection = 'column';
      textResultsNode.style.alignItems = 'center';
      const htmlParts = [];
      const reactionOccurred = mixture.limitingReactant || mixture
        .neutralizationLR;

      if (reactionOccurred && mixture.molecularEquation) {
        let equationHTML = `<div class="equation-block">`;
        equationHTML +=
          `<div class="molecular-equation"><b>Molecular Equation</b><p>${mixture.molecularEquation}</p></div>`;
        equationHTML +=
          `<div class="molecular-equation"><b>Complete Ionic Equation</b><p>${mixture.completeIonicEquation}</p></div>`;
        equationHTML +=
          `<div class="molecular-equation"><b>Net Ionic Equation</b><p>${mixture.netIonicEquation}</p></div>`;
        equationHTML += `</div>`;
        htmlParts.push(equationHTML);
      }

      if (mixture.precipitates.length === 0 && !reactionOccurred) {
        const hasIons = mixture.finalContents.some(item => item
          .formula.includes('<sup>'));
        if (hasIons) {
          htmlParts.push(
            `<div class="saturation-alert" style="display: block; background-color: #e8f6f3; border-color: #16a085;"><strong>No Reaction Occurred: All ions are spectators.</strong></div>`
          );
        } else {
          htmlParts.push(
            `<div class="saturation-alert" style="display: block; background-color: #e8f6f3; border-color: #16a085;"><strong>No Reaction Occurred: The molecular compounds do not react.</strong></div>`
          );
        }
      } else {
        htmlParts.push(`<table class="results-table"><tbody>`);
        if (mixture.precipitates.length > 0) {
          mixture.precipitates.forEach(p => {
            htmlParts.push(
              `<tr><th>Mass of ${p.formula}(s)</th><td>${roundBankers(p.mass, sigFigs).toPrecision(sigFigs)} g</td></tr>`
            );
            htmlParts.push(
              `<tr><th style="padding-left: 20px;">&rdsh; Moles</th><td>${roundBankers(p.moles, sigFigs).toPrecision(sigFigs)} mol</td></tr>`
            );
          });
        }
        if (mixture.limitingReactant) {
          htmlParts.push(
            `<tr><th>Limiting Reactant</th><td>${mixture.limitingReactant.formula}</td></tr>`
          );
        }
        if (mixture.neutralizationLR) {
          htmlParts.push(
            `<tr><th>Limiting Reactant</th><td>${mixture.neutralizationLR.formula}</td></tr>`
          );
        }
        if (mixture.final_pH) {
          htmlParts.push(
            `<tr><th>Final pH</th><td>${mixture.final_pH.toFixed(2)}</td></tr>`
          );
        }
        htmlParts.push(`</tbody></table>`);
      }
      htmlParts.push(
        `<table class="results-table" style="margin-top: 15px;"><tbody>`
      );
      htmlParts.push(
        '<tr><th>Species</th><th>Moles</th><th>Concentration</th></tr>'
      );
      mixture.finalContents.sort((a, b) => a.formula.includes('⁻') - b
        .formula.includes('⁻')).forEach(item => {
        const formulaDisplay = item.formula.includes(
          '<sup>') ? item.formula : item.formula;
        htmlParts.push(
          `<tr><th style="padding-left: 20px;">${formulaDisplay}</th><td>${roundBankers(item.moles, sigFigs).toPrecision(sigFigs)}</td><td>${roundBankers(item.concentration, sigFigs).toPrecision(sigFigs)} <span class='unit-molar'>M</span></td></tr>`
        );
      });
      htmlParts.push(`</tbody></table>`);


      textResultsNode.innerHTML = htmlParts.join('');
      bottomWrapper.append(textResultsNode);
      const mathPanels = [];

      const createCalculationPanel = (title, latexContent) => {
        if (!latexContent) return null;

        const details = document.createElement('details');
        details.className =
          'calculation-details'; // Main style for the outer box

        const summary = document.createElement('summary');
        summary.textContent = title;

        // The math content now goes into a div that ONLY handles padding/centering
        const contentDiv = document.createElement('div');
        contentDiv.className =
          'calculation-panel-content'; // A new class for this purpose
        contentDiv.innerHTML = latexContent;

        details.append(summary);
        details.append(contentDiv); // Append the content div

        bottomWrapper.append(details);
        return contentDiv; // Return the content div for MathJax
      };

      const neutPanel = createCalculationPanel(
        "Neutralization Calculation", mixture
        .latexNeutralizationCalc);
      if (neutPanel) mathPanels.push(neutPanel);

      const precipPanel = createCalculationPanel(
        "Precipitate Calculation", mixture.latexPrecipitateCalc);
      if (precipPanel) mathPanels.push(precipPanel);

      const excessPanel = createCalculationPanel(
        "Excess Reactant Calculation", mixture
        .latexExcessReactantCalc);
      if (excessPanel) mathPanels.push(excessPanel);

      const ionPanel = createCalculationPanel(
        "Final Ion Concentration Calculations", mixture
        .latexIonCalc);
      if (ionPanel) mathPanels.push(ionPanel);

      finalMixtureOutput.append(bottomWrapper);
      if (mathPanels.length > 0 && window.MathJax) {
        setTimeout(() => {
          window.MathJax.typesetPromise(mathPanels);
        }, 0);
      }
    } else {
      finalMixtureOutput.style.display = 'none';
    }
  };


  // --- EVENT LISTENER SETUP ---
  const setupInputListeners = (id) => {
    const panel = container.querySelector(`#panel-${id}`);
    const solutionKey = id === 'a' ? 'solutionA' : 'solutionB';
    panel.querySelector(`#solute-select-${id}`).addEventListener('change', (event) => {
      appState[solutionKey].soluteFormula = event.target.value;
      mixButton.style.backgroundColor = '';
      mixButton.style.borderColor = '';
      const solute = allCompounds.find(c => c.formula === event.target.value);
      const infoBox = panel.querySelector(`#solute-info-${id}`);
      if (!solute) return;
      let solubilityText;
      if (solute.solubilityLimit === "Infinity") { solubilityText = "Miscible in water"; } 
      else if (!solute.isSoluble) { solubilityText = "Insoluble in water"; } 
      else { solubilityText = `${solute.solubilityLimit} g / 100 g H₂O`; }
      infoBox.innerHTML = `Molar Mass: ${solute.molarMass} g mol<sup>&minus;1</sup> <br> Solubility (25 °C): ${solubilityText}`;
    });
    panel.querySelector(`#amount-input-${id}`).addEventListener('input', (event) => {
      appState[solutionKey].userInputAmount = parseFloat(event.target.value) || 0;
      mixButton.style.backgroundColor = '';
      mixButton.style.borderColor = '';
    });
    panel.querySelector(`#volume-input-${id}`).addEventListener('input', (event) => {
      appState[solutionKey].userInputVolume = parseFloat(event.target.value) || 0;
      mixButton.style.backgroundColor = '';
      mixButton.style.borderColor = '';
    });
    panel.querySelectorAll(`input[name="amount-type-${id}"]`).forEach(radio => {
      radio.addEventListener('change', (event) => {
        const amountInput = panel.querySelector(`#amount-input-${id}`);
        const unitSpan = panel.querySelector(`#amount-unit-${id}`);
        appState[solutionKey].userInputType = event.target.value;
        if (event.target.value === 'mass') {
          unitSpan.textContent = '(g)';
          amountInput.value = '10.00';
          appState[solutionKey].userInputAmount = 10.00;
        } else if (event.target.value === 'moles') {
          unitSpan.textContent = '(mol)';
          amountInput.value = '1.000';
          appState[solutionKey].userInputAmount = 1.000;
        } else {
          unitSpan.innerHTML = '(mol L<sup>&minus;1</sup>)';
          amountInput.value = '0.1000';
          appState[solutionKey].userInputAmount = 0.1000;
        }
        mixButton.style.backgroundColor = '';
        mixButton.style.borderColor = '';
      });
    });
  };

  // --- INITIALIZATION ---
  setupInputListeners('a');
  setupInputListeners('b');
  mixButton.addEventListener("click", handleMix);
  
  container.addEventListener('keydown', (event) => {
    if (event.key === 'Enter') {
      event.preventDefault();
      mixButton.click();
    }
  });
  
  setTimeout(() => {
    render();
    container.querySelector("#solute-select-a").dispatchEvent(new Event('change'));
    container.querySelector("#solute-select-b").dispatchEvent(new Event('change'));
  }, 0);

  return container;
}



Learning Lab: Solution Builder
Basics
 
 

© Copyright 2025