diff --git a/scripts/_lib/assets/app.js b/scripts/_lib/assets/app.js
index 7594967..8c121f2 100644
--- a/scripts/_lib/assets/app.js
+++ b/scripts/_lib/assets/app.js
@@ -350,6 +350,10 @@
return REGION_LABELS[region] || region;
}
+ function countryLabel(cc) {
+ return COUNTRY_LABELS[cc] || cc || '';
+ }
+
function oneCountryChip(countryCode) {
const flag = COUNTRY_FLAG_EMOJI[countryCode] || '';
const name = COUNTRY_LABELS[countryCode] || '';
@@ -662,7 +666,10 @@
else if (kind === 'principal') filters.principal.delete(key);
else if (kind === 'accessory') filters.accessory.delete(key);
else if (kind === 'appellation') filters.appellations.delete(key);
- else if (kind === 'region') setRegionSelection(key, false);
+ // Region chips are country-scoped (the name alone is ambiguous — Tokaj is
+ // both Hungarian and Slovak), so the country rides along on the chip.
+ else if (kind === 'region') setSlugSelection(visibleSlugsInRegion(chip.dataset.country || '', key), false);
+ else if (kind === 'country') setSlugSelection(visibleSlugsInCountry(key), false);
else if (kind === 'mainGrapeOnly') {
filters.mainGrapeOnly = false;
const m = document.getElementById('main-grape-only'); if (m) m.checked = false;
@@ -677,7 +684,7 @@
}
});
refreshSidebarCheckedState();
- refreshRegionTriStates();
+ refreshTreeTriStates();
applyFilter();
});
@@ -837,41 +844,93 @@
buildGrapeChipFilter(container, role, set);
});
- // Map of region → list of slugs, computed once. The appellation tree
- // re-renders on spirits-toggle (entries appear/disappear), but the
- // per-region grouping itself is stable across rebuilds.
- const REGION_SLUGS = (() => {
- const m = new Map();
- const order = FACET_REGIONS.map(([r]) => r);
- for (const r of order) m.set(r, []);
- m.set('', []);
- for (const slug in AOCS) {
- const r = AOCS[slug].region || '';
- if (!m.has(r)) m.set(r, []);
- m.get(r).push(slug);
+ // Country → region → list of slugs, computed once. The tree re-renders on
+ // spirits-toggle (entries appear/disappear), but the grouping itself is
+ // stable across rebuilds.
+ //
+ // Two levels, not one: 21 countries contribute 177 regions, and a single
+ // flat list ordered by size interleaves them (FR Bourgogne, IT Toscana,
+ // GR Ελλάδα, FR Val de Loire, …) with no way to scan by country. Nesting
+ // also disambiguates the region names that repeat across a border — Tokaj
+ // is both a Hungarian borrégió and a Slovak vinohradnícka oblasť, so a
+ // region name alone is not a key.
+ //
+ // A cross-border appellation (a record carrying `country_aliases`, e.g.
+ // Maasvallei Limburg on the BE/NL border) is listed under EVERY country it
+ // spans. It stays one record with one polygon and one slug, so ticking it
+ // on either side toggles the same entry in `filters.appellations`.
+ const COUNTRY_TREE = (() => {
+ const byCountry = new Map();
+ function push(cc, region, slug) {
+ let regions = byCountry.get(cc);
+ if (!regions) { regions = new Map(); byCountry.set(cc, regions); }
+ const arr = regions.get(region);
+ if (arr) arr.push(slug); else regions.set(region, [slug]);
}
- for (const arr of m.values()) {
- arr.sort((a, b) => AOCS[a].name.localeCompare(AOCS[b].name, 'fr'));
+ for (const slug in AOCS) {
+ const rec = AOCS[slug];
+ const region = rec.region || '';
+ push(rec.country || '', region, slug);
+ for (const alias of rec.country_aliases || []) push(alias, region, slug);
+ }
+ function total(regions) {
+ let n = 0;
+ for (const arr of regions.values()) n += arr.length;
+ return n;
+ }
+ // Countries by corpus weight (the ordering every other facet uses), then
+ // by localized name. Regions the same within their country, except the
+ // "no region" residue bucket, pinned last however big it is.
+ const ordered = new Map();
+ const countries = [...byCountry.entries()].sort((a, b) =>
+ total(b[1]) - total(a[1]) || countryLabel(a[0]).localeCompare(countryLabel(b[0]), 'fr'));
+ for (const [cc, regions] of countries) {
+ const sorted = new Map([...regions.entries()].sort((a, b) =>
+ (a[0] === '' ? 1 : 0) - (b[0] === '' ? 1 : 0) ||
+ b[1].length - a[1].length ||
+ regionLabel(a[0]).localeCompare(regionLabel(b[0]), 'fr')));
+ for (const arr of sorted.values()) {
+ arr.sort((a, b) => AOCS[a].name.localeCompare(AOCS[b].name, 'fr'));
+ }
+ ordered.set(cc, sorted);
}
- return m;
+ return ordered;
})();
- function visibleSlugsInRegion(region) {
- const all = REGION_SLUGS.get(region) || [];
+ function visibleSlugsInRegion(country, region) {
+ const regions = COUNTRY_TREE.get(country);
+ const all = (regions && regions.get(region)) || [];
if (spiritsVisible()) return all;
return all.filter(s => AOCS[s].is_wine !== false);
}
- function setRegionSelection(region, on) {
- const slugs = visibleSlugsInRegion(region);
+ function visibleSlugsInCountry(country) {
+ const regions = COUNTRY_TREE.get(country);
+ if (!regions) return [];
+ const out = [];
+ for (const region of regions.keys()) {
+ for (const s of visibleSlugsInRegion(country, region)) out.push(s);
+ }
+ return out;
+ }
+
+ function setSlugSelection(slugs, on) {
for (const s of slugs) {
if (on) filters.appellations.add(s);
else filters.appellations.delete(s);
}
}
- function regionTriState(region) {
- const slugs = visibleSlugsInRegion(region);
+ // The omnisearch indexes regions by NAME (its source is the global region
+ // facet), so picking one there selects that region in every country using
+ // the name — the one place Tokaj is treated as a single entry.
+ function setRegionNameSelection(region, on) {
+ for (const [cc, regions] of COUNTRY_TREE) {
+ if (regions.has(region)) setSlugSelection(visibleSlugsInRegion(cc, region), on);
+ }
+ }
+
+ function triState(slugs) {
if (!slugs.length) return 'empty';
let n = 0;
for (const s of slugs) if (filters.appellations.has(s)) n++;
@@ -885,8 +944,9 @@
// subzona, IT sottozona, DE Einzellage). One combined number implies a legal
// precision it doesn't have — Burgundy reads 869 where the regulator
// recognises 84 appellations carrying 785 complementary designations.
- // Written by both the initial paint and the filter-driven refresh, so it
- // lives here rather than being duplicated at either call site.
+ // Written by the initial paint and the filter-driven refresh, at both the
+ // country and the region row, so it lives here rather than being duplicated
+ // at each call site.
function renderRegionCount(el, parents, subs) {
if (!el) return;
el.innerHTML = subs
@@ -902,37 +962,49 @@
const el = document.getElementById('facet-appellations');
if (!el) return; // defensive null-guard
const html = [];
- for (const [region, allSlugs] of REGION_SLUGS) {
- const slugs = spiritsVisible() ? allSlugs : allSlugs.filter(s => AOCS[s].is_wine !== false);
- if (!slugs.length) continue;
- const label = region ? regionLabel(region) : LABELS.meta_no_region;
- const items = slugs.map(slug => {
- const safeSlug = escapeAttr(slug);
- const rec = AOCS[slug];
- const nameHtml = nameWithLatin(rec);
- const checked = filters.appellations.has(slug) ? ' checked' : '';
- const openLbl = escapeAttr(fmt(LABELS.open_appellation_aria, { name: rec.name || slug }));
- return ``;
- }).join('');
- let parentCount = 0;
- for (const s of slugs) if (!AOCS[s].is_sub_denomination) parentCount++;
- const subCount = slugs.length - parentCount;
- const safeRegion = escapeAttr(region);
- // Checkbox lives outside `` (sibling of ``,
- // not a descendant) so the nested-interactive-in-summary
- // accessibility warning doesn't fire. Visual layout is restored
- // via `.region-group-wrap`'s flex rule — checkbox + disclosure
- // sit in the same row.
- html.push(`
${escapeHtml(label)}
${items}
`);
+ const selectAllAria = name => escapeAttr(`${LABELS.select_all_aria} — ${name}`);
+ for (const [cc, regions] of COUNTRY_TREE) {
+ const safeCountry = escapeAttr(cc);
+ const regionHtml = [];
+ let countryParents = 0;
+ let countryTotal = 0;
+ for (const region of regions.keys()) {
+ const slugs = visibleSlugsInRegion(cc, region);
+ if (!slugs.length) continue;
+ const items = slugs.map(slug => {
+ const safeSlug = escapeAttr(slug);
+ const rec = AOCS[slug];
+ const nameHtml = nameWithLatin(rec);
+ const checked = filters.appellations.has(slug) ? ' checked' : '';
+ const openLbl = escapeAttr(fmt(LABELS.open_appellation_aria, { name: rec.name || slug }));
+ return ``;
+ }).join('');
+ let parentCount = 0;
+ for (const s of slugs) if (!AOCS[s].is_sub_denomination) parentCount++;
+ countryParents += parentCount;
+ countryTotal += slugs.length;
+ const safeRegion = escapeAttr(region);
+ // Checkbox lives outside `` (sibling of ``,
+ // not a descendant) so the nested-interactive-in-summary
+ // accessibility warning doesn't fire. Visual layout is restored
+ // via `.region-group-wrap`'s flex rule — checkbox + disclosure
+ // sit in the same row. Both levels carry `data-country`: a region
+ // name is only unique within its country.
+ regionHtml.push(`
${escapeHtml(regionLabel(region))}
${items}
`);
+ }
+ if (!regionHtml.length) continue;
+ const flag = COUNTRY_FLAG_EMOJI[cc] || '';
+ const flagHtml = flag ? `${flag}` : '';
+ html.push(`
${flagHtml}${escapeHtml(countryLabel(cc))}
${regionHtml.join('')}
`);
}
el.innerHTML = html.join('');
- el.querySelectorAll('.region-group > summary > .count').forEach(c => {
+ el.querySelectorAll('.region-group > summary > .count, .country-group > summary > .count').forEach(c => {
renderRegionCount(c, +c.dataset.parents, +c.dataset.subs);
});
// Reapply current search visibility (so a tree rebuild during a typed
// query keeps the filtered view).
refreshFacetVisibility('facet-appellations', filters.q);
- refreshRegionTriStates();
+ refreshTreeTriStates();
}
// Single delegated listener — buildAppellationFacet may run multiple
@@ -941,25 +1013,40 @@
document.getElementById('facet-appellations')?.addEventListener('change', e => {
const el = document.getElementById('facet-appellations');
if (e.target.tagName !== 'INPUT') return;
- if (e.target.classList.contains('region-select')) {
- const region = e.target.dataset.region;
- setRegionSelection(region, e.target.checked);
- for (const inp of el.querySelectorAll(
- `.region-group[data-region="${CSS.escape(region)}"] .region-items input[type=checkbox]`
- )) {
+ // A bulk row (country or region) may share slugs with another country's
+ // subtree — a cross-border appellation is listed on both sides — so the
+ // checkbox re-sync sweeps the whole tree rather than the toggled subtree.
+ const syncItemCheckboxes = () => {
+ for (const inp of el.querySelectorAll('.region-items input[type=checkbox]')) {
inp.checked = filters.appellations.has(inp.dataset.key);
}
+ };
+ if (e.target.classList.contains('country-select')) {
+ const cc = e.target.dataset.country;
+ setSlugSelection(visibleSlugsInCountry(cc), e.target.checked);
+ syncItemCheckboxes();
+ if (e.target.checked) {
+ track('Filter Applied', { facet: 'country', value: cc || '(none)', locale: LANG });
+ }
+ } else if (e.target.classList.contains('region-select')) {
+ const region = e.target.dataset.region;
+ setSlugSelection(visibleSlugsInRegion(e.target.dataset.country, region), e.target.checked);
+ syncItemCheckboxes();
if (e.target.checked) {
track('Filter Applied', { facet: 'region', value: region || '(none)', locale: LANG });
}
} else {
const k = e.target.dataset.key;
if (e.target.checked) filters.appellations.add(k); else filters.appellations.delete(k);
+ // Mirror onto the twin row when the slug is listed under two countries.
+ for (const inp of el.querySelectorAll(`input[data-key="${CSS.escape(k)}"]`)) {
+ inp.checked = e.target.checked;
+ }
if (e.target.checked) {
track('Filter Applied', { facet: 'appellation', value: k, locale: LANG });
}
}
- refreshRegionTriStates();
+ refreshTreeTriStates();
applyFilter({ fit: true });
});
@@ -996,51 +1083,66 @@
}
});
- function refreshRegionTriStates() {
+ function refreshTreeTriStates() {
const el = document.getElementById('facet-appellations');
if (!el) return;
- el.querySelectorAll('.region-group').forEach(group => {
- const region = group.dataset.region;
- // `.region-select` is a sibling of `.region-group` inside the
- // `.region-group-wrap`, not a descendant. Reach via the parent.
- const cb = (group.parentElement || group).querySelector('.region-select');
- if (!cb) return;
- const state = regionTriState(region);
- cb.checked = state === 'checked';
- cb.indeterminate = state === 'indeterminate';
- });
+ // The bulk checkbox is a sibling of its `` inside the wrap, not
+ // a descendant (see buildAppellationFacet) — reach it via the parent.
+ const paint = (selector, cls, slugsFor) => {
+ el.querySelectorAll(selector).forEach(group => {
+ const cb = (group.parentElement || group).querySelector(cls);
+ if (!cb) return;
+ const state = triState(slugsFor(group));
+ cb.checked = state === 'checked';
+ cb.indeterminate = state === 'indeterminate';
+ });
+ };
+ paint('.region-group', '.region-select',
+ g => visibleSlugsInRegion(g.dataset.country, g.dataset.region));
+ paint('.country-group', '.country-select',
+ g => visibleSlugsInCountry(g.dataset.country));
}
function refreshFacetVisibility(containerId, q) {
const el = document.getElementById(containerId);
if (!el) return;
const nq = searchNormalize(q);
- // Appellation tree: groups + labels with data-name dataset.
- const groups = el.querySelectorAll('.region-group');
- if (groups.length) {
- groups.forEach(group => {
- let visible = 0;
- group.querySelectorAll('label').forEach(lbl => {
- const match = !nq || lbl.dataset.name.includes(nq);
- lbl.style.display = match ? '' : 'none';
- if (match) visible++;
+ // Appellation tree: country groups > region groups > labels with a
+ // data-name dataset. Auto-expand groups that hold a match while
+ // searching, but re-collapse them when the query is cleared — tracking
+ // which groups WE opened so one the user expanded by hand stays open.
+ // (Was: open-on-search with no matching collapse, leaving every group
+ // expanded after clear.)
+ const setOpenState = (group, hasMatch) => {
+ if (nq) {
+ if (hasMatch && !group.open) { group.open = true; group.dataset.autoOpened = '1'; }
+ } else if (group.dataset.autoOpened) {
+ group.open = false;
+ delete group.dataset.autoOpened;
+ }
+ };
+ const showWrap = (group, wrapClass, visible) => {
+ const wrap = group.parentElement;
+ if (wrap && wrap.classList.contains(wrapClass)) wrap.style.display = visible ? '' : 'none';
+ else group.style.display = visible ? '' : 'none';
+ };
+ const countryGroups = el.querySelectorAll('.country-group');
+ if (countryGroups.length) {
+ countryGroups.forEach(countryGroup => {
+ let countryVisible = 0;
+ countryGroup.querySelectorAll('.region-group').forEach(group => {
+ let visible = 0;
+ group.querySelectorAll('label').forEach(lbl => {
+ const match = !nq || lbl.dataset.name.includes(nq);
+ lbl.style.display = match ? '' : 'none';
+ if (match) visible++;
+ });
+ showWrap(group, 'region-group-wrap', visible);
+ setOpenState(group, visible);
+ countryVisible += visible;
});
- const wrap = group.parentElement;
- if (wrap && wrap.classList.contains('region-group-wrap')) {
- wrap.style.display = visible ? '' : 'none';
- } else {
- group.style.display = visible ? '' : 'none';
- }
- // Auto-expand matching groups while searching, but re-collapse them
- // when the query is cleared — tracking which groups WE opened so a
- // group the user expanded by hand stays open. (Was: open-on-search
- // with no matching collapse, leaving every group expanded after clear.)
- if (nq) {
- if (visible && !group.open) { group.open = true; group.dataset.autoOpened = '1'; }
- } else if (group.dataset.autoOpened) {
- group.open = false;
- delete group.dataset.autoOpened;
- }
+ showWrap(countryGroup, 'country-group-wrap', countryVisible);
+ setOpenState(countryGroup, countryVisible);
});
return;
}
@@ -1277,13 +1379,31 @@
for (const k of filters.classifications) chips.push({ kind: 'classification', key: k, label: CLASS_LABELS[k] || k });
for (const k of filters.grapesAll) chips.push({ kind: 'grapeAll', key: k, label: grapeName(k) });
if (filters.mainGrapeOnly) chips.push({ kind: 'mainGrapeOnly', key: '1', label: LABELS.main_grape_only_label });
+ // Collapse a fully-selected subtree into one chip — a whole country first,
+ // else each of its whole regions. The flag prefix disambiguates region
+ // names shared across a border, which the tree does by nesting.
const collapsed = new Set();
- for (const [region] of REGION_SLUGS) {
- const slugs = visibleSlugsInRegion(region);
- if (!slugs.length) continue;
- if (slugs.every(s => filters.appellations.has(s))) {
- chips.push({ kind: 'region', key: region, label: region ? regionLabel(region) : LABELS.meta_no_region });
- for (const s of slugs) collapsed.add(s);
+ for (const [cc, regions] of COUNTRY_TREE) {
+ const flag = COUNTRY_FLAG_EMOJI[cc] || '';
+ const inCountry = visibleSlugsInCountry(cc);
+ if (inCountry.length > 1 && inCountry.every(s => filters.appellations.has(s))) {
+ chips.push({
+ kind: 'country', key: cc, country: cc,
+ label: `${flag} ${countryLabel(cc)}`.trim(),
+ });
+ for (const s of inCountry) collapsed.add(s);
+ continue;
+ }
+ for (const region of regions.keys()) {
+ const slugs = visibleSlugsInRegion(cc, region);
+ if (slugs.length < 2) continue;
+ if (slugs.every(s => filters.appellations.has(s))) {
+ chips.push({
+ kind: 'region', key: region, country: cc,
+ label: `${flag} ${regionLabel(region)}`.trim(),
+ });
+ for (const s of slugs) collapsed.add(s);
+ }
}
}
for (const slug of filters.appellations) {
@@ -1292,17 +1412,20 @@
if (rec) chips.push({ kind: 'appellation', key: slug, label: rec.name });
}
el.innerHTML = chips.map(c => {
- const cls = c.kind === 'region' ? 'filter-chip region-chip' : 'filter-chip';
+ const bulk = c.kind === 'region' || c.kind === 'country';
+ const cls = bulk ? 'filter-chip region-chip' : 'filter-chip';
const removeAria = fmt(LABELS.remove_filter_aria, { label: c.label });
- return `${escapeHtml(c.label)}`;
+ return `${escapeHtml(c.label)}`;
}).join('');
}
function regionsSelectedCount() {
let n = 0;
- for (const [region] of REGION_SLUGS) {
- const st = regionTriState(region);
- if (st === 'checked' || st === 'indeterminate') n++;
+ for (const [cc, regions] of COUNTRY_TREE) {
+ for (const region of regions.keys()) {
+ const st = triState(visibleSlugsInRegion(cc, region));
+ if (st === 'checked' || st === 'indeterminate') n++;
+ }
}
return n;
}
@@ -1385,24 +1508,36 @@
const appEl = document.getElementById('facet-appellations');
if (appEl) {
const except = new Set(['appellations']);
- appEl.querySelectorAll('.region-group').forEach(group => {
- let visible = 0;
- let visibleParents = 0;
- group.querySelectorAll('label').forEach(lbl => {
- const inp = lbl.querySelector('input[type=checkbox]'); if (!inp) return;
- const slug = inp.dataset.key; const rec = AOCS[slug];
- const reachable = rec ? matchesExceptFacets(rec, slug, except) : false;
- const hide = !reachable && !inp.checked;
- lbl.classList.toggle('facet-unavailable', hide);
- if (!hide) {
- visible++;
- if (!(rec && rec.is_sub_denomination)) visibleParents++;
- }
+ appEl.querySelectorAll('.country-group').forEach(countryGroup => {
+ let countryVisible = 0;
+ let countryParents = 0;
+ countryGroup.querySelectorAll('.region-group').forEach(group => {
+ let visible = 0;
+ let visibleParents = 0;
+ group.querySelectorAll('label').forEach(lbl => {
+ const inp = lbl.querySelector('input[type=checkbox]'); if (!inp) return;
+ const slug = inp.dataset.key; const rec = AOCS[slug];
+ const reachable = rec ? matchesExceptFacets(rec, slug, except) : false;
+ const hide = !reachable && !inp.checked;
+ lbl.classList.toggle('facet-unavailable', hide);
+ if (!hide) {
+ visible++;
+ if (!(rec && rec.is_sub_denomination)) visibleParents++;
+ }
+ });
+ (group.parentElement || group).classList.toggle('facet-unavailable', visible === 0);
+ renderRegionCount(
+ group.querySelector(':scope > summary > .count'),
+ visibleParents, visible - visibleParents,
+ );
+ countryVisible += visible;
+ countryParents += visibleParents;
});
- (group.parentElement || group).classList.toggle('facet-unavailable', visible === 0);
+ (countryGroup.parentElement || countryGroup)
+ .classList.toggle('facet-unavailable', countryVisible === 0);
renderRegionCount(
- group.querySelector(':scope > summary > .count'),
- visibleParents, visible - visibleParents,
+ countryGroup.querySelector(':scope > summary > .count'),
+ countryParents, countryVisible - countryParents,
);
});
}
@@ -1509,13 +1644,13 @@
refreshAllGrapeChipFilters();
applyFilter({ fit: true });
} else if (type === 'region') {
- setRegionSelection(key, true);
+ setRegionNameSelection(key, true);
// Reflect the new selection in the appellation-tree checkboxes.
const appEl = document.getElementById('facet-appellations');
if (appEl) appEl.querySelectorAll('input[type=checkbox]').forEach(inp => {
if (inp.dataset.key) inp.checked = filters.appellations.has(inp.dataset.key);
});
- refreshRegionTriStates();
+ refreshTreeTriStates();
track('Omnisearch Result Picked', { type: 'region', locale: LANG });
applyFilter({ fit: true });
} else if (type === 'style') {
diff --git a/scripts/_lib/map_template.py b/scripts/_lib/map_template.py
index 8e68566..9489a70 100644
--- a/scripts/_lib/map_template.py
+++ b/scripts/_lib/map_template.py
@@ -1332,6 +1332,9 @@ def _country_name(cc: str) -> str:
# buildAppellationFacet splits each region's count into appellations
# vs complementary geographic designations.
"is_sub_denomination",
+ # The appellation tree nests region groups under a country group, and
+ # lists a cross-border appellation under every country it spans.
+ "country_aliases",
})
@@ -1954,11 +1957,12 @@ def _emit(slug: str, meta: dict, ssr: str, has_card: bool) -> None:
#sidebar > details > summary .facet-badge:empty {{ display:none }}
#sidebar > details > summary {{ display:flex; align-items:center }}
#sidebar > details > summary .facet-label {{ flex:1 }}
- .facet .region-group-wrap {{ display:flex; align-items:flex-start; gap:6px; margin:2px 0 }}
- .facet .region-group-wrap > .region-select {{ accent-color:#934050; cursor:pointer; flex:0 0 auto; margin-top:6px }}
- .facet .region-group-wrap > .region-select:checked, .facet .region-group-wrap > .region-select:indeterminate {{ accent-color:#934050 }}
- .facet .region-group-wrap > .region-group {{ flex:1 1 auto; min-width:0 }}
- .facet .region-group > summary {{ display:flex; align-items:center; gap:6px }}
+ .facet .region-group-wrap, .facet .country-group-wrap {{ display:flex; align-items:flex-start; gap:6px; margin:2px 0 }}
+ .facet .region-group-wrap > .region-select, .facet .country-group-wrap > .country-select {{ accent-color:#934050; cursor:pointer; flex:0 0 auto; margin-top:6px }}
+ .facet .region-group-wrap > .region-select:checked, .facet .region-group-wrap > .region-select:indeterminate,
+ .facet .country-group-wrap > .country-select:checked, .facet .country-group-wrap > .country-select:indeterminate {{ accent-color:#934050 }}
+ .facet .region-group-wrap > .region-group, .facet .country-group-wrap > .country-group {{ flex:1 1 auto; min-width:0 }}
+ .facet .region-group > summary, .facet .country-group > summary {{ display:flex; align-items:center; gap:6px }}
#status {{ padding:8px 16px; font-size:11px; color:#aaa; background:#222; border-bottom:1px solid #333 }}
#status .hint-action {{ background:none; border:0; padding:0; margin-left:6px; color:#9ac4ff; font:inherit; cursor:pointer; text-decoration:underline }}
#status .hint-action:hover {{ color:#cfe0ff }}
@@ -1985,18 +1989,38 @@ def _emit(slug: str, meta: dict, ssr: str, has_card: bool) -> None:
.facet .open-aoc:hover {{ color:#fff; background:#333 }}
.facet .open-aoc:focus-visible {{ opacity:1 }}
.facet label.facet-unavailable {{ display:none }}
- .facet .region-group-wrap.facet-unavailable {{ display:none }}
+ .facet .region-group-wrap.facet-unavailable, .facet .country-group-wrap.facet-unavailable {{ display:none }}
.facet .tree-row[data-depth="0"] {{ padding-left:0 }}
.facet .tree-row[data-depth="1"] {{ padding-left:14px }}
.facet .tree-row[data-depth="2"] {{ padding-left:28px }}
.facet .tree-row-parent {{ font-weight:600; color:#eee }}
.facet .tree-row[data-depth="0"]:not(:first-child) {{ margin-top:4px }}
.facet .tree-row[data-depth="2"] .name {{ color:#bbb }}
- .facet .region-group > summary {{ padding:4px 0; border-top:none; font-size:10.5px; color:#888; letter-spacing:0.06em; display:flex; align-items:center; gap:6px }}
+ /* Mixed case here too. The FR bassin labels are the join key's own
+ ALL-CAPS form only until the catalog translates them — every locale
+ including fr now carries the proper name, so the transform that used
+ to hide that inconsistency is gone. */
+ .facet .region-group > summary {{ padding:4px 0; border-top:none; font-size:11px; color:#9a9a9a; text-transform:none; letter-spacing:0.01em; display:flex; align-items:center; gap:6px }}
.facet .region-group > summary:hover {{ color:#ddd }}
.facet .region-group > summary .name {{ flex:1 }}
.facet .region-group > summary .count {{ color:#555 }}
- .facet .region-group .region-items {{ padding-left:10px }}
+ .facet .region-group .region-items {{ padding-left:8px }}
+ /* Country level sits above the regions: brighter and heavier, with the
+ flag as the scanning anchor. Regions keep their dimmer, smaller row. */
+ /* Mixed case, against the sidebar's global uppercase `summary` rule: a
+ country name reads better next to its flag, and it separates the two
+ tree levels more clearly than size alone. The region rows keep the
+ uppercase — it normalises INAO's ALL-CAPS bassin names against the
+ mixed-case regions every other country stores. */
+ .facet .country-group > summary {{ padding:5px 0; border-top:none; font-size:12px; font-weight:600; color:#c9c9c9; text-transform:none; letter-spacing:0.01em }}
+ .facet .country-group > summary:hover {{ color:#fff }}
+ .facet .country-group > summary .name {{ flex:1 }}
+ .facet .country-group > summary .country-flag {{ flex:0 0 auto; font-size:12px; line-height:1; letter-spacing:normal }}
+ .facet .country-group > summary .count {{ color:#666; font-weight:400 }}
+ /* Two levels of indent cost the appellation name ~10px of the ellipsis
+ budget, so each step is 8px rather than 10 — the disclosure marker and
+ the summary's weight carry most of the hierarchy anyway. */
+ .facet .country-group .country-items {{ padding-left:8px }}
.facet .empty {{ color:#666; font-size:11px; font-style:italic; padding:4px 0 }}
#actions {{ position:sticky; bottom:0; background:#1a1a1a; padding:10px 16px; border-top:1px solid #333; display:flex; gap:8px }}
#actions button {{ flex:1; padding:6px; background:#333; color:#eee; border:1px solid #555; border-radius:3px; cursor:pointer; font-size:12px }}