diff --git a/docs/stylesheets/landing.css b/docs/stylesheets/landing.css
index 548984e..877b4c9 100644
--- a/docs/stylesheets/landing.css
+++ b/docs/stylesheets/landing.css
@@ -164,9 +164,35 @@
/* ── Hero ─────────────────────────────────────────────────────────── */
.quantui-landing .hero {
- background: var(--quantui-hero-bg);
+ position: relative;
+ background: transparent;
padding: 4rem 0 4.5rem;
overflow: hidden;
+ isolation: isolate;
+}
+
+/* Gradient sits behind content and fades to transparent at the edges so the
+ full-bleed hero does not show a hard rectangular cutoff against the page. */
+.quantui-landing .hero::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: var(--quantui-hero-bg);
+ pointer-events: none;
+ -webkit-mask-image:
+ linear-gradient(to right, transparent 0%, #000 10%, #000 90%, transparent 100%),
+ linear-gradient(to bottom, transparent 0%, #000 8%, #000 92%, transparent 100%);
+ -webkit-mask-composite: source-in;
+ mask-image:
+ linear-gradient(to right, transparent 0%, #000 10%, #000 90%, transparent 100%),
+ linear-gradient(to bottom, transparent 0%, #000 8%, #000 92%, transparent 100%);
+ mask-composite: intersect;
+}
+
+.quantui-landing .hero > .container {
+ position: relative;
+ z-index: 1;
}
.quantui-landing .hero__inner {
diff --git a/quantui/app.py b/quantui/app.py
index 3dd2762..46d5603 100644
--- a/quantui/app.py
+++ b/quantui/app.py
@@ -739,7 +739,8 @@ def _load_last_calibration_label() -> str:
# ── Module-level constants ────────────────────────────────────────────────────
-_APP_CSS: str = """"""
+)
_LAYOUT_TRAITS: frozenset[str] = frozenset(widgets.Layout.class_trait_names())
diff --git a/quantui/app_visualization.py b/quantui/app_visualization.py
index 41b28e5..dc4b36f 100644
--- a/quantui/app_visualization.py
+++ b/quantui/app_visualization.py
@@ -539,6 +539,9 @@ def render_traj_frame(app: Any, molecule: Any, output_widget: Any) -> None:
view = make_view(width=460, height=340)
view.addModel(xyz, "xyz")
view.setStyle({"stick": {}, "sphere": {"scale": 0.3}})
+ from quantui.visualization_py3dmol import decorate_py3dmol_coordination_bonds
+
+ decorate_py3dmol_coordination_bonds(view, molecule.atoms, molecule.coordinates)
view.setBackgroundColor("white")
view.zoomTo()
output_widget.clear_output()
@@ -2710,7 +2713,7 @@ def _frame_stepper_controls(
f'style="{btn}">▶ Play'
f''
- f''
f"{ab_html}"
diff --git a/quantui/theme.py b/quantui/theme.py
index 4e72549..c295f5e 100644
--- a/quantui/theme.py
+++ b/quantui/theme.py
@@ -232,6 +232,62 @@ def _neutral_button_css() -> str:
)
+def slider_chrome_css() -> str:
+ """High-contrast range-slider styling for ipywidgets and in-viewer scrubbers.
+
+ WebKit requires ``-webkit-slider-runnable-track`` / ``-webkit-slider-thumb``;
+ styling ``input[type=range]`` alone is often ignored. Thumb uses the accent
+ colour with a page-bg ring so it stays visible on every palette.
+ """
+ selectors = (
+ ".jupyter-widgets input[type=range], "
+ ".widget-slider input[type=range], "
+ ".widget-hslider input[type=range], "
+ "input.quantui-range[type=range]"
+ )
+ thumb = (
+ f"{selectors}::-webkit-slider-thumb, "
+ ".jupyter-widgets input[type=range]::-moz-range-thumb, "
+ ".widget-slider input[type=range]::-moz-range-thumb, "
+ ".widget-hslider input[type=range]::-moz-range-thumb, "
+ "input.quantui-range[type=range]::-moz-range-thumb"
+ )
+ track = (
+ f"{selectors}::-webkit-slider-runnable-track, "
+ ".jupyter-widgets input[type=range]::-moz-range-track, "
+ ".widget-slider input[type=range]::-moz-range-track, "
+ ".widget-hslider input[type=range]::-moz-range-track, "
+ "input.quantui-range[type=range]::-moz-range-track"
+ )
+ return (
+ f"{selectors} "
+ "{ -webkit-appearance: none !important; appearance: none !important; "
+ "width: 100% !important; height: 20px !important; "
+ "background: transparent !important; "
+ "accent-color: var(--q-accent-info) !important; "
+ "cursor: pointer !important; }\n"
+ f"{track} "
+ "{ height: 10px !important; border-radius: 5px !important; "
+ "background: var(--q-border-strong) !important; "
+ "border: 1px solid var(--q-border) !important; "
+ "box-shadow: inset 0 0 0 1px var(--q-page-bg) !important; }\n"
+ f"{thumb} "
+ "{ -webkit-appearance: none !important; appearance: none !important; "
+ "width: 20px !important; height: 20px !important; "
+ "border-radius: 50% !important; "
+ "background: var(--q-accent-info) !important; "
+ "border: 2px solid var(--q-page-bg) !important; "
+ "box-shadow: 0 0 0 1px var(--q-accent-info), "
+ "0 1px 4px rgba(0,0,0,0.45) !important; "
+ "cursor: pointer !important; margin-top: -6px !important; }\n"
+ ".widget-hslider .widget-readout, "
+ ".widget-slider .widget-readout, "
+ ".widget-float-slider .widget-readout "
+ "{ color: var(--q-text-strong) !important; "
+ "font-weight: 600 !important; min-width: 2.5em !important; }\n"
+ )
+
+
def _widget_chrome_css() -> str:
"""CSS rules for ipywidgets/Lumino chrome (palette-agnostic — uses vars)."""
surface = "var(--q-bg-panel)"
@@ -266,28 +322,7 @@ def _widget_chrome_css() -> str:
"border-color: var(--q-border) !important; }}\n"
f"{_neutral_button_css()}"
f"{_accent_button_css()}"
- ".jupyter-widgets input[type=range], "
- ".widget-slider input[type=range] "
- "{ height: 6px !important; border-radius: 3px !important; "
- "background: var(--q-border-strong) !important; "
- "accent-color: var(--q-accent-info) !important; }\n"
- ".jupyter-widgets input[type=range]::-webkit-slider-thumb, "
- ".widget-slider input[type=range]::-webkit-slider-thumb "
- "{ -webkit-appearance: none !important; width: 16px !important; "
- "height: 16px !important; border-radius: 50% !important; "
- "background: var(--q-text-strong) !important; "
- "border: 2px solid var(--q-accent-info) !important; "
- "cursor: pointer !important; }\n"
- ".jupyter-widgets input[type=range]::-moz-range-track, "
- ".widget-slider input[type=range]::-moz-range-track "
- "{ background: var(--q-border-strong) !important; "
- "height: 6px !important; border-radius: 3px !important; }\n"
- ".jupyter-widgets input[type=range]::-moz-range-thumb, "
- ".widget-slider input[type=range]::-moz-range-thumb "
- "{ width: 16px !important; height: 16px !important; "
- "border-radius: 50% !important; background: var(--q-text-strong) !important; "
- "border: 2px solid var(--q-accent-info) !important; "
- "cursor: pointer !important; }\n"
+ f"{slider_chrome_css()}"
".quantui-info-box "
f"{{ background: {surface} !important; "
"color: var(--q-text-body) !important; "
diff --git a/quantui/visualization_py3dmol.py b/quantui/visualization_py3dmol.py
index aa61a44..5235b64 100644
--- a/quantui/visualization_py3dmol.py
+++ b/quantui/visualization_py3dmol.py
@@ -12,7 +12,7 @@
import logging
import os
import tempfile
-from typing import Literal, cast
+from typing import Literal, Sequence, cast
from quantui import theme as _theme
@@ -200,11 +200,10 @@ def visualize_molecule_py3dmol(
else:
view.setStyle({style: {}})
- # MET.6: 3Dmol.js's own bond perception leaves a coordination metal as a lone
- # dot — it draws no bonds to the centre. Draw the metal↔donor bonds ourselves,
- # dashed (GaussView convention), from the same distance-based connectivity the
- # salt-warning uses. No-op for purely organic molecules.
- _add_coordination_bonds(view, molecule)
+ # MET.6: 3Dmol.js now infers some metal-ligand contacts as covalent sticks,
+ # which render as thick tapered spokes (ferrocene). Hide those and draw
+ # dashed coordination bonds ourselves (GaussView convention).
+ decorate_py3dmol_coordination_bonds(view, molecule.atoms, molecule.coordinates)
# Set background
view.setBackgroundColor(bgcolor)
@@ -220,11 +219,41 @@ def visualize_molecule_py3dmol(
# Dashed coordination-bond styling (py3Dmol addCylinder): a thin gray dashed
# cylinder from the metal centre to each donor atom.
-_COORD_BOND_RADIUS = 0.06
-_COORD_BOND_COLOR = "#777777"
+_COORD_BOND_RADIUS = 0.05
+_COORD_BOND_COLOR = "#888888"
-def _add_coordination_bonds(view, molecule) -> None:
+def _suppress_metal_coordination_sticks(
+ view,
+ atoms: Sequence[str],
+ coords: Sequence[Sequence[float]],
+) -> None:
+ """Hide 3Dmol's default sticks on metal↔donor pairs before overlaying dashes."""
+ try:
+ from quantui.connectivity import metal_coordination_bonds
+
+ for i, j in metal_coordination_bonds(atoms, coords):
+ view.setStyle({"atom": i, "bond": j}, {"stick": {"hidden": True}})
+ view.setStyle({"atom": j, "bond": i}, {"stick": {"hidden": True}})
+ except Exception: # noqa: BLE001 — bond decoration must never break the viewer
+ logger.debug("coordination-stick suppression skipped", exc_info=True)
+
+
+def decorate_py3dmol_coordination_bonds(
+ view,
+ atoms: Sequence[str],
+ coords: Sequence[Sequence[float]],
+) -> None:
+ """Hide metal-ligand sticks and draw dashed coordination bonds (MET.6)."""
+ _suppress_metal_coordination_sticks(view, atoms, coords)
+ _add_coordination_bonds(view, atoms, coords)
+
+
+def _add_coordination_bonds(
+ view,
+ atoms: Sequence[str],
+ coords: Sequence[Sequence[float]],
+) -> None:
"""Draw dashed metal↔donor cylinders so a metal centre isn't a lone dot.
Uses the distance-based, metal-aware connectivity finder. Best-effort: any
@@ -233,8 +262,7 @@ def _add_coordination_bonds(view, molecule) -> None:
try:
from quantui.connectivity import metal_coordination_bonds
- coords = molecule.coordinates
- bonds = metal_coordination_bonds(molecule.atoms, coords)
+ bonds = metal_coordination_bonds(atoms, coords)
for i, j in bonds:
xi, yi, zi = coords[i]
xj, yj, zj = coords[j]
@@ -245,8 +273,8 @@ def _add_coordination_bonds(view, molecule) -> None:
"radius": _COORD_BOND_RADIUS,
"color": _COORD_BOND_COLOR,
"dashed": True,
- "fromCap": 1,
- "toCap": 1,
+ "fromCap": 0,
+ "toCap": 0,
}
)
except Exception: # noqa: BLE001 — bond decoration must never break the viewer
diff --git a/tests/test_metal_viewer_fallback.py b/tests/test_metal_viewer_fallback.py
index 13b9c91..fdf63d0 100644
--- a/tests/test_metal_viewer_fallback.py
+++ b/tests/test_metal_viewer_fallback.py
@@ -91,6 +91,16 @@ def test_metal_html_has_dashed_cylinders(self):
assert "addCylinder" in html # coordination bonds drawn
assert "dashed" in html
+ def test_ferrocene_hides_default_metal_sticks(self):
+ pytest.importorskip("py3Dmol")
+ from quantui.visualization_py3dmol import render_molecule_html
+
+ html = render_molecule_html(
+ _mol("inorganic-ferrocene"), backend="py3dmol", width=300, height=250
+ )
+ assert '"hidden": true' in html
+ assert html.count("addCylinder") == 10
+
def test_organic_html_has_no_cylinders(self):
pytest.importorskip("py3Dmol")
from quantui.visualization_py3dmol import render_molecule_html
diff --git a/tests/test_theme_contrast.py b/tests/test_theme_contrast.py
index f2c00ab..3a22160 100644
--- a/tests/test_theme_contrast.py
+++ b/tests/test_theme_contrast.py
@@ -135,6 +135,7 @@ def test_app_css_uses_css_variables(self):
assert "var(--q-accent-info)" in _APP_CSS
assert "var(--q-page-bg)" in _APP_CSS
assert ".widget-dropdown select" in _APP_CSS
+ assert "-webkit-slider-runnable-track" in _APP_CSS
def test_theme_css_block_styles_widget_tabs(self):
block = theme.theme_css_block("Dark")
@@ -145,6 +146,12 @@ def test_theme_css_block_styles_widget_tabs(self):
assert "var(--q-bg-panel)" in block
assert ":not(.mod-danger)" in block
+ def test_theme_css_block_styles_sliders_with_webkit_track(self):
+ block = theme.theme_css_block("Dark")
+ assert "-webkit-slider-runnable-track" in block
+ assert "quantui-range" in block
+ assert "var(--q-accent-info)" in block
+
def test_app_css_preserves_accent_buttons(self):
from quantui.app import _APP_CSS