diff --git a/css/modal.css b/css/modal.css index d45002a..c25e81d 100644 --- a/css/modal.css +++ b/css/modal.css @@ -1224,6 +1224,222 @@ transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1); } +/* ── View Toggle (Bohr / Energy Levels) ── */ +#atom-view-toggle { + position: absolute; + bottom: 14px; + left: 50%; + transform: translateX(-50%); + z-index: 25; + display: none; /* shown by JS when modal opens */ + gap: 0; + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border-radius: 10px; + padding: 3px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07), 0 0 0 1px rgba(0, 0, 0, 0.05); +} + +.atom-view-btn { + border: none; + background: transparent; + color: #777; + font-size: 11.5px; + font-weight: 600; + font-family: 'Inter', sans-serif; + padding: 5px 14px; + border-radius: 7px; + cursor: pointer; + transition: all 0.2s ease; + letter-spacing: 0.2px; +} +.atom-view-btn:hover { color: #333; background: rgba(0,0,0,0.04); } +.atom-view-btn.active { background: #333; color: #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.12); } + +/* ── Energy Level Diagram Container ── */ +#energy-diagram-container { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 12; + display: none; /* toggled by JS */ + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + padding: 24px 24px 56px; + overflow-y: auto; + overflow-x: hidden; +} +#energy-diagram-container.active { + display: flex; +} + +/* ── Energy diagram internals ── */ +.energy-diagram { + display: flex; + flex-direction: column; + gap: 0; + position: relative; + width: 100%; + max-width: 480px; + margin: 0 auto; + flex-shrink: 0; +} + +/* Energy axis */ +.energy-axis-label { + position: absolute; + left: -2px; + top: 50%; + transform: translateY(-50%) rotate(-90deg); + font-size: 11px; + font-weight: 700; + color: rgba(0,0,0,0.18); + letter-spacing: 2px; + text-transform: uppercase; + pointer-events: none; +} +.energy-axis-arrow { + position: absolute; + left: 16px; + top: 8px; + bottom: 8px; + width: 1.5px; + background: linear-gradient(to top, rgba(0,0,0,0.06), rgba(0,0,0,0.16)); + border-radius: 1px; +} +.energy-axis-arrow::after { + content: ''; + position: absolute; + top: -3px; + left: -3px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 6px solid rgba(0,0,0,0.18); +} + +/* Each sublevel row */ +.energy-row { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 0 5px 32px; + position: relative; +} + +.energy-row-label { + min-width: 32px; + font-size: 13px; + font-weight: 700; + font-family: 'Inter', monospace; + text-align: right; + padding-right: 4px; + flex-shrink: 0; +} +.energy-row-label.type-s { color: #1976D2; } +.energy-row-label.type-p { color: #E65100; } +.energy-row-label.type-d { color: #2E7D32; } +.energy-row-label.type-f { color: #7B1FA2; } + +/* Orbital boxes group */ +.orbital-boxes { + display: flex; + gap: 3px; + align-items: flex-end; +} + +/* Single orbital box */ +.orbital-box { + width: 26px; + height: 32px; + border: 1.5px solid rgba(0,0,0,0.12); + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + gap: 1px; + background: rgba(255,255,255,0.7); + position: relative; + transition: border-color 0.2s, background 0.2s; +} +.orbital-box.filled { background: rgba(255,255,255,0.95); } +.orbital-box.type-s { border-color: rgba(25, 118, 210, 0.3); } +.orbital-box.type-p { border-color: rgba(230, 81, 0, 0.3); } +.orbital-box.type-d { border-color: rgba(46, 125, 50, 0.3); } +.orbital-box.type-f { border-color: rgba(123, 31, 162, 0.3); } +.orbital-box.filled.type-s { border-color: rgba(25, 118, 210, 0.5); background: rgba(25, 118, 210, 0.06); } +.orbital-box.filled.type-p { border-color: rgba(230, 81, 0, 0.5); background: rgba(230, 81, 0, 0.06); } +.orbital-box.filled.type-d { border-color: rgba(46, 125, 50, 0.5); background: rgba(46, 125, 50, 0.06); } +.orbital-box.filled.type-f { border-color: rgba(123, 31, 162, 0.5); background: rgba(123, 31, 162, 0.06); } + +/* Arrows inside box */ +.arrow-up, .arrow-down { + font-size: 15px; + line-height: 1; + font-weight: 700; +} +.arrow-up { transform: scaleX(0.8); } +.arrow-down { transform: scaleX(0.8) rotate(180deg); } +.arrow-up.type-s, .arrow-down.type-s { color: #1976D2; } +.arrow-up.type-p, .arrow-down.type-p { color: #E65100; } +.arrow-up.type-d, .arrow-down.type-d { color: #2E7D32; } +.arrow-up.type-f, .arrow-down.type-f { color: #7B1FA2; } + +/* Noble gas core label */ +.energy-core-label { + text-align: center; + font-size: 12px; + font-weight: 600; + color: rgba(0,0,0,0.35); + padding: 8px 0 4px 32px; + letter-spacing: 0.3px; +} +.energy-core-divider { + margin: 4px 32px 6px; + border: none; + border-top: 1px dashed rgba(0,0,0,0.12); +} + +/* Row electron count badge */ +.energy-row-count { + font-size: 11px; + color: rgba(0,0,0,0.3); + font-weight: 500; + margin-left: 4px; + white-space: nowrap; + flex-shrink: 0; +} + +/* Exception highlight on row */ +.energy-row.exception .orbital-boxes { + outline: 2px solid rgba(255, 171, 0, 0.4); + outline-offset: 2px; + border-radius: 6px; +} +.energy-row .exc-row-badge { + font-size: 10px; + color: #d49a00; + margin-left: 2px; +} + +/* Diagram title */ +.energy-diagram-title { + font-size: 14px; + font-weight: 700; + color: rgba(0,0,0,0.55); + text-align: center; + margin-bottom: 12px; + letter-spacing: 0.3px; +} +.energy-diagram-config { + font-size: 12px; + font-weight: 500; + color: rgba(0,0,0,0.35); + text-align: center; + margin-bottom: 16px; + font-family: 'Inter', monospace; +} + #modal-watermark { position: absolute; top: 50%; @@ -3617,3 +3833,143 @@ body.tutorial-active .level-lock-btn { pointer-events: auto !important; z-index: 9999 !important; } + +/* ============================================================================= + Exception & Anomaly Badges + ============================================================================= */ + +.exception-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + cursor: pointer; + z-index: 15; + border-radius: 50%; + background: rgb(255, 210, 0); + box-shadow: 0 0 0 2px rgba(255, 210, 0, 0.3); + transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease; + position: relative; + vertical-align: middle; + margin-left: 6px; + flex-shrink: 0; +} + +.exception-badge:hover { + background: rgb(255, 220, 40); + box-shadow: 0 0 0 3px rgba(255, 210, 0, 0.45); + transform: scale(1.15); +} + +.exception-badge-icon { + font-size: 14px; + line-height: 1; + color: #222; +} + +/* Tooltip — rendered fixed on body, floats above everything */ +.exception-tooltip { + position: fixed; + width: max-content; + max-width: 300px; + min-width: 210px; + padding: 14px 16px; + border-radius: 14px; + background: rgba(24, 24, 30, 0.97); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + color: #f0f0f0; + font-size: 12.5px; + line-height: 1.55; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.08); + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease; + transform: translateY(6px); + z-index: 99999; +} + +.exception-tooltip.visible { + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: translateY(0); +} + +/* Arrow */ +.exception-tooltip::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + background: rgba(24, 24, 30, 0.97); + transform: rotate(45deg); + z-index: -1; +} +/* Arrow bottom (tooltip above badge) */ +.exception-tooltip.arrow-bottom::after { + bottom: -5px; + right: 16px; +} +/* Arrow top (tooltip below badge) */ +.exception-tooltip.arrow-top::after { + top: -5px; + right: 16px; +} + +.exc-title { + font-weight: 700; + font-size: 13px; + color: #ffb84d; + margin-bottom: 8px; + letter-spacing: 0.2px; +} + +.exc-comparison { + background: rgba(255, 255, 255, 0.06); + border-radius: 8px; + padding: 8px 10px; + margin-bottom: 8px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.exc-predicted, +.exc-actual { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; +} + +.exc-label { + color: #999; + font-size: 11px; + font-weight: 500; + min-width: 65px; +} + +.exc-value { + font-family: 'Inter', monospace; + font-weight: 500; + color: #ccc; +} + +.exc-value.exc-highlight { + color: #4FC3F7; + font-weight: 700; +} + +.exc-predicted .exc-value { + text-decoration: line-through; + opacity: 0.6; +} + +.exc-detail { + color: #bbb; + font-size: 11.5px; + line-height: 1.55; +} diff --git a/index.html b/index.html index 684ecce..de58c73 100644 --- a/index.html +++ b/index.html @@ -1555,6 +1555,11 @@

Common Uses & Hazards

diff --git a/js/data/exceptionsData.js b/js/data/exceptionsData.js new file mode 100644 index 0000000..de2f556 --- /dev/null +++ b/js/data/exceptionsData.js @@ -0,0 +1,465 @@ +// ============================================================================= +// Periodic Table Exceptions & Anomalies Data +// Hover tooltips for properties that deviate from expected periodic trends +// ============================================================================= + +// Each entry: element atomic number → { property → exception info } +// Properties: config, ionization, electronAffinity, electronegativity, oxidation, placement, diagonal, inertPair + +export const exceptionsData = { + + // ─── Hydrogen: dual character ─── + 1: { + config: null, + placement: { + title: "Positional Anomaly", + detail: "Has 1s¹ like alkali metals (1 valence e⁻, forms H⁺), but also resembles halogens (needs 1 e⁻ for noble gas config, forms H₂). Deserves independent positioning.", + }, + oxidation: { + title: "Unusual Oxidation States", + detail: "Exhibits −1, 0, and +1 oxidation states. Acts as both an electron donor (like metals) and acceptor (like nonmetals).", + }, + }, + + // ─── Helium: s² but noble gas ─── + 2: { + placement: { + title: "Configuration vs Placement", + detail: "Has 1s² (like group 2 Be, Mg), but placed in group 18 with noble gases because it has a full valence shell, is chemically inert, and monatomic.", + }, + electronAffinity: { + title: "Zero / Positive Electron Affinity", + detail: "He has a closed-shell 1s² configuration. Adding an electron would require the 2s orbital — a much higher energy level — so EA is essentially zero or positive.", + }, + }, + + // ─── Lithium: diagonal with Mg ─── + 3: { + diagonal: { + title: "Diagonal Relationship with Mg", + detail: "Li resembles Mg more than its own group-mate Na. Similar ionic radii, similar oxide/nitride chemistry, and comparable salt behavior despite being in groups 1 and 2.", + }, + }, + + // ─── Beryllium: diagonal with Al ─── + 4: { + diagonal: { + title: "Diagonal Relationship with Al", + detail: "Be resembles Al more than Ca. Both form amphoteric oxides, tend toward covalent bonding, and share similar charge density despite being in groups 2 and 13.", + }, + ionization: { + title: "IE Anomaly: Be > B", + detail: "Be (899.5 kJ/mol) has a higher 1st IE than B (800.6 kJ/mol) despite lower Z. Removing B's 2p electron is easier than Be's tightly bound 2s² pair.", + }, + electronAffinity: { + title: "Near-Zero Electron Affinity", + detail: "Be has a filled 2s² subshell. The next electron would enter the higher-energy 2p orbital, so the process is not energetically favorable — EA is near zero or slightly positive.", + }, + }, + + // ─── Boron: diagonal with Si, IE dip ─── + 5: { + diagonal: { + title: "Diagonal Relationship with Si", + detail: "B resembles Si more than Al in some ways. Both form acidic oxides and covalent network structures, despite being in groups 13 and 14.", + }, + ionization: { + title: "IE Anomaly: B < Be", + detail: "B (800.6 kJ/mol) has lower 1st IE than Be (899.5 kJ/mol). The 2p electron in B is higher in energy and easier to remove than Be's 2s² electrons.", + }, + }, + + // ─── Nitrogen: IE higher than O, EA anomaly ─── + 7: { + ionization: { + title: "IE Anomaly: N > O", + detail: "N (1402 kJ/mol) has higher 1st IE than O (1314 kJ/mol). N's half-filled 2p³ subshell (one e⁻ per orbital) is extra stable. O must pair an electron, increasing repulsion.", + }, + electronAffinity: { + title: "Unusually Low Electron Affinity", + detail: "N has a near-zero or slightly positive first EA. Its half-filled 2p³ configuration is very stable; adding a fourth p electron breaks the symmetry and introduces significant repulsion, making the process energetically unfavorable.", + }, + }, + + // ─── Oxygen: IE dip, EA anomaly ─── + 8: { + ionization: { + title: "IE Anomaly: O < N", + detail: "O (1314 kJ/mol) has lower 1st IE than N (1402 kJ/mol), despite higher Z. The paired electron in O's 2p⁴ experiences extra repulsion, making it easier to remove.", + }, + electronAffinity: { + title: "EA Anomaly: O < S", + detail: "O has a less negative EA (−141 kJ/mol) than S (−200 kJ/mol), despite being above S in the group. The compact 2p orbitals of O cause strong electron–electron repulsion for the incoming electron, reducing the energy released compared to S's more diffuse 3p orbitals.", + }, + }, + + // ─── Fluorine: extreme electronegativity, EA anomaly ─── + 9: { + electronegativity: { + title: "Most Electronegative Element", + detail: "F (3.98 Pauling) is the most electronegative element. Its tiny atomic radius and high effective nuclear charge create extreme electron-attracting power, exceeding even oxygen.", + }, + electronAffinity: { + title: "EA Anomaly: F < Cl", + detail: "F (−328 kJ/mol) has a less negative EA than Cl (−349 kJ/mol), despite being the most electronegative element. The very compact 2p orbitals cause strong electron–electron repulsion for the added electron, partially offsetting the high nuclear charge. Cl's larger 3p orbitals have less repulsion, so more energy is released.", + }, + }, + + // ─── Neon: noble gas EA ─── + 10: { + electronAffinity: { + title: "Zero / Positive Electron Affinity", + detail: "Ne has a closed-shell 2s²2p⁶ configuration. Adding an electron would require the 3s orbital, so EA is essentially zero or positive. This applies to all noble gases.", + }, + }, + + // ─── Magnesium: IE anomaly, EA anomaly ─── + 12: { + ionization: { + title: "IE Anomaly: Mg > Al", + detail: "Mg (737.7 kJ/mol) has higher 1st IE than Al (577.5 kJ/mol). Al's 3p electron is easier to remove than Mg's tightly held 3s² pair — same s→p pattern as Be→B.", + }, + electronAffinity: { + title: "Near-Zero Electron Affinity", + detail: "Mg has a filled 3s² subshell. The next electron would enter the higher-energy 3p orbital, making the process energetically unfavorable — EA is near zero or slightly positive, same pattern as Be.", + }, + }, + + // ─── Aluminium: IE dip, diagonal ─── + 13: { + ionization: { + title: "IE Anomaly: Al < Mg", + detail: "Al (577.5 kJ/mol) has lower 1st IE than Mg (737.7 kJ/mol) despite higher Z. The 3p electron is in a higher-energy subshell and easier to remove than Mg's 3s².", + }, + diagonal: { + title: "Diagonal Relationship with Be", + detail: "Al resembles Be (group 2) more than expected. Both form amphoteric oxides, prefer covalent bonding, and have similar charge-to-radius ratios.", + }, + }, + + // ─── Silicon: diagonal with B ─── + 14: { + diagonal: { + title: "Diagonal Relationship with B", + detail: "Si shares traits with B — both form acidic oxides and network covalent structures, despite being in groups 14 and 13 respectively.", + }, + }, + + // ─── Phosphorus: IE higher than S ─── + 15: { + ionization: { + title: "IE Anomaly: P > S", + detail: "P (1011.8 kJ/mol) has higher 1st IE than S (999.6 kJ/mol). P's half-filled 3p³ subshell is extra stable, same pattern as N > O in period 2.", + }, + }, + + // ─── Sulfur: IE dip ─── + 16: { + ionization: { + title: "IE Anomaly: S < P", + detail: "S (999.6 kJ/mol) has lower 1st IE than P (1011.8 kJ/mol). The paired electron in S's 3p⁴ experiences repulsion, making it easier to remove than P's half-filled 3p³.", + }, + }, + + // ─── Chlorine: EA larger than F ─── + 17: { + electronAffinity: { + title: "EA Anomaly: Cl > F", + detail: "Cl (−349 kJ/mol) has a more negative EA than F (−328 kJ/mol), despite F being above it. The larger 3p orbitals in Cl have less electron–electron repulsion for the incoming electron, so more energy is released when attaching it.", + }, + }, + + // ─── Argon: noble gas EA ─── + 18: { + electronAffinity: { + title: "Zero / Positive Electron Affinity", + detail: "Ar has a closed-shell 3s²3p⁶ configuration. Adding an electron would require the 4s orbital, so EA is essentially zero or positive — characteristic of all noble gases.", + }, + }, + + // ─── Gallium: d-block contraction anomalies ─── + 31: { + ionization: { + title: "IE Anomaly: d-Block Contraction", + detail: "The sum of the first three IEs of Ga is higher than that of Al, even though IE usually decreases down a group. The filled 3d¹⁰ shell shields the nucleus poorly, increasing effective nuclear charge on the 4s²4p¹ electrons.", + }, + atomicRadius: { + title: "Radius Anomaly: Ga ≈ Al", + detail: "Ga has a similar or slightly smaller atomic/metallic radius than Al, contrary to the expected increase down a group. The poorly shielding 3d¹⁰ electrons increase Z_eff, pulling 4s²4p¹ electrons closer to the nucleus (d-block contraction).", + }, + electronegativity: { + title: "EN Anomaly: Ga > Al", + detail: "Ga has higher electronegativity than Al, despite being below it in group 13. Poor shielding by the filled 3d subshell increases effective nuclear charge, enhancing attraction for bonding electrons.", + }, + }, + + // ─── Germanium: d-block contraction EN anomaly ─── + 32: { + electronegativity: { + title: "EN Anomaly: Ge > Si", + detail: "Ge has a slightly higher electronegativity than Si, despite being below it in group 14. The filled 3d¹⁰ subshell shields poorly, increasing Z_eff on the valence electrons (d-block contraction).", + }, + }, + + // ─── Chromium: Aufbau exception ─── + 24: { + config: { + title: "Aufbau Exception", + predicted: "[Ar] 3d⁴ 4s²", + actual: "[Ar] 3d⁵ 4s¹", + detail: "Cr adopts a half-filled 3d⁵ subshell by moving one 4s electron to 3d. The symmetric half-filled d subshell provides extra exchange energy stabilization.", + }, + }, + + // ─── Manganese: extreme oxidation range ─── + 25: { + oxidation: { + title: "Extreme Oxidation Range", + detail: "Mn exhibits oxidation states from −3 to +7. The +7 state (in MnO₄⁻ permanganate) is unusually high, stabilized by strong covalent Mn–O bonds.", + }, + }, + + // ─── Copper: Aufbau exception ─── + 29: { + config: { + title: "Aufbau Exception", + predicted: "[Ar] 3d⁹ 4s²", + actual: "[Ar] 3d¹⁰ 4s¹", + detail: "Cu achieves a fully filled 3d¹⁰ subshell by shifting one 4s electron. The completely filled d subshell has maximum exchange stabilization energy.", + }, + }, + + // ─── Zirconium: lanthanide contraction radius anomaly ─── + 40: { + atomicRadius: { + title: "Radius Anomaly: Zr ≈ Hf", + detail: "Zr and Hf (one period below) have nearly identical atomic and ionic radii, despite the expected increase down a group. The lanthanide contraction — poor shielding by the 4f¹⁴ electrons in Hf — almost exactly cancels the normal size increase from adding a new shell.", + }, + }, + + // ─── Niobium: Aufbau exception ─── + 41: { + config: { + title: "Aufbau Exception", + predicted: "[Kr] 4d³ 5s²", + actual: "[Kr] 4d⁴ 5s¹", + detail: "Nb shifts one 5s electron to 4d. In the 4d series, the energy gap between 4d and 5s narrows, making d-filling more favorable.", + }, + }, + + // ─── Molybdenum: Aufbau exception ─── + 42: { + config: { + title: "Aufbau Exception", + predicted: "[Kr] 4d⁴ 5s²", + actual: "[Kr] 4d⁵ 5s¹", + detail: "Mo achieves a half-filled 4d⁵ subshell, mirroring Cr in the 3d series. The extra exchange energy from five unpaired d electrons outweighs the 5s pairing energy.", + }, + }, + + // ─── Ruthenium: Aufbau exception ─── + 44: { + config: { + title: "Aufbau Exception", + predicted: "[Kr] 4d⁶ 5s²", + actual: "[Kr] 4d⁷ 5s¹", + detail: "Ru shifts one 5s electron to 4d. The 4d and 5s orbitals are close in energy in mid-series, favoring more d occupation.", + }, + }, + + // ─── Rhodium: Aufbau exception ─── + 45: { + config: { + title: "Aufbau Exception", + predicted: "[Kr] 4d⁷ 5s²", + actual: "[Kr] 4d⁸ 5s¹", + detail: "Rh shifts one 5s electron to 4d. As Z increases across the 4d series, d orbitals become increasingly lower in energy than 5s.", + }, + }, + + // ─── Palladium: Aufbau exception (most dramatic) ─── + 46: { + config: { + title: "Aufbau Exception — Most Extreme", + predicted: "[Kr] 4d⁸ 5s²", + actual: "[Kr] 4d¹⁰", + detail: "Pd has a completely empty 5s subshell! Both 5s electrons move to 4d, achieving a fully filled 4d¹⁰. This is the most dramatic Aufbau violation in the periodic table.", + }, + }, + + // ─── Silver: Aufbau exception ─── + 47: { + config: { + title: "Aufbau Exception", + predicted: "[Kr] 4d⁹ 5s²", + actual: "[Kr] 4d¹⁰ 5s¹", + detail: "Ag mirrors Cu — achieves a fully filled 4d¹⁰ subshell by shifting one 5s electron. Same pattern as Cu in the 3d series.", + }, + }, + + // ─── Hafnium: lanthanide contraction radius anomaly ─── + 72: { + atomicRadius: { + title: "Radius Anomaly: Hf ≈ Zr", + detail: "Hf has nearly the same atomic radius as Zr (period above), differing by only ~1 pm. The lanthanide contraction — poor shielding by 4f¹⁴ electrons — causes a significant decrease in radius for post-lanthanide 5d elements, almost cancelling the expected size increase.", + }, + }, + + // ─── Lanthanum: f–d anomaly ─── + 57: { + config: { + title: "f–d Configuration Anomaly", + predicted: "[Xe] 4f¹ 6s²", + actual: "[Xe] 5d¹ 6s²", + detail: "La places its differentiating electron in 5d rather than 4f. At Z=57, the 5d orbital is slightly lower in energy than 4f, so 4f filling doesn't begin until Ce.", + }, + }, + + // ─── Cerium: f–d anomaly ─── + 58: { + config: { + title: "f–d Configuration Anomaly", + predicted: "[Xe] 4f² 6s²", + actual: "[Xe] 4f¹ 5d¹ 6s²", + detail: "Ce splits its electrons between 4f and 5d. The near-degeneracy of 4f and 5d at this point allows dual occupancy.", + }, + }, + + // ─── Gadolinium: Aufbau exception ─── + 64: { + config: { + title: "Aufbau Exception — Half-filled 4f", + predicted: "[Xe] 4f⁸ 6s²", + actual: "[Xe] 4f⁷ 5d¹ 6s²", + detail: "Gd achieves a half-filled 4f⁷ subshell by placing one electron in 5d instead. The exchange energy gain from seven unpaired f electrons is substantial.", + }, + }, + + // ─── Platinum: Aufbau exception ─── + 78: { + config: { + title: "Aufbau Exception", + predicted: "[Xe] 4f¹⁴ 5d⁸ 6s²", + actual: "[Xe] 4f¹⁴ 5d⁹ 6s¹", + detail: "Pt shifts one 6s electron to 5d. Relativistic effects contract 6s and expand 5d, narrowing their energy gap significantly in heavy elements.", + }, + }, + + // ─── Gold: Aufbau exception ─── + 79: { + config: { + title: "Aufbau Exception", + predicted: "[Xe] 4f¹⁴ 5d⁹ 6s²", + actual: "[Xe] 4f¹⁴ 5d¹⁰ 6s¹", + detail: "Au achieves a fully filled 5d¹⁰, mirroring Cu and Ag. Relativistic contraction of 6s makes this especially favorable, also giving gold its distinctive color.", + }, + }, + + // ─── Thallium: inert pair effect ─── + 81: { + inertPair: { + title: "Inert Pair Effect", + detail: "Tl(+1) is more stable than Tl(+3), despite being in group 13. The 6s² electrons are relativistically stabilized and resist participation in bonding.", + }, + }, + + // ─── Lead: inert pair effect, EN anomaly ─── + 82: { + inertPair: { + title: "Inert Pair Effect", + detail: "Pb(+2) is far more stable than Pb(+4). The 6s² pair is 'inert' due to poor shielding by 4f¹⁴ and relativistic contraction, unlike lighter C and Si which readily form +4.", + }, + + }, + + // ─── Bismuth: inert pair effect ─── + 83: { + inertPair: { + title: "Inert Pair Effect", + detail: "Bi(+3) is stable while Bi(+5) is a powerful oxidizer. The 6s² pair resists bonding — in contrast to lighter N and P which commonly show +5.", + }, + }, + + // ─── Actinium: f–d anomaly ─── + 89: { + config: { + title: "f–d Configuration Anomaly", + predicted: "[Rn] 5f¹ 7s²", + actual: "[Rn] 6d¹ 7s²", + detail: "Ac places its electron in 6d rather than 5f, mirroring La. The 5f orbitals are not yet low enough in energy for occupancy at Z=89.", + }, + }, + + // ─── Thorium: f–d anomaly ─── + 90: { + config: { + title: "f–d Configuration Anomaly", + predicted: "[Rn] 5f² 7s²", + actual: "[Rn] 6d² 7s²", + detail: "Th still has no 5f electrons — both go to 6d. The 5f orbitals are higher in energy than 6d for the early actinides.", + }, + }, + + // ─── Protactinium: f–d anomaly ─── + 91: { + config: { + title: "f–d Configuration Anomaly", + predicted: "[Rn] 5f³ 7s²", + actual: "[Rn] 5f² 6d¹ 7s²", + detail: "Pa splits electrons between 5f and 6d. The energy crossover between 5f and 6d occurs in this region of the actinides.", + }, + }, + + // ─── Uranium: f–d anomaly ─── + 92: { + config: { + title: "f–d Configuration Anomaly", + predicted: "[Rn] 5f⁴ 7s²", + actual: "[Rn] 5f³ 6d¹ 7s²", + detail: "U retains one 6d electron alongside 5f, similar to Pa. The close energy levels of 5f and 6d persist through the early actinides.", + }, + }, + + // ─── Neptunium: f–d anomaly ─── + 93: { + config: { + title: "f–d Configuration Anomaly", + predicted: "[Rn] 5f⁵ 7s²", + actual: "[Rn] 5f⁴ 6d¹ 7s²", + detail: "Np also retains one electron in 6d. By Pu (Z=94), the 5f orbitals finally become clearly lower in energy and normal filling resumes.", + }, + }, + + // ─── Curium: Aufbau exception ─── + 96: { + config: { + title: "Aufbau Exception — Half-filled 5f", + predicted: "[Rn] 5f⁸ 7s²", + actual: "[Rn] 5f⁷ 6d¹ 7s²", + detail: "Cm achieves a half-filled 5f⁷ subshell by placing one electron in 6d, mirroring Gd's behavior in the lanthanides.", + }, + }, + + // ─── Lawrencium: Aufbau exception ─── + 103: { + config: { + title: "Aufbau Exception", + predicted: "[Rn] 5f¹⁴ 6d¹ 7s²", + actual: "[Rn] 5f¹⁴ 7s² 7p¹", + detail: "Lr's last electron goes to 7p instead of 6d. Relativistic effects stabilize 7p₁/₂ below 6d, making this the only element where a p-block config appears in the f-block.", + }, + }, +}; + +// ─── Ionization Energy Discontinuity Pairs ─── +// Maps element Z → the comparison element and direction +export const ieDiscontinuities = { + 4: { compare: 5, direction: "higher", label: "Be > B" }, + 5: { compare: 4, direction: "lower", label: "B < Be" }, + 7: { compare: 8, direction: "higher", label: "N > O" }, + 8: { compare: 7, direction: "lower", label: "O < N" }, + 12: { compare: 13, direction: "higher", label: "Mg > Al" }, + 13: { compare: 12, direction: "lower", label: "Al < Mg" }, + 15: { compare: 16, direction: "higher", label: "P > S" }, + 16: { compare: 15, direction: "lower", label: "S < P" }, +}; diff --git a/js/modules/energyDiagram.js b/js/modules/energyDiagram.js new file mode 100644 index 0000000..9fed6b8 --- /dev/null +++ b/js/modules/energyDiagram.js @@ -0,0 +1,301 @@ +// ============================================================================= +// Energy Level Diagram Renderer +// Renders stacked orbital box-and-arrow diagrams for any element +// ============================================================================= + +import { finallyData } from "../data/elementsData.js"; +import { exceptionsData } from "../data/exceptionsData.js"; + +// Noble gas cores — config strings to abbreviate +const NOBLE_GAS_CORES = [ + { z: 86, symbol: "Rn", config: "1s² 2s² 2p⁶ 3s² 3p⁶ 3d¹⁰ 4s² 4p⁶ 4d¹⁰ 5s² 5p⁶ 4f¹⁴ 5d¹⁰ 6s² 6p⁶" }, + { z: 54, symbol: "Xe", config: "1s² 2s² 2p⁶ 3s² 3p⁶ 3d¹⁰ 4s² 4p⁶ 4d¹⁰ 5s² 5p⁶" }, + { z: 36, symbol: "Kr", config: "1s² 2s² 2p⁶ 3s² 3p⁶ 3d¹⁰ 4s² 4p⁶" }, + { z: 18, symbol: "Ar", config: "1s² 2s² 2p⁶ 3s² 3p⁶" }, + { z: 10, symbol: "Ne", config: "1s² 2s² 2p⁶" }, + { z: 2, symbol: "He", config: "1s²" }, +]; + +// Parse superscript digits +const SUP_MAP = { '⁰':0,'¹':1,'²':2,'³':3,'⁴':4,'⁵':5,'⁶':6,'⁷':7,'⁸':8,'⁹':9 }; + +function parseSuperscript(str) { + let n = 0; + for (const ch of str) { + if (SUP_MAP[ch] !== undefined) n = n * 10 + SUP_MAP[ch]; + } + return n; +} + +// Expand noble gas shorthand and parse into orbital list +// Returns { orbitals: [...], coreSymbol: "Ar"|null } +function parseConfig(configStr) { + if (!configStr) return { orbitals: [], coreSymbol: null }; + + const cores = { + '[He]': 'He', '[Ne]': 'Ne', '[Ar]': 'Ar', + '[Kr]': 'Kr', '[Xe]': 'Xe', '[Rn]': 'Rn', '[Og]': 'Og', + }; + const coreExpansions = { + 'He': '1s²', + 'Ne': '1s² 2s² 2p⁶', + 'Ar': '1s² 2s² 2p⁶ 3s² 3p⁶', + 'Kr': '1s² 2s² 2p⁶ 3s² 3p⁶ 3d¹⁰ 4s² 4p⁶', + 'Xe': '1s² 2s² 2p⁶ 3s² 3p⁶ 3d¹⁰ 4s² 4p⁶ 4d¹⁰ 5s² 5p⁶', + 'Rn': '1s² 2s² 2p⁶ 3s² 3p⁶ 3d¹⁰ 4s² 4p⁶ 4d¹⁰ 5s² 5p⁶ 4f¹⁴ 5d¹⁰ 6s² 6p⁶', + }; + + let coreSymbol = null; + let expanded = configStr; + for (const [bracket, sym] of Object.entries(cores)) { + if (expanded.includes(bracket)) { + coreSymbol = sym; + expanded = expanded.replace(bracket, coreExpansions[sym]); + break; + } + } + + const orbitals = []; + const regex = /(\d)([spdf])([\u2070\u00b9\u00b2\u00b3\u2074\u2075\u2076\u2077\u2078\u2079]+)/g; + let match; + while ((match = regex.exec(expanded)) !== null) { + orbitals.push({ + n: parseInt(match[1]), + type: match[2], + electrons: parseSuperscript(match[3]), + }); + } + return { orbitals, coreSymbol }; +} + +// Max electrons per orbital type +const MAX_ELECTRONS = { s: 2, p: 6, d: 10, f: 14 }; +const NUM_ORBITALS = { s: 1, p: 3, d: 5, f: 7 }; + +// Energy ordering for sorting (Aufbau-based, n+l rule with known exceptions) +// Lower value = lower energy = rendered at bottom +function energyOrder(n, type) { + const lMap = { s: 0, p: 1, d: 2, f: 3 }; + const l = lMap[type]; + // n+l rule, then by n for same n+l + return (n + l) * 100 + n; +} + +// Determine which orbitals belong to core vs valence +function splitCoreValence(orbitals, coreSymbol) { + if (!coreSymbol) return { core: [], valence: orbitals }; + + const coreZ = { He: 2, Ne: 10, Ar: 18, Kr: 36, Xe: 54, Rn: 86 }; + const coreElectrons = coreZ[coreSymbol] || 0; + + let count = 0; + let splitIdx = 0; + for (let i = 0; i < orbitals.length; i++) { + count += orbitals[i].electrons; + if (count >= coreElectrons) { + splitIdx = i + 1; + break; + } + } + return { + core: orbitals.slice(0, splitIdx), + valence: orbitals.slice(splitIdx), + }; +} + +// Build HTML for a single orbital box with arrows +function renderOrbitalBox(electrons, type) { + // electrons = 0, 1, or 2 for this specific box + const filled = electrons > 0 ? ' filled' : ''; + let arrows = ''; + if (electrons >= 1) arrows += ``; + if (electrons >= 2) arrows += ``; + return `
${arrows}
`; +} + +// Distribute electrons across boxes following Hund's rule +function distributeElectrons(totalElectrons, numBoxes) { + const boxes = new Array(numBoxes).fill(0); + let remaining = totalElectrons; + // First pass: one electron per box (Hund's rule) + for (let i = 0; i < numBoxes && remaining > 0; i++) { + boxes[i] = 1; + remaining--; + } + // Second pass: pair up + for (let i = 0; i < numBoxes && remaining > 0; i++) { + boxes[i] = 2; + remaining--; + } + return boxes; +} + +// Build the full energy level diagram HTML +export function renderEnergyDiagram(element) { + const container = document.getElementById('energy-diagram-container'); + if (!container) return; + + const data = finallyData?.[element.number]; + const configStr = data?.level3_properties?.electronic?.configuration; + if (!configStr) { + container.innerHTML = '
No configuration data
'; + return; + } + + const { orbitals, coreSymbol } = parseConfig(configStr); + if (orbitals.length === 0) { + container.innerHTML = '
No configuration data
'; + return; + } + + const { core, valence } = splitCoreValence(orbitals, coreSymbol); + + // Sort valence by energy (ascending = bottom to top) + const sortedValence = [...valence].sort((a, b) => energyOrder(a.n, a.type) - energyOrder(b.n, b.type)); + + // Check for exceptions + const exc = exceptionsData[element.number]; + const hasConfigException = exc?.config; + + // Build HTML + let html = ''; + html += `
Energy Level Diagram
`; + html += `
${configStr}
`; + + // Exception callout — placed at top so it's always visible + if (hasConfigException) { + html += `
`; + html += `
⚠ ${exc.config.title}
`; + if (exc.config.predicted && exc.config.actual) { + html += `
Predicted: ${exc.config.predicted}
`; + html += `
Actual: ${exc.config.actual}
`; + } + html += `
${exc.config.detail}
`; + html += '
'; + } + + html += '
'; + + // Energy axis + html += '
'; + html += '
ENERGY
'; + + // Valence rows (reversed so highest energy is at top) + const valenceReversed = [...sortedValence].reverse(); + for (const orb of valenceReversed) { + const numBoxes = NUM_ORBITALS[orb.type]; + const boxes = distributeElectrons(orb.electrons, numBoxes); + const maxE = MAX_ELECTRONS[orb.type]; + const isFull = orb.electrons === maxE; + const isHalf = orb.electrons === maxE / 2; + + // Is this the subshell involved in the exception? + const isExcRow = hasConfigException && ( + orb.type === 'd' || orb.type === 'f' || + (orb.type === 's' && orb.electrons === 1 && hasConfigException) + ); + + html += `
`; + html += `${orb.n}${orb.type}`; + html += '
'; + for (const boxE of boxes) { + html += renderOrbitalBox(boxE, orb.type); + } + html += '
'; + html += `${orb.electrons}/${maxE}`; + if (isHalf) html += ' ½'; + if (isFull) html += ' '; + html += ''; + if (isExcRow) html += ''; + html += '
'; + } + + // Core divider + label + if (coreSymbol && core.length > 0) { + html += '
'; + html += `
[${coreSymbol}] core — ${core.reduce((s, o) => s + o.electrons, 0)} electrons
`; + + // Show core rows collapsed (smaller) + const coreReversed = [...core].sort((a, b) => energyOrder(a.n, a.type) - energyOrder(b.n, b.type)).reverse(); + for (const orb of coreReversed) { + const numBoxes = NUM_ORBITALS[orb.type]; + const boxes = distributeElectrons(orb.electrons, numBoxes); + html += `
`; + html += `${orb.n}${orb.type}`; + html += '
'; + for (const boxE of boxes) { + html += renderOrbitalBox(boxE, orb.type); + } + html += '
'; + html += `${orb.electrons}/${MAX_ELECTRONS[orb.type]}`; + html += '
'; + } + } + + html += '
'; // .energy-diagram + + + + container.innerHTML = html; +} + +// Toggle between Bohr and Energy Level views +export function initViewToggle(element) { + const toggle = document.getElementById('atom-view-toggle'); + const atomContainer = document.getElementById('atom-container'); + const energyContainer = document.getElementById('energy-diagram-container'); + if (!toggle || !atomContainer || !energyContainer) return; + + toggle.style.display = 'flex'; + + // Reset to Bohr + const btns = toggle.querySelectorAll('.atom-view-btn'); + btns.forEach(b => b.classList.toggle('active', b.dataset.mode === 'bohr')); + atomContainer.style.display = ''; + atomContainer.style.opacity = '1'; + energyContainer.classList.remove('active'); + + // Clone buttons to remove old listeners + btns.forEach(btn => { + const newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + }); + + const freshBtns = toggle.querySelectorAll('.atom-view-btn'); + freshBtns.forEach(btn => { + btn.addEventListener('click', () => { + const mode = btn.dataset.mode; + freshBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + if (mode === 'energy') { + atomContainer.style.opacity = '0'; + setTimeout(() => { atomContainer.style.display = 'none'; }, 350); + renderEnergyDiagram(element); + energyContainer.classList.add('active'); + } else { + energyContainer.classList.remove('active'); + atomContainer.style.display = ''; + requestAnimationFrame(() => { atomContainer.style.opacity = '1'; }); + } + }); + }); +} + +// Cleanup on modal close +export function cleanupViewToggle() { + const toggle = document.getElementById('atom-view-toggle'); + if (toggle) toggle.style.display = 'none'; + + const energyContainer = document.getElementById('energy-diagram-container'); + if (energyContainer) { + energyContainer.classList.remove('active'); + energyContainer.innerHTML = ''; + } + + const atomContainer = document.getElementById('atom-container'); + if (atomContainer) { + atomContainer.style.display = ''; + atomContainer.style.opacity = '1'; + } +} diff --git a/js/modules/uiController.js b/js/modules/uiController.js index e4cafad..cc00a93 100644 --- a/js/modules/uiController.js +++ b/js/modules/uiController.js @@ -4,6 +4,8 @@ // ============================================================================= import { finallyData, elements } from "../data/elementsData.js"; +import { exceptionsData } from "../data/exceptionsData.js"; +import { initViewToggle, cleanupViewToggle } from "./energyDiagram.js"; import { translations } from "../data/translations.js"; import { ensureThreeLibLoaded, @@ -2229,6 +2231,198 @@ function populateSimplifiedView(element) { } } } + + // ─── Inject exception tooltips ─── + injectExceptionBadges(element); +} + +// ============================================================================= +// Exception & Anomaly Badges +// ============================================================================= +function injectExceptionBadges(element) { + // Clean up any previous badges and body-level tooltips + document.querySelectorAll('.exception-badge').forEach(el => el.remove()); + document.querySelectorAll('.exception-tooltip').forEach(el => el.remove()); + + const exc = exceptionsData[element.number]; + if (!exc) return; + + // ─── Electron Configuration exception ─── + if (exc.config) { + const configNode = document.querySelector('#l2-configuration-value'); + if (configNode) { + attachExceptionBadge(configNode, exc.config, 'config'); + } + } + + // ─── Ionization Energy exception ─── + if (exc.ionization) { + const ieItem = document.querySelector('.l3-stat-item[data-metric="ie"]'); + const ieLabel = ieItem?.querySelector('.l3-stat-label'); + if (ieLabel) { + attachExceptionBadge(ieLabel, exc.ionization, 'ionization'); + } + } + + // ─── Electronegativity exception ─── + if (exc.electronegativity) { + const enItems = document.querySelectorAll('.l3-stat-item'); + for (const item of enItems) { + const label = item.querySelector('.l3-stat-label'); + if (label && /electronegativity/i.test(label.textContent)) { + attachExceptionBadge(label, exc.electronegativity, 'electronegativity'); + break; + } + } + } + + // ─── Electron Affinity exception ─── + if (exc.electronAffinity) { + const eaItem = document.querySelector('.l3-stat-item[data-metric="ea"]'); + const eaLabel = eaItem?.querySelector('.l3-stat-label'); + if (eaLabel) { + attachExceptionBadge(eaLabel, exc.electronAffinity, 'electronAffinity'); + } + } + + // ─── Atomic Radius exception ─── + if (exc.atomicRadius) { + const arItems = document.querySelectorAll('.l3-stat-item'); + for (const item of arItems) { + const label = item.querySelector('.l3-stat-label'); + if (label && /atomic\s*radius/i.test(label.textContent)) { + attachExceptionBadge(label, exc.atomicRadius, 'atomicRadius'); + break; + } + } + } + + // ─── Oxidation State anomaly ─── + if (exc.oxidation) { + const oxLabel = document.querySelector('.oxidation-container')?.previousElementSibling; + if (oxLabel) { + attachExceptionBadge(oxLabel, exc.oxidation, 'oxidation'); + } + } + + // ─── Placement anomaly ─── + if (exc.placement) { + const typeLabel = document.querySelector('#l1-type-value'); + if (typeLabel) { + attachExceptionBadge(typeLabel, exc.placement, 'placement'); + } + } + + // ─── Diagonal relationship ─── + if (exc.diagonal) { + const groupLabel = document.querySelector('#l1-group-period-value'); + if (groupLabel) { + attachExceptionBadge(groupLabel, exc.diagonal, 'diagonal'); + } + } + + // ─── Inert Pair Effect ─── + if (exc.inertPair) { + const oxLabel = document.querySelector('.oxidation-container')?.previousElementSibling; + if (oxLabel) { + attachExceptionBadge(oxLabel, exc.inertPair, 'inertPair'); + } + } +} + +function attachExceptionBadge(parentEl, excData, type) { + if (!parentEl) return; + + // Badge icon — placed inline next to the value + const badge = document.createElement('span'); + badge.className = 'exception-badge'; + badge.dataset.excType = type; + badge.innerHTML = ``; + badge.setAttribute('aria-label', excData.title); + + // Tooltip — rendered on body so it escapes overflow:hidden containers + const tooltip = document.createElement('div'); + tooltip.className = 'exception-tooltip'; + tooltip.dataset.excType = type; + + let tooltipHtml = `
${excData.title}
`; + + // Config exceptions show predicted vs actual + if (excData.predicted && excData.actual) { + tooltipHtml += ` +
+
Predicted: ${excData.predicted}
+
Actual: ${excData.actual}
+
`; + } + + tooltipHtml += `
${excData.detail}
`; + tooltip.innerHTML = tooltipHtml; + + // Append tooltip to body, badge inline next to value + document.body.appendChild(tooltip); + parentEl.appendChild(badge); + + function positionTooltip() { + const badgeRect = badge.getBoundingClientRect(); + // Try above first + tooltip.style.left = ''; + tooltip.style.top = ''; + tooltip.classList.remove('arrow-top', 'arrow-bottom'); + + // Temporarily show to measure + tooltip.style.visibility = 'hidden'; + tooltip.style.opacity = '0'; + tooltip.style.display = 'block'; + const tipRect = tooltip.getBoundingClientRect(); + tooltip.style.display = ''; + tooltip.style.visibility = ''; + tooltip.style.opacity = ''; + + const tipW = tipRect.width; + const tipH = tipRect.height; + const gap = 10; + + // Horizontal: align right edge of tooltip with right edge of badge + let left = badgeRect.right - tipW + 20; + if (left < 8) left = 8; + if (left + tipW > window.innerWidth - 8) left = window.innerWidth - tipW - 8; + + // Vertical: prefer above + let top = badgeRect.top - tipH - gap; + if (top < 8) { + // Show below + top = badgeRect.bottom + gap; + tooltip.classList.add('arrow-top'); + } else { + tooltip.classList.add('arrow-bottom'); + } + + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; + } + + // Show/hide tooltip + badge.addEventListener('mouseenter', () => { + // Close any other open tooltips + document.querySelectorAll('.exception-tooltip.visible').forEach(t => t.classList.remove('visible')); + positionTooltip(); + tooltip.classList.add('visible'); + }); + badge.addEventListener('mouseleave', () => { + tooltip.classList.remove('visible'); + }); + + // Touch support + badge.addEventListener('click', (e) => { + e.stopPropagation(); + const isVisible = tooltip.classList.contains('visible'); + document.querySelectorAll('.exception-tooltip.visible').forEach(t => t.classList.remove('visible')); + if (!isVisible) { + positionTooltip(); + tooltip.classList.add('visible'); + } + }); } // ===== Show Modal (main element modal) ===== @@ -2803,6 +2997,7 @@ export function showModal(element) { onWindowResize(); reset3DView(); animateAtom(); + initViewToggle(element); requestAnimationFrame(() => { atomContainer.style.opacity = "1"; }); @@ -2813,6 +3008,7 @@ export function showModal(element) { } else { atomContainer.classList.remove("visible"); cleanup3D(); + cleanupViewToggle(); } }