diff --git a/README.md b/README.md
index 6477100..0f8852c 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ Inputs (4 numbers)
│
▼
compute() [line 678]
- ├── Validation: flags convX > visX or any negative value (red border + clears results)
+ ├── Validation: requires safe whole numbers, visitors > 0, and conversions between 0 and visitors
├── Conversion rates: pA = xA / nA, pB = xB / nB
├── Relative improvement: (pB − pA) / pA
├── Absolute difference: pB − pA (percentage points)
@@ -149,9 +149,9 @@ All UI strings are in the `STRINGS` object at lines 555–606. Two patterns are
**Static labels (not touched by JS) — use HTML attributes:**
```html
Your English text
-Votre texte français
+Votre texte français
```
-CSS hides the inactive language automatically via `[data-lang]` on ``.
+CSS hides the inactive language automatically via `[data-lang]` on ``. JavaScript also updates the root `lang` attribute when the active language changes.
**Dynamic values rendered by JS — use `s().yourKey`:**
`s()` returns `STRINGS[currentLanguage]`, so `s().tie` gives the right string for the active language. Add your key to both `STRINGS.en` and `STRINGS.fr`, then call it from `compute()`.
@@ -162,7 +162,7 @@ CSS hides the inactive language automatically via `[data-lang]` on ``.
1. Add a display element in the HTML results area:
```html
- –
+ –
```
2. Add strings to both `STRINGS.en` and `STRINGS.fr`
3. Compute the value inside `compute()` and assign it:
@@ -171,6 +171,8 @@ CSS hides the inactive language automatically via `[data-lang]` on ``.
```
4. Call `flash($('#my-metric'))` immediately after to trigger the update animation
+Do not add live-region semantics to individual result values. After all visible results have been updated, update the existing `#confidence-status` live region once with a concise English or French summary of the completed result.
+
---
---
@@ -195,8 +197,8 @@ Importing jStat or stdlib for a single CDF call is unjustified in a zero-depende
**Live calculation, no submit button**
The audience forgets to click buttons. Live updates also make validation errors immediately visible and give instant feedback while entering numbers, which reduces data entry mistakes.
-**NaN propagation is intentional**
-When inputs are missing or invalid, `pA` and `pB` become `NaN` and every downstream result becomes `NaN`, which renders as `—`. Do not replace missing inputs with `0` — that would produce valid-looking but meaningless results (0% conversion rate is a real, distinct state).
+**Invalid input stops calculation**
+Missing or invalid input stops the calculation. The interface displays a specific, programmatically associated error for each affected field, sets `aria-invalid="true"`, clears downstream results to `—`, and updates the single result-summary live region with a concise validation summary. Do not replace missing inputs with `0` — that would produce valid-looking but meaningless results (0% conversion rate is a real, distinct state).
---
@@ -247,9 +249,6 @@ The test always checks whether B differs from A in either direction. For users w
**No correction for multiple comparisons**
If a user checks significance daily during a two-week test, the actual false positive rate inflates well above the stated confidence level. The tool provides no peeking warning.
-**Validation is visual, not blocking**
-When conversions exceed visitors, inputs turn red and results blank out — but there is no error message explaining why. A user on a small screen may miss the red border entirely.
-
**No automated tests**
All statistical logic is inside `compute()` mixed with DOM manipulation. There are no unit tests. Before modifying the math, manually verify against a reference calculator using the example values in this README.
@@ -270,7 +269,7 @@ For |z| > 6 the polynomial approximation degrades. In practice, if z exceeds 6 t
**Handle with care:**
- `phi()` — do not rewrite without verifying `phi(1.96) ≈ 0.97500` and `phi(0) = 0.5`
-- `compute()` NaN propagation — do not add default fallback values of `0` for missing inputs; that silently produces wrong results
+- `compute()` validation and result announcements — preserve the field-specific errors, `aria-invalid` state, and single result-summary live region; do not add live semantics to individual metrics
- CSS i18n rules — do not add inline `display:` styles to elements with `data-i18n-en` or `data-i18n-fr`; it will break language-specific rendering
**Recommended next improvements (priority order):**
diff --git a/css/style.css b/css/style.css
index fbd9177..2e5471c 100644
--- a/css/style.css
+++ b/css/style.css
@@ -36,6 +36,13 @@
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+.sr-only {
+ position: absolute;
+ left: -10000px;
+ top: auto;
+ width: 1px;
+ height: 1px;
+}
html, body {
background: var(--bg); color: var(--ink);
font-family: var(--font); font-size: var(--text-lg); line-height: 1.6;
@@ -70,7 +77,6 @@ html, body {
border-color: var(--border-hover);
}
.language-toggle:focus {
- outline: none;
box-shadow: 0 0 0 3px var(--focus), var(--shadow-card);
}
#lang-toggle .lbl-to-fr { display: inline; }
@@ -197,6 +203,8 @@ html, body {
.field:last-child { margin-bottom: 0; }
.field-label { display: block; font-size: var(--text-md); font-weight: 500; color: var(--ink); margin-bottom: 7px; }
.field-hint { display: block; font-size: var(--text-sm); font-weight: 400; color: var(--ink-3); margin-top: 6px; line-height: 1.4; }
+.field-error { display: block; font-size: var(--text-sm); font-weight: 500; color: var(--red); margin-top: 6px; line-height: 1.4; }
+.field-error:empty { margin-top: 0; }
input[type=number] {
width: 100%; padding: 13px 16px;
@@ -209,7 +217,7 @@ input[type=number] {
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; }
input[type=number]:hover { border-color: var(--border-hover); }
-input[type=number]:focus { outline: none; border-color: var(--brand); background: #fff; box-shadow: 0 0 0 3px var(--focus); }
+input[type=number]:focus { border-color: var(--brand); background: #fff; box-shadow: 0 0 0 3px var(--focus); }
input[type=number].err { border-color: rgba(220,38,38,.5); background: rgba(220,38,38,.03); box-shadow: 0 0 0 3px rgba(220,38,38,.12); }
/* ── Rate badge ── */
@@ -253,7 +261,7 @@ input[type=number].err { border-color: rgba(220,38,38,.5); background: rgba(220,
.adv-details summary::before { content: '▶'; font-size: 9px; transition: transform var(--tr); display: inline-block; }
.adv-details[open] summary::before { transform: rotate(90deg); }
.adv-details summary:hover { color: var(--ink-2); }
-.adv-details summary:focus { outline: none; color: var(--brand); }
+.adv-details summary:focus { color: var(--brand); }
/* ── Stat grid ── */
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); border-radius: var(--radius-md); overflow: visible; margin-top: 14px; }
@@ -263,44 +271,48 @@ input[type=number].err { border-color: rgba(220,38,38,.5); background: rgba(220,
.stat-cell-value { font-family: var(--mono); font-size: var(--text-lg); font-weight: 400; color: var(--ink); }
/* ── Interpretation table ── */
-.interp-table { width: 100%; border-collapse: collapse; min-width: 1780px; }
+.interp-table-scroll { width: 100%; max-width: 100%; overflow-x: auto; }
+.interp-table { width: 100%; border-collapse: collapse; min-width: 1080px; }
.interp-table thead th { padding: 13px 16px; text-align: left; font-size: var(--text-md); font-weight: 500; letter-spacing: 0; color: var(--ink-3); border-bottom: 2px solid var(--border); background: var(--surface-2); }
.interp-table tbody tr { border-bottom: 1px solid var(--border); transition: background var(--tr); }
.interp-table tbody tr:last-child { border-bottom: none; }
.interp-table tbody tr:hover { background: var(--surface-2); }
.interp-table th,
-.interp-table td { white-space: nowrap; }
+.interp-table td { white-space: normal; }
.interp-table td { padding: 15px 16px; vertical-align: top; font-size: var(--text-md); color: var(--ink-2); line-height: 1.55; }
-.interp-table td:first-child { font-family: var(--mono); font-size: var(--text-lg); font-weight: 500; color: var(--ink); white-space: nowrap; width: 72px; }
-.interp-table td:nth-child(2) { width: 160px; }
-.interp-table td:nth-child(3) { font-size: var(--text-md); }
+.interp-table td:first-child { font-family: var(--mono); font-size: var(--text-lg); font-weight: 500; color: var(--ink); }
+.interp-table th:nth-child(1),
+.interp-table td:nth-child(1),
+.interp-table th:nth-child(2),
+.interp-table td:nth-child(2) { white-space: nowrap; }
+.interp-table th:nth-child(3),
+.interp-table td:nth-child(3) { width: 32%; }
.interp-help { padding: 15px 18px; border-top: 1px solid var(--border); }
.interp-help p { font-size: var(--text-md); color: var(--ink-3); line-height: 1.6; }
.tsig { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font); font-size: var(--text-md); font-weight: 500; }
.tsig.ok { color: var(--green); }
.tsig.no { color: var(--red); }
.tsig::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
-@media (max-width: 1800px) {
- .interp-table {
- min-width: 1080px;
- table-layout: fixed;
- }
- .interp-table th:nth-child(1),
- .interp-table td:nth-child(1) { width: 96px; }
- .interp-table th:nth-child(2),
- .interp-table td:nth-child(2) { width: 150px; }
- .interp-table th:nth-child(4),
- .interp-table td:nth-child(4) { width: 210px; }
- .interp-table td:nth-child(3),
- .interp-table td:nth-child(4),
- .interp-table td:nth-child(5) {
- white-space: normal;
- }
-}
/* ── Tooltips ── */
.tt { position: relative; border-bottom: 0; cursor: help; }
-.tt:focus { outline: 2px solid var(--brand); border-radius: 2px; }
+.tt:focus { border-radius: 2px; }
+.language-toggle:focus,
+input[type=number]:focus,
+.adv-details summary:focus,
+.tt:focus,
+.interp-table-scroll:focus {
+ outline: 3px solid var(--brand);
+ outline-offset: 3px;
+}
+.language-toggle:focus-visible,
+input[type=number]:focus-visible,
+.adv-details summary:focus-visible,
+.tt:focus-visible,
+.interp-table-scroll:focus-visible {
+ outline: 3px solid var(--brand);
+ outline-offset: 3px;
+}
.tooltip { position: absolute; left: 0; top: calc(100% + 8px); background: var(--tooltip-bg); border: 1px solid var(--tooltip-bdr); border-radius: var(--radius-md); padding: 12px 14px; max-width: 360px; min-width: 240px; width: max-content; font-size: var(--text-xs); line-height: 1.6; color: var(--tooltip-ink); box-shadow: 0 4px 16px rgba(0,0,0,.1); z-index: 100; display: none; white-space: normal; font-family: var(--font); }
.tooltip .title { font-weight: 600; color: var(--tooltip-ink-strong); display: block; margin-bottom: 4px; font-size: var(--text-xs); }
.tooltip::before { content: ''; position: absolute; left: 12px; top: -5px; width: 8px; height: 8px; background: var(--tooltip-bg); border-left: 1px solid var(--tooltip-bdr); border-top: 1px solid var(--tooltip-bdr); transform: rotate(45deg); }
@@ -318,3 +330,56 @@ input[type=number].err { border-color: rgba(220,38,38,.5); background: rgba(220,
/* ── Flash ── */
@keyframes flash { 0%{opacity:.2} 100%{opacity:1} }
.updated { animation: flash .3s ease; }
+
+@media (max-width: 620px) {
+ .container {
+ padding: 20px 16px 48px;
+ }
+
+ .tool-section-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .language-toggle {
+ align-self: flex-start;
+ }
+
+ .rate-badge,
+ .result-row {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ }
+
+ .rate-badge-value,
+ .result-val {
+ align-self: flex-end;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ }
+
+ .result-key,
+ .card-title {
+ min-width: 0;
+ overflow-wrap: anywhere;
+ }
+
+ .stat-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .stat-cell {
+ min-width: 0;
+ }
+
+ .stat-cell-value {
+ overflow-wrap: anywhere;
+ }
+
+ .tooltip {
+ min-width: 0;
+ max-width: min(360px, calc(100vw - 80px));
+ overflow-wrap: anywhere;
+ }
+}
diff --git a/index.html b/index.html
index 0a71742..6e3da10 100644
--- a/index.html
+++ b/index.html
@@ -10,7 +10,7 @@
-
+
@@ -18,13 +18,13 @@
@@ -37,29 +37,31 @@
Calculateur de signification statistique de test A/B
Version A
Control
- Témoin
+ Témoin
@@ -70,29 +72,31 @@ Calculateur de signification statistique de test A/B
Version B
Variant
- Variante
+ Variante
@@ -104,11 +108,11 @@ Calculateur de signification statistique de test A/B
@@ -119,10 +123,10 @@
Calculateur de signification statistique de test A/B
data-tip-title-fr="Amélioration relative"
data-tip-fr="Dans quelle mesure la version B performe mieux (ou moins bien) que la version A. Formule : (Taux B − Taux A) ÷ Taux A × 100.">
Relative improvement
- Amélioration relative
+ Amélioration relative
- –
+ –
@@ -132,15 +136,15 @@ Calculateur de signification statistique de test A/B
data-tip-title-fr="Différence absolue (points de pourcentage)"
data-tip-fr="L'écart brut entre les taux de conversion. Exemple : si la version A convertit à 5 % et la version B à 7 %, la différence absolue est de +2,00 pp.">
Absolute difference
- Différence absolue
+ Différence absolue
- –
+ –
Statistical Details
- Détails statistiques
+ Détails statistiques
@@ -150,10 +154,10 @@
Calculateur de signification statistique de test A/B
data-tip-en="Combined conversion rate across both groups. Used internally to calculate the test statistic."
data-tip-title-fr="Proportion groupée"
data-tip-fr="Taux de conversion combiné des deux groupes. Utilisé en interne pour calculer la statistique de test.">
- Pooled proportionProportion groupée
+ Pooled proportionProportion groupée
-
–
+
–
@@ -162,10 +166,10 @@
Calculateur de signification statistique de test A/B
data-tip-en="How much random variation we expect in the difference between the two conversion rates."
data-tip-title-fr="Erreur standard"
data-tip-fr="La variation aléatoire attendue dans la différence entre les deux taux de conversion.">
- Standard errorErreur standard
+ Standard errorErreur standard
-
–
+
–
@@ -174,10 +178,10 @@
Calculateur de signification statistique de test A/B
data-tip-en="How many standard errors the observed difference is from zero. A larger absolute value means a more reliable result."
data-tip-title-fr="Score Z"
data-tip-fr="Le nombre d'erreurs standard qui séparent la différence observée de zéro. Une valeur absolue plus grande signifie un résultat plus fiable.">
- Z‑scoreScore Z
+ Z‑scoreScore Z
-
–
+
–
@@ -186,10 +190,10 @@
Calculateur de signification statistique de test A/B
data-tip-en="The chance that a difference this large happened randomly. Below 0.05 is generally considered reliable."
data-tip-title-fr="Valeur p"
data-tip-fr="La probabilité que cette différence soit due au hasard. En dessous de 0,05 est généralement considéré comme fiable.">
- P‑valueValeur p
+ P‑valueValeur p
-
–
+
–
@@ -197,12 +201,13 @@ Calculateur de signification statistique de test A/B